From 77094dd01f0c6ff59b4fb4fa1105addf34b2398c Mon Sep 17 00:00:00 2001
From: doum <doum>
Date: 星期二, 16 六月 2026 18:49:03 +0800
Subject: [PATCH] 新增智能电表、空调管理

---
 h5/pages/customer/contract/detail.vue                                                                                 |  596 ++++
 h5/pages/customer/login.vue                                                                                           |   73 
 server/visits/dmvisit_service/src/main/java/com/doumee/service/business/impl/YwCustomerH5AuthServiceImpl.java         |   63 
 admin/src/assets/style/style.scss                                                                                     |   50 
 h5/pages/index.vue                                                                                                    |  111 
 h5/pages/login.vue                                                                                                    |  121 
 h5/pages/customer/electricity/list.vue                                                                                |   96 
 admin/src/views/business/ywcustomerrecharge.vue                                                                       |    4 
 server/visits/dmvisit_service/src/main/java/com/doumee/dao/business/model/YwWxPayOrder.java                           |   54 
 server/visits/dmvisit_service/src/main/java/com/doumee/service/business/impl/YwH5BannerServiceImpl.java               |  169 +
 server/system_service/src/main/java/com/doumee/config/cloudfilter/LoginHandlerInterceptor.java                        |   19 
 server/visits/dmvisit_service/src/main/java/com/doumee/core/haikang/model/HKConstants.java                            |    1 
 admin/src/views/client/components/OperaYwCustomerWindow.vue                                                           |   83 
 server/visits/dmvisit_service/src/main/java/com/doumee/service/business/impl/YwCustomerH5BizServiceImpl.java          |  752 +++++
 h5/styles/customer.scss                                                                                               | 2024 ++++++++++++++
 server/visits/dmvisit_service/src/main/java/com/doumee/dao/business/model/YwContractBill.java                         |   17 
 server/visits/dmvisit_service/src/main/java/com/doumee/dao/business/dto/h5/CustomerContractQueryDTO.java              |    8 
 server/visits/dmvisit_service/src/main/java/com/doumee/dao/business/dto/h5/CustomerDeviceQueryDTO.java                |    9 
 h5/pages.json                                                                                                         |  225 
 server/visits/dmvisit_service/src/main/java/com/doumee/dao/business/dto/h5/CustomerPayCreateDTO.java                  |   16 
 server/visits/dmvisit_service/src/main/java/com/doumee/dao/business/model/YwH5Banner.java                             |   31 
 server/visits/dmvisit_service/src/main/java/com/doumee/service/business/impl/YwMaterialServiceImpl.java               |    1 
 h5/utils/config.js                                                                                                    |   19 
 server/visits/dmvisit_service/src/main/java/com/doumee/service/business/impl/MemberServiceImpl.java                   |  142 
 server/visits/dmvisit_service/src/main/java/com/doumee/service/business/YwH5BannerService.java                        |   25 
 server/visits/dmvisit_admin/src/main/resources/application.yml                                                        |    6 
 h5/utils/service.js                                                                                                   |   34 
 server/visits/dmvisit_service/src/main/java/com/doumee/dao/business/dto/EditRecordDataVO.java                         |    9 
 server/visits/dmvisit_service/src/main/java/com/doumee/dao/business/model/YwContract.java                             |   18 
 h5/utils/loginSms.js                                                                                                  |   32 
 server/visits/dmvisit_service/src/main/java/com/doumee/dao/business/dto/h5/CustomerBillQueryDTO.java                  |    8 
 admin/src/views/business/components/YwCustomerDeviceWindow.vue                                                        |   20 
 server/visits/dmvisit_service/src/main/java/com/doumee/service/business/impl/YwContractRevenueServiceImpl.java        |    3 
 h5/utils/roleSwitch.js                                                                                                |   19 
 server/visits/dmvisit_service/src/main/java/com/doumee/service/business/impl/SmsEmailServiceImpl.java                 |  143 
 server/visits/dmvisit_service/src/main/java/com/doumee/service/business/impl/YwContractServiceImpl.java               |   14 
 server/visits/dmvisit_service/src/main/java/com/doumee/dao/business/model/YwCustomerElectrical.java                   |    6 
 h5/pages/customer/electricity/recharge.vue                                                                            |   79 
 h5/pages/customer/index.vue                                                                                           |   81 
 server/visits/dmvisit_service/src/main/java/com/doumee/dao/business/model/YwElectricalCharge.java                     |    2 
 server/visits/dmvisit_service/src/main/java/com/doumee/dao/business/model/YwContractRevenue.java                      |    7 
 server/visits/dmvisit_service/src/main/java/com/doumee/service/business/YwCustomerWxPayService.java                   |   16 
 admin/src/views/business/components/YwCustomerConditionerTab.vue                                                      |   27 
 admin/src/views/business/components/YwH5BannerEdit.vue                                                                |  147 +
 admin/src/views/business/ywh5banner.vue                                                                               |  182 +
 h5/api/customer.js                                                                                                    |   23 
 h5/utils/wechatAuth.js                                                                                                |   55 
 server/visits/dmvisit_service/src/main/java/com/doumee/service/business/impl/YwCustomerWxPayServiceImpl.java          |  257 +
 h5/pages/customer/bill/list.vue                                                                                       |  263 +
 server/visits/dmvisit_admin/src/main/resources/bootstrap.yml                                                          |    2 
 server/visits/dmvisit_service/src/main/java/com/doumee/dao/business/dto/h5/CustomerRechargeRecordH5QueryDTO.java      |   13 
 server/visits/dmvisit_service/src/main/java/com/doumee/dao/business/model/YwCustomer.java                             |    3 
 h5/pages/customer/contract/list.vue                                                                                   |  103 
 h5/pages/customer/recharge/record.vue                                                                                 |   84 
 h5/pages/roleSelect.vue                                                                                               |   49 
 server/visits/dmvisit_service/src/main/java/com/doumee/service/business/YwCustomerDeviceAutoBindService.java          |   18 
 admin/src/views/client/staffList.vue                                                                                  |    5 
 h5/store/index.js                                                                                                     |   10 
 h5/pages/customer/pay/result.vue                                                                                      |   34 
 server/visits/dmvisit_service/src/main/java/com/doumee/service/business/SmsEmailService.java                          |    3 
 h5/pages/customer/conditioner/recharge.vue                                                                            |   77 
 server/visits/dmvisit_service/src/main/java/com/doumee/service/business/impl/YwCustomerServiceImpl.java               |    3 
 h5/pages/customer/bill/pay.vue                                                                                        |   60 
 server/visits/dmvisit_service/src/main/java/com/doumee/dao/business/YwH5BannerMapper.java                             |    7 
 server/visits/dmvisit_service/src/main/java/com/doumee/service/business/impl/YwCustomerRechargeBizServiceImpl.java    |    1 
 server/system_service/src/main/java/com/doumee/dao/business/model/SmsEmail.java                                       |    4 
 server/db/business.yw_h5_banner.menu.sql                                                                              |   35 
 server/visits/dmvisit_admin/src/main/java/com/doumee/cloud/web/YwCustomerH5Controller.java                            |  212 +
 server/db/business.yw_h5_banner.permissions.sql                                                                       |   17 
 server/visits/dmvisit_service/src/main/java/com/doumee/dao/business/dto/h5/CustomerDeviceH5VO.java                    |   24 
 h5/pages/customer/bill/detail.vue                                                                                     |  330 ++
 h5/api/index.js                                                                                                       |    1 
 server/visits/dmvisit_service/src/main/java/com/doumee/dao/business/YwWxPayOrderMapper.java                           |    7 
 server/system_service/src/main/java/com/doumee/config/handler/GlobalExceptionHandler.java                             |   33 
 server/visits/dmvisit_service/src/main/java/com/doumee/service/business/YwCustomerH5BizService.java                   |   36 
 server/visits/dmvisit_service/src/main/java/com/doumee/dao/business/dto/YwCustomerRechargeRecordQueryDTO.java         |    1 
 server/visits/dmvisit_service/src/main/java/com/doumee/core/wx/WxJsapiPayUtil.java                                    |  135 
 /dev/null                                                                                                             |   50 
 server/visits/dmvisit_service/src/main/java/com/doumee/service/business/YwCustomerH5AuthService.java                  |    3 
 server/db/business.yw_customer_h5.sql                                                                                 |   70 
 h5/utils/wxpay.js                                                                                                     |   22 
 server/pom.xml                                                                                                        |    4 
 server/visits/dmvisit_service/src/main/java/com/doumee/service/business/impl/YwCustomerDeviceAutoBindServiceImpl.java |  218 +
 admin/src/api/business/ywh5banner.js                                                                                  |   27 
 server/visits/dmvisit_admin/src/main/java/com/doumee/cloud/admin/YwH5BannerCloudController.java                       |   78 
 h5/App.vue                                                                                                            |    1 
 admin/src/views/business/components/YwCustomerElectricalTab.vue                                                       |   12 
 admin/src/views/client/components/staffEdit.vue                                                                       |  303 +
 88 files changed, 7,726 insertions(+), 529 deletions(-)

diff --git a/admin/src/api/business/ywh5banner.js b/admin/src/api/business/ywh5banner.js
new file mode 100644
index 0000000..d7ef981
--- /dev/null
+++ b/admin/src/api/business/ywh5banner.js
@@ -0,0 +1,27 @@
+import request from '../../utils/request'
+
+const base = '/visitsAdmin/cloudService/business/ywH5Banner'
+
+export function fetchList (data) {
+  return request.post(base + '/page', data, { trim: true })
+}
+
+export function create (data) {
+  return request.post(base + '/create', data)
+}
+
+export function getInfoById (id) {
+  return request.get(base + '/' + id)
+}
+
+export function updateById (data) {
+  return request.post(base + '/updateById', data)
+}
+
+export function deleteById (id) {
+  return request.get(base + '/delete/' + id)
+}
+
+export function deleteByIdInBatch (ids) {
+  return request.get(base + '/delete/batch', { params: { ids } })
+}
diff --git a/admin/src/assets/style/style.scss b/admin/src/assets/style/style.scss
index afecdf3..d28370f 100644
--- a/admin/src/assets/style/style.scss
+++ b/admin/src/assets/style/style.scss
@@ -254,7 +254,7 @@
   margin-bottom: 20px;
 }
 .red{
-  color: red !important;
+  color: #f56c6c !important;
 }
 .green{
   color: #00BA67;
@@ -348,3 +348,51 @@
 .tac{
   text-align: center;
 }
+
+/* 鍒犻櫎鎸夐挳缁熶竴鏍峰紡 */
+$btn-delete-color: #f56c6c;
+$btn-delete-hover: #f78989;
+$btn-delete-bg: #fef0f0;
+$btn-delete-border: #fbc4c4;
+
+.el-button.btn-delete,
+.el-button--text.btn-delete {
+  color: $btn-delete-color !important;
+
+  &:hover,
+  &:focus {
+    color: $btn-delete-hover !important;
+  }
+}
+
+.el-button--text:has(.el-icon-delete) {
+  color: $btn-delete-color;
+
+  &:hover,
+  &:focus {
+    color: $btn-delete-hover;
+  }
+}
+
+.el-button:not(.el-button--text):not(.el-button--primary):not(.el-button--success):not(.el-button--warning):not(.el-button--info):not(.el-button--danger):not(.is-disabled):has(.el-icon-delete) {
+  color: $btn-delete-color;
+  border-color: $btn-delete-border;
+  background-color: $btn-delete-bg;
+
+  &:hover,
+  &:focus {
+    color: #fff;
+    background-color: $btn-delete-color;
+    border-color: $btn-delete-color;
+  }
+}
+
+span.btn-delete,
+.cu.btn-delete {
+  color: $btn-delete-color !important;
+  cursor: pointer;
+
+  &:hover {
+    color: $btn-delete-hover !important;
+  }
+}
diff --git a/admin/src/views/business/components/YwCustomerConditionerTab.vue b/admin/src/views/business/components/YwCustomerConditionerTab.vue
index 7a1e404..f0e4219 100644
--- a/admin/src/views/business/components/YwCustomerConditionerTab.vue
+++ b/admin/src/views/business/components/YwCustomerConditionerTab.vue
@@ -12,6 +12,7 @@
                   :min="0"
                   :precision="2"
                   :step="10"
+                  :disabled="readonly"
                   controls-position="right"
                   class="stop-money-input"
                 />
@@ -23,12 +24,12 @@
         <el-row :gutter="24">
           <el-col :span="12">
             <el-form-item label="璁¤垂寮�鍏�" prop="isPwr">
-              <el-switch v-model="form.isPwr" :active-value="1" :inactive-value="0" active-text="寮�鍚�" inactive-text="鍏抽棴"/>
+              <el-switch v-model="form.isPwr" :active-value="1" :inactive-value="0" :disabled="readonly" active-text="寮�鍚�" inactive-text="鍏抽棴"/>
             </el-form-item>
           </el-col>
           <el-col :span="12">
             <el-form-item label="18:00-09:00 涓嶅仠鏈�" prop="isRestStop">
-              <el-switch v-model="form.isRestStop" :active-value="1" :inactive-value="0" active-text="鏄�" inactive-text="鍚�"/>
+              <el-switch v-model="form.isRestStop" :active-value="1" :inactive-value="0" :disabled="readonly" active-text="鏄�" inactive-text="鍚�"/>
             </el-form-item>
           </el-col>
         </el-row>
@@ -38,6 +39,7 @@
             type="textarea"
             :rows="2"
             maxlength="500"
+            :disabled="readonly"
             show-word-limit
             placeholder="璇疯緭鍏ュ娉紙閫夊~锛�"
           />
@@ -46,11 +48,11 @@
 
       <section class="config-section">
         <div class="section-header">
-          <span class="section-title required-title">鍏宠仈鍐呮満</span>
-          <el-button type="primary" size="small" icon="el-icon-plus" @click="openSelector">娣诲姞鍐呮満</el-button>
+          <span class="section-title" :class="{ 'required-title': !readonly }">鍏宠仈鍐呮満</span>
+          <el-button v-if="!readonly" type="primary" size="small" icon="el-icon-plus" @click="openSelector">娣诲姞鍐呮満</el-button>
         </div>
         <el-form-item prop="conditioners" label-width="0" class="conditioners-form-item">
-          <el-table :data="form.conditioners" stripe size="small" class="device-table" empty-text="鏆傛湭鍏宠仈鍐呮満锛岃鐐瑰嚮娣诲姞">
+          <el-table :data="form.conditioners" stripe size="small" class="device-table" :empty-text="readonly ? '鏆傛棤鍏宠仈鍐呮満' : '鏆傛湭鍏宠仈鍐呮満锛岃鐐瑰嚮娣诲姞'">
             <el-table-column label="璁惧" min-width="200" align="left" show-overflow-tooltip>
               <template slot-scope="{ row }">{{ deviceLabel(row) }}</template>
             </el-table-column>
@@ -62,10 +64,11 @@
             </el-table-column>
             <el-table-column label="鐢佃垂鍗犳瘮%" min-width="130" align="center">
               <template slot-scope="{ row }">
-                <el-input-number v-model="row.devRatio" :min="1" :max="100" size="small" controls-position="right"/>
+                <el-input-number v-if="!readonly" v-model="row.devRatio" :min="1" :max="100" size="small" controls-position="right"/>
+                <span v-else>{{ row.devRatio != null ? row.devRatio : '-' }}</span>
               </template>
             </el-table-column>
-            <el-table-column label="鎿嶄綔" width="80" align="center" fixed="right">
+            <el-table-column v-if="!readonly" label="鎿嶄綔" width="80" align="center" fixed="right">
               <template slot-scope="{ $index }">
                 <el-button type="text" class="red" @click="removeConditioner($index)">绉婚櫎</el-button>
               </template>
@@ -75,11 +78,11 @@
       </section>
     </el-form>
 
-    <div class="footer-btns">
+    <div v-if="!readonly" class="footer-btns">
       <el-button type="primary" :loading="saving" v-permissions="['business:ywcustomerrecharge:bindDevice']" @click="save">淇濆瓨閰嶇疆</el-button>
     </div>
 
-    <GlobalWindow title="閫夋嫨绌鸿皟鍐呮満" :visible.sync="selectorVisible" width="780px" @confirm="confirmSelect">
+    <GlobalWindow v-if="!readonly" title="閫夋嫨绌鸿皟鍐呮満" :visible.sync="selectorVisible" width="780px" @confirm="confirmSelect">
       <el-form inline @submit.native.prevent class="selector-form">
         <el-form-item label="鍏抽敭瀛�">
           <el-input v-model="selectorKeyword" placeholder="鍚嶇О/缂栧彿" clearable @keypress.enter.native="searchDevices"/>
@@ -110,7 +113,11 @@
   components: { GlobalWindow, Pagination },
   props: {
     customerId: Number,
-    active: Boolean
+    active: Boolean,
+    readonly: {
+      type: Boolean,
+      default: false
+    }
   },
   data () {
     return {
diff --git a/admin/src/views/business/components/YwCustomerDeviceWindow.vue b/admin/src/views/business/components/YwCustomerDeviceWindow.vue
index d8e873a..a96a320 100644
--- a/admin/src/views/business/components/YwCustomerDeviceWindow.vue
+++ b/admin/src/views/business/components/YwCustomerDeviceWindow.vue
@@ -1,5 +1,6 @@
 <template>
   <GlobalWindow title="鍏宠仈璁惧" :visible.sync="visible" width="920px" :show-confirm="false">
+    <div v-if="readonly" class="readonly-tip">璁惧鐢辩璧佸悎鍚岃嚜鍔ㄥ叧鑱旓紝浠呮敮鎸佹煡鐪�</div>
     <div class="merchant-info">
       <div class="merchant-info__item">
         <span class="merchant-info__label">瀹㈡埛绫诲瀷</span>
@@ -20,10 +21,10 @@
     </div>
     <el-tabs v-model="activeTab" class="device-tabs">
       <el-tab-pane label="鍏宠仈鐢佃〃" name="electrical">
-        <YwCustomerElectricalTab :customer-id="customer.id" :active="activeTab === 'electrical'" @success="$emit('success')"/>
+        <YwCustomerElectricalTab :customer-id="customer.id" :active="activeTab === 'electrical'" :readonly="readonly"/>
       </el-tab-pane>
       <el-tab-pane label="鍏宠仈绌鸿皟" name="conditioner">
-        <YwCustomerConditionerTab :customer-id="customer.id" :active="activeTab === 'conditioner'" @success="$emit('success')"/>
+        <YwCustomerConditionerTab :customer-id="customer.id" :active="activeTab === 'conditioner'" :readonly="readonly"/>
       </el-tab-pane>
     </el-tabs>
   </GlobalWindow>
@@ -37,6 +38,13 @@
 export default {
   name: 'YwCustomerDeviceWindow',
   components: { GlobalWindow, YwCustomerElectricalTab, YwCustomerConditionerTab },
+  props: {
+    /** 鍙鏌ョ湅锛堝悎鍚岃嚜鍔ㄥ叧鑱旓紝涓嶅彲鎵嬪姩澧炲垹锛� */
+    readonly: {
+      type: Boolean,
+      default: true
+    }
+  },
   data () {
     return {
       visible: false,
@@ -94,4 +102,12 @@
 .device-tabs {
   margin-top: 4px;
 }
+.readonly-tip {
+  margin-bottom: 12px;
+  padding: 8px 12px;
+  font-size: 13px;
+  color: #909399;
+  background: #fdf6ec;
+  border-radius: 4px;
+}
 </style>
diff --git a/admin/src/views/business/components/YwCustomerElectricalTab.vue b/admin/src/views/business/components/YwCustomerElectricalTab.vue
index 4282abc..3e58fcf 100644
--- a/admin/src/views/business/components/YwCustomerElectricalTab.vue
+++ b/admin/src/views/business/components/YwCustomerElectricalTab.vue
@@ -1,6 +1,6 @@
 <template>
   <div>
-    <div class="toolbar-row">
+    <div v-if="!readonly" class="toolbar-row">
       <el-button type="primary" size="small" v-permissions="['business:ywcustomerrecharge:bindDevice']" @click="openSelector">鍘婚�夋嫨鐢佃〃</el-button>
     </div>
     <el-table v-loading="loading" :data="list" stripe size="small">
@@ -16,7 +16,7 @@
       <el-table-column label="缁х數鍣�" min-width="80" align="center">
         <template slot-scope="{ row }">{{ relayText(row.relayStatus) }}</template>
       </el-table-column>
-      <el-table-column label="鎿嶄綔" min-width="80" align="center">
+      <el-table-column v-if="!readonly" label="鎿嶄綔" min-width="80" align="center">
         <template slot-scope="{ row }">
           <el-button type="text" class="red" v-permissions="['business:ywcustomerrecharge:bindDevice']" @click="remove(row)">绉婚櫎</el-button>
         </template>
@@ -24,7 +24,7 @@
     </el-table>
     <pagination small @size-change="onSizeChange" @current-change="onPageChange" :pagination="pagination"/>
 
-    <GlobalWindow title="閫夋嫨鐢佃〃" :visible.sync="selectorVisible" width="780px" @confirm="confirmSelect">
+    <GlobalWindow v-if="!readonly" title="閫夋嫨鐢佃〃" :visible.sync="selectorVisible" width="780px" @confirm="confirmSelect">
       <el-form inline @submit.native.prevent>
         <el-form-item label="鍏抽敭瀛�">
           <el-input v-model="selectorKeyword" placeholder="鍚嶇О/鍦板潃" clearable @keypress.enter.native="searchSelectable"/>
@@ -57,7 +57,11 @@
   components: { GlobalWindow, Pagination },
   props: {
     customerId: Number,
-    active: Boolean
+    active: Boolean,
+    readonly: {
+      type: Boolean,
+      default: false
+    }
   },
   data () {
     return {
diff --git a/admin/src/views/business/components/YwH5BannerEdit.vue b/admin/src/views/business/components/YwH5BannerEdit.vue
new file mode 100644
index 0000000..7f1b348
--- /dev/null
+++ b/admin/src/views/business/components/YwH5BannerEdit.vue
@@ -0,0 +1,147 @@
+<template>
+  <GlobalWindow
+    :title="title"
+    :visible.sync="visible"
+    :confirm-working="isWorking"
+    width="640px"
+    @confirm="confirm"
+  >
+    <el-form ref="form" :model="form" :rules="rules" label-width="96px" class="banner-edit-form">
+      <el-form-item label="鏍囬" prop="title">
+        <el-input v-model="form.title" placeholder="閫夊~锛岀敤浜庡悗鍙拌瘑鍒�" maxlength="200" clearable v-trim />
+      </el-form-item>
+      <el-form-item label="杞挱鍥剧墖" prop="imageUrl">
+        <UploadAvatarImage
+          :file="imageFile"
+          tips-label="涓婁紶鍥剧墖"
+          custom-style="width: 320px; height: 120px;"
+          :upload-data="{ folder: 'ywH5Banner' }"
+          @uploadSuccess="onUploadSuccess"
+          @uploadBegin="isUploading = true"
+          @uploadEnd="isUploading = false"
+        />
+        <p class="form-tip">寤鸿灏哄 750脳320 宸﹀彸锛屾敮鎸� jpg/png</p>
+      </el-form-item>
+      <el-form-item label="璺宠浆閾炬帴" prop="linkUrl">
+        <el-input v-model="form.linkUrl" placeholder="閫夊~锛孒5 鍐呴摼鎴栧閾�" clearable v-trim />
+      </el-form-item>
+      <el-form-item label="鎺掑簭" prop="sortnum">
+        <el-input-number v-model="form.sortnum" :min="0" :max="9999" controls-position="right" />
+        <span class="inline-tip">鏁板�艰秺灏忚秺闈犲墠</span>
+      </el-form-item>
+      <el-form-item label="鐘舵��" prop="status">
+        <el-radio-group v-model="form.status">
+          <el-radio :label="0">鍚敤</el-radio>
+          <el-radio :label="1">绂佺敤</el-radio>
+        </el-radio-group>
+      </el-form-item>
+      <el-form-item label="澶囨敞" prop="remark">
+        <el-input v-model="form.remark" type="textarea" :rows="2" placeholder="閫夊~" maxlength="500" show-word-limit />
+      </el-form-item>
+    </el-form>
+  </GlobalWindow>
+</template>
+
+<script>
+import BaseOpera from '@/components/base/BaseOpera'
+import GlobalWindow from '@/components/common/GlobalWindow'
+import UploadAvatarImage from '@/components/common/UploadAvatarImage'
+
+const defaultForm = () => ({
+  id: null,
+  title: '',
+  imageUrl: '',
+  linkUrl: '',
+  sortnum: 0,
+  status: 0,
+  remark: ''
+})
+
+export default {
+  name: 'YwH5BannerEdit',
+  extends: BaseOpera,
+  components: { GlobalWindow, UploadAvatarImage },
+  data () {
+    return {
+      form: defaultForm(),
+      imageFile: { imgurl: '', imgurlfull: '' },
+      isUploading: false,
+      rules: {
+        imageUrl: [{ required: true, message: '璇蜂笂浼犺疆鎾浘鐗�', trigger: 'change' }],
+        sortnum: [{ required: true, message: '璇疯緭鍏ユ帓搴�', trigger: 'change' }]
+      }
+    }
+  },
+  created () {
+    this.config({
+      api: '/business/ywh5banner',
+      'field.id': 'id'
+    })
+  },
+  methods: {
+    open (title, target) {
+      this.title = title
+      this.visible = true
+      this.isUploading = false
+      if (target == null) {
+        this.$nextTick(() => {
+          this.form = defaultForm()
+          this.imageFile = { imgurl: '', imgurlfull: '' }
+          this.$refs.form && this.$refs.form.clearValidate()
+        })
+        return
+      }
+      this.$nextTick(() => {
+        this.form = Object.assign(defaultForm(), target)
+        this.imageFile = {
+          imgurl: target.imageUrl || '',
+          imgurlfull: target.imageUrl || ''
+        }
+        this.$refs.form && this.$refs.form.clearValidate()
+      })
+    },
+    onUploadSuccess ({ imgurlfull, imgurl }) {
+      this.form.imageUrl = imgurlfull || imgurl || ''
+      this.imageFile.imgurl = imgurl || this.form.imageUrl
+      this.imageFile.imgurlfull = imgurlfull || this.form.imageUrl
+      this.$refs.form && this.$refs.form.validateField('imageUrl')
+    },
+    confirm () {
+      if (this.isUploading) {
+        this.$message.warning('鍥剧墖涓婁紶涓紝璇风◢鍊�')
+        return
+      }
+      if (this.form.id == null || this.form.id === '') {
+        this.__confirmCreate()
+        return
+      }
+      this.__confirmEdit()
+    }
+  }
+}
+</script>
+
+<style scoped lang="scss">
+.banner-edit-form {
+  padding: 8px 0 12px;
+}
+
+.banner-edit-form ::v-deep .avatar-uploader-icon {
+  line-height: 120px;
+}
+
+.form-tip,
+.inline-tip {
+  margin: 6px 0 0;
+  font-size: 12px;
+  color: #909399;
+  line-height: 1.5;
+}
+
+.inline-tip {
+  display: inline-block;
+  margin-left: 10px;
+  margin-top: 0;
+  vertical-align: middle;
+}
+</style>
diff --git a/admin/src/views/business/ywcustomerrecharge.vue b/admin/src/views/business/ywcustomerrecharge.vue
index ecc712b..ca0a0c4 100644
--- a/admin/src/views/business/ywcustomerrecharge.vue
+++ b/admin/src/views/business/ywcustomerrecharge.vue
@@ -65,14 +65,14 @@
         <el-table-column prop="createDate" label="鍒涘缓鏃堕棿" min-width="160" align="center"/>
         <el-table-column label="鎿嶄綔" min-width="180" align="center" fixed="right">
           <template slot-scope="{ row }">
-            <el-button type="text" v-permissions="['business:ywcustomerrecharge:bindDevice']" @click="openDevice(row)">鍏宠仈璁惧</el-button>
+            <el-button type="text" v-permissions="['business:ywcustomerrecharge:query']" @click="openDevice(row)">鍏宠仈璁惧</el-button>
             <el-button type="text" v-permissions="['business:ywcustomerrecharge:recharge']" @click="openRecharge(row)">鍏呭��</el-button>
           </template>
         </el-table-column>
       </el-table>
       <pagination @size-change="handleSizeChange" @current-change="handlePageChange" :pagination="tableData.pagination"/>
     </template>
-    <YwCustomerDeviceWindow ref="deviceWindow" @success="search"/>
+    <YwCustomerDeviceWindow ref="deviceWindow"/>
     <YwCustomerRechargeWindow ref="rechargeWindow" @success="search"/>
   </TableLayout>
 </template>
diff --git a/admin/src/views/business/ywh5banner.vue b/admin/src/views/business/ywh5banner.vue
new file mode 100644
index 0000000..80b73a2
--- /dev/null
+++ b/admin/src/views/business/ywh5banner.vue
@@ -0,0 +1,182 @@
+<template>
+  <TableLayout :permissions="['business:ywh5banner:query']">
+    <el-form ref="searchForm" slot="search-form" :model="searchForm" label-width="88px" inline>
+      <el-form-item label="鏍囬" prop="title">
+        <el-input v-model="searchForm.title" placeholder="璇疯緭鍏ユ爣棰�" clearable @keypress.enter.native="search" />
+      </el-form-item>
+      <el-form-item label="鐘舵��" prop="status">
+        <el-select v-model="searchForm.status" clearable placeholder="鍏ㄩ儴">
+          <el-option :value="0" label="鍚敤" />
+          <el-option :value="1" label="绂佺敤" />
+        </el-select>
+      </el-form-item>
+      <section>
+        <el-button type="primary" icon="el-icon-search" @click="search">鏌ヨ</el-button>
+        <el-button icon="el-icon-refresh" @click="reset">閲嶇疆</el-button>
+      </section>
+    </el-form>
+
+    <template v-slot:table-wrap>
+      <ul class="toolbar">
+        <li>
+          <el-button
+            type="primary"
+            icon="el-icon-plus"
+            @click="$refs.editWindow.open('鏂板缓杞挱鍥�')"
+            v-permissions="['business:ywh5banner:create']"
+          >鏂板缓</el-button>
+        </li>
+        <li>
+          <el-button
+            icon="el-icon-delete"
+            @click="deleteByIdInBatch"
+            v-permissions="['business:ywh5banner:delete']"
+          >鎵归噺鍒犻櫎</el-button>
+        </li>
+      </ul>
+
+      <el-table
+        v-loading="isWorking.search"
+        :data="tableData.list"
+        stripe
+        @selection-change="handleSelectionChange"
+      >
+        <el-table-column type="selection" width="48" />
+        <el-table-column label="棰勮" width="160" align="center">
+          <template slot-scope="{ row }">
+            <el-image
+              v-if="row.imageUrl"
+              :src="row.imageUrl"
+              :preview-src-list="[row.imageUrl]"
+              fit="cover"
+              class="banner-thumb"
+            />
+            <span v-else>-</span>
+          </template>
+        </el-table-column>
+        <el-table-column prop="title" label="鏍囬" min-width="140" show-overflow-tooltip />
+        <el-table-column prop="sortnum" label="鎺掑簭" width="80" align="center" />
+        <el-table-column label="鐘舵��" width="100" align="center">
+          <template slot-scope="{ row }">
+            <el-switch
+              v-model="row.status"
+              :active-value="0"
+              :inactive-value="1"
+              :disabled="!containPermissions(['business:ywh5banner:update'])"
+              @change="changeStatus(row)"
+            />
+          </template>
+        </el-table-column>
+        <el-table-column prop="linkUrl" label="璺宠浆閾炬帴" min-width="160" show-overflow-tooltip />
+        <el-table-column prop="editDate" label="鏇存柊鏃堕棿" width="168" align="center" />
+        <el-table-column label="鎿嶄綔" width="140" align="center" fixed="right">
+          <template slot-scope="{ row }">
+            <el-button
+              type="text"
+              @click="$refs.editWindow.open('缂栬緫杞挱鍥�', row)"
+              v-permissions="['business:ywh5banner:update']"
+            >缂栬緫</el-button>
+            <el-button
+              type="text"
+              class="red"
+              @click="deleteById(row)"
+              v-permissions="['business:ywh5banner:delete']"
+            >鍒犻櫎</el-button>
+          </template>
+        </el-table-column>
+      </el-table>
+
+      <pagination
+        @size-change="handleSizeChange"
+        @current-change="handlePageChange"
+        :pagination="tableData.pagination"
+      />
+    </template>
+
+    <YwH5BannerEdit 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 YwH5BannerEdit from './components/YwH5BannerEdit'
+import { updateById } from '@/api/business/ywh5banner'
+import { Message } from 'element-ui'
+
+export default {
+  name: 'YwH5Banner',
+  extends: BaseTable,
+  components: { TableLayout, Pagination, YwH5BannerEdit },
+  data () {
+    return {
+      searchForm: {
+        title: '',
+        status: null
+      }
+    }
+  },
+  created () {
+    this.config({
+      module: 'H5杞挱鍥�',
+      api: '/business/ywh5banner',
+      'field.id': 'id',
+      'field.main': 'title'
+    })
+    this.search()
+  },
+  methods: {
+    buildSearchModel () {
+      const model = {}
+      if (this.searchForm.title) model.title = this.searchForm.title
+      if (this.searchForm.status !== null && this.searchForm.status !== '') {
+        model.status = this.searchForm.status
+      }
+      return model
+    },
+    reset () {
+      this.searchForm = { title: '', status: null }
+      this.search()
+    },
+    handlePageChange (pageIndex) {
+      this.tableData.pagination.pageIndex = pageIndex || this.tableData.pagination.pageIndex
+      this.isWorking.search = true
+      this.api.fetchList({
+        page: this.tableData.pagination.pageIndex,
+        capacity: this.tableData.pagination.pageSize,
+        model: this.buildSearchModel(),
+        sorts: this.tableData.sorts
+      })
+        .then(data => {
+          this.tableData.list = data.records
+          this.tableData.pagination.total = data.total
+        })
+        .catch(() => {})
+        .finally(() => { this.isWorking.search = false })
+    },
+    changeStatus (row) {
+      updateById({ id: row.id, status: row.status })
+        .then(() => {
+          Message.success('鐘舵�佸凡鏇存柊')
+        })
+        .catch(() => {
+          row.status = row.status === 0 ? 1 : 0
+        })
+    }
+  }
+}
+</script>
+
+<style scoped>
+.banner-thumb {
+  width: 128px;
+  height: 56px;
+  border-radius: 4px;
+  border: 1px solid #ebeef5;
+}
+
+.red {
+  color: #f56c6c;
+}
+</style>
diff --git a/admin/src/views/client/components/OperaYwCustomerWindow.vue b/admin/src/views/client/components/OperaYwCustomerWindow.vue
index c966c3f..bc8ada4 100644
--- a/admin/src/views/client/components/OperaYwCustomerWindow.vue
+++ b/admin/src/views/client/components/OperaYwCustomerWindow.vue
@@ -171,6 +171,47 @@
     })
   },
   methods: {
+    defaultForm() {
+      return {
+        id: null,
+        validity: '',
+        creator: '',
+        createDate: '',
+        editor: '',
+        editDate: '',
+        isdeleted: '',
+        remark: '',
+        industryId: '',
+        type: 1,
+        name: '',
+        phone: '',
+        idcardNo: '',
+        idcardDecode: '',
+        code: '',
+        status: '',
+        memberName: '',
+        lastLoginDate: '',
+        loginNum: '',
+        userId: '',
+        memberId: '',
+        accountBank: '',
+        accountNo: '',
+        accountPhone: '',
+        creditCard: '',
+        fpType: '',
+        accountAddr: '',
+        email: '',
+        selLangTime: false,
+        member: {
+          name: '',
+          phone: '',
+          highCheckor: 0,
+          idcardType: 0,
+          idcardNo: '',
+          email: ''
+        }
+      }
+    },
     openTrade() {
       this.$refs.OperaCategoryWindowRef.open('鏂板琛屼笟')
     },
@@ -189,37 +230,31 @@
         this.cateList = res
       })
     },
+    applyDetail(res) {
+      const base = this.defaultForm()
+      this.form = Object.assign(base, res, {
+        member: Object.assign({}, base.member, res.member || {})
+      })
+      this.form.selLangTime = this.form.validity === '2099-12-31'
+    },
     open(title, target) {
       this.title = title
       this.visible = true
       this.initData()
-      // 鏂板缓
+      this.form = this.defaultForm()
+      this.clientList = []
+      this.$nextTick(() => {
+        this.$refs.form && this.$refs.form.clearValidate()
+      })
       if (target == null) {
-        this.$nextTick(() => {
-          this.$refs.form.resetFields()
-          this.form.validity = ''
-          this.form.id = ''
-          this.form.member = {
-            name: "",
-            phone: "",
-            highCheckor: 0,
-            idcardType: 0,
-            idcardNo: '',
-            email: '',
-          }
-        })
-        this.form.type = 1
         return
       }
-      // 缂栬緫
-      this.$nextTick(() => {
-        if (title == '缂栬緫瀹㈡埛') {
-          this.getClient(target.id)
-          detailById(target.id).then(res => {
-            this.form = res
-          })
-        }
-      })
+      if (title === '缂栬緫瀹㈡埛') {
+        this.getClient(target.id)
+        detailById(target.id).then(res => {
+          this.applyDetail(res)
+        })
+      }
     },
     getClient(customerId) {
       fetchList({
diff --git a/admin/src/views/client/components/staffEdit.vue b/admin/src/views/client/components/staffEdit.vue
index ffe8cd5..4390a98 100644
--- a/admin/src/views/client/components/staffEdit.vue
+++ b/admin/src/views/client/components/staffEdit.vue
@@ -1,53 +1,105 @@
 <template>
-  <GlobalWindow :title="title" width="820px" :visible.sync="visible" :confirm-working="isWorking" @confirm="confirm">
-    <el-form :model="form" ref="form" label-position="top" :rules="rules">
-      <div class="list">
-        <el-form-item label="瀹㈡埛鍚嶇О" prop="customerId">
-          <el-select v-model="form.customerId" :disabled="form.id || customerId != ''" clearable filterable>
-            <el-option v-for="item in clientList" :value="item.id" :label="item.name" />
-          </el-select>
-        </el-form-item>
-        <el-form-item label="濮撳悕" prop="name">
-          <div class="df">
-            <el-input v-model="form.name" placeholder="璇疯緭鍏�" v-trim />
-          </div>
-        </el-form-item>
-        <el-form-item label="鎵嬫満鍙�" prop="phone">
-          <el-input v-model="form.phone" placeholder="璇疯緭鍏ユ墜鏈哄彿" v-trim />
-        </el-form-item>
-        <el-form-item label="韬唤">
-          <el-select v-model="form.highCheckor" filterable>
-            <el-option :value="0" label="鑰佹澘/瓒呯骇绠$悊鍛�" />
-            <el-option :value="1" label="浜轰簨/绠$悊鍛�" />
-            <el-option :value="2" label="鍛樺伐/鏅�氬憳宸�" />
-          </el-select>
-        </el-form-item>
-        <el-form-item label="璇佷欢绫诲瀷">
-          <el-select v-model="form.idcardType" filterable>
-            <el-option :value="0" label="韬唤璇�" />
-            <el-option :value="1" label="娓境璇佷欢" />
-            <el-option :value="2" label="鎶ょ収" />
-          </el-select>
-        </el-form-item>
-        <el-form-item label="璇佷欢鍙风爜" prop="member.idcardNo">
-          <el-input v-model="form.idcardNo" placeholder="璇疯緭鍏�" v-trim />
-        </el-form-item>
-        <el-form-item label="閭" prop="email" :rules="[
-          { required: false, type: 'email', message: '璇疯緭鍏ユ纭殑閭鏍煎紡'}
-        ]">
-          <el-input v-model="form.email" placeholder="璇疯緭鍏ラ偖绠�" v-trim />
-        </el-form-item>
-        <el-form-item label="鎬у埆">
-          <el-select v-model="form.sex" filterable>
-            <el-option :value="1" label="鐢�" />
-            <el-option :value="2" label="濂�" />
-          </el-select>
-        </el-form-item>
-        <el-form-item label="鍑虹敓鏃ユ湡">
-          <el-date-picker type="date" v-model="form.birthday" value-format="yyyy-MM-dd" placeholder="璇烽�夋嫨" />
-        </el-form-item>
+  <GlobalWindow :title="title" width="720px" :visible.sync="visible" :confirm-working="isWorking" @confirm="confirm">
+    <el-form
+      ref="form"
+      :model="form"
+      :rules="rules"
+      label-width="88px"
+      label-position="right"
+      class="staff-edit-form"
+    >
+      <section class="form-section">
+        <div class="form-section__title">褰掑睘淇℃伅</div>
+        <div class="form-grid">
+          <el-form-item label="瀹㈡埛鍚嶇О" prop="customerId" class="col-full">
+            <el-select
+              v-model="form.customerId"
+              :disabled="form.id || customerId != ''"
+              clearable
+              filterable
+              placeholder="璇烽�夋嫨瀹㈡埛"
+            >
+              <el-option v-for="item in clientList" :key="item.id" :value="item.id" :label="item.name" />
+            </el-select>
+          </el-form-item>
+          <el-form-item label="韬唤">
+            <el-select v-model="form.highCheckor" filterable placeholder="璇烽�夋嫨韬唤">
+              <el-option :value="0" label="鑰佹澘/瓒呯骇绠$悊鍛�" />
+              <el-option :value="1" label="浜轰簨/绠$悊鍛�" />
+              <el-option :value="2" label="鍛樺伐/鏅�氬憳宸�" />
+            </el-select>
+          </el-form-item>
+        </div>
+      </section>
 
-      </div>
+      <section class="form-section">
+        <div class="form-section__title">鍩烘湰淇℃伅</div>
+        <div class="form-grid">
+          <el-form-item label="濮撳悕" prop="name">
+            <el-input v-model="form.name" placeholder="璇疯緭鍏ュ鍚�" v-trim />
+          </el-form-item>
+          <el-form-item label="鎵嬫満鍙�" prop="phone">
+            <el-input v-model="form.phone" placeholder="璇疯緭鍏ユ墜鏈哄彿" v-trim maxlength="11" />
+          </el-form-item>
+          <el-form-item label="鎬у埆">
+            <el-select v-model="form.sex" filterable clearable placeholder="璇烽�夋嫨鎬у埆">
+              <el-option :value="1" label="鐢�" />
+              <el-option :value="2" label="濂�" />
+            </el-select>
+          </el-form-item>
+          <el-form-item label="鍑虹敓鏃ユ湡">
+            <el-date-picker
+              v-model="form.birthday"
+              type="date"
+              value-format="yyyy-MM-dd"
+              placeholder="璇烽�夋嫨鍑虹敓鏃ユ湡"
+            />
+          </el-form-item>
+          <el-form-item
+            label="閭"
+            prop="email"
+            class="col-full"
+            :rules="[{ required: false, type: 'email', message: '璇疯緭鍏ユ纭殑閭鏍煎紡' }]"
+          >
+            <el-input v-model="form.email" placeholder="璇疯緭鍏ラ偖绠�" v-trim />
+          </el-form-item>
+        </div>
+      </section>
+
+      <section class="form-section">
+        <div class="form-section__title">璇佷欢淇℃伅</div>
+        <div class="form-grid">
+          <el-form-item label="璇佷欢绫诲瀷">
+            <el-select v-model="form.idcardType" filterable placeholder="璇烽�夋嫨璇佷欢绫诲瀷">
+              <el-option :value="0" label="韬唤璇�" />
+              <el-option :value="1" label="娓境璇佷欢" />
+              <el-option :value="2" label="鎶ょ収" />
+            </el-select>
+          </el-form-item>
+          <el-form-item label="璇佷欢鍙风爜" :prop="form.id ? '' : 'idcardNo'" class="col-full">
+            <el-input
+              v-if="!form.id"
+              v-model="form.idcardNo"
+              placeholder="璇疯緭鍏ヨ瘉浠跺彿鐮�"
+              v-trim
+              clearable
+            />
+            <div v-else class="idcard-field">
+              <div class="idcard-field__current">
+                <i class="el-icon-bank-card" />
+                <span class="idcard-field__label">褰撳墠璇佷欢鍙�</span>
+                <span class="idcard-field__value">{{ form.idcardDecode || '鏈櫥璁�' }}</span>
+              </div>
+              <el-input
+                v-model="form.idcardNoNew"
+                placeholder="濡傞渶鍙樻洿锛岃杈撳叆鏂拌瘉浠跺彿锛堢暀绌鸿〃绀轰笉淇敼锛�"
+                v-trim
+                clearable
+              />
+            </div>
+          </el-form-item>
+        </div>
+      </section>
     </el-form>
   </GlobalWindow>
 </template>
@@ -55,17 +107,15 @@
 <script>
 import BaseOpera from '@/components/base/BaseOpera'
 import GlobalWindow from '@/components/common/GlobalWindow'
-import { fetchCateList } from '@/api/business/category'
 import { staffRules } from './config'
-import { detailById } from '@/api/client/ywCustomer'
 import { fetchList } from '@/api/client/ywCustomer'
+
 export default {
   name: 'OperaYwCustomerWindow',
   extends: BaseOpera,
   components: { GlobalWindow },
   data() {
     return {
-      // 琛ㄥ崟鏁版嵁
       form: {
         id: null,
         customerId: '',
@@ -81,28 +131,25 @@
         sex: '',
         status: '',
         memberName: '',
-
         lastLoginDate: '',
         loginNum: '',
         userId: '',
-
         accountBank: '',
         accountNo: '',
         accountPhone: '',
         creditCard: '',
         fpType: '',
         accountAddr: '',
-        // identityType: '0',
-        name: "",
-        phone: "",
+        name: '',
+        phone: '',
         highCheckor: 0,
         idcardType: 0,
         idcardNo: '',
-        email: '',
+        idcardNoNew: '',
+        email: ''
       },
       customerId: '',
       clientList: [],
-      // 楠岃瘉瑙勫垯
       rules: staffRules
     }
   },
@@ -112,7 +159,6 @@
       'field.id': 'id'
     })
   },
-
   methods: {
     initData() {
       fetchList({
@@ -127,11 +173,7 @@
       this.title = title
       this.visible = true
       this.customerId = ''
-      console.log(this.customerId);
-      console.log(this.form.id);
-      
       this.initData()
-      // 鏂板缓
       if (target == null) {
         this.$nextTick(() => {
           this.form = {
@@ -149,70 +191,147 @@
             sex: '',
             status: '',
             memberName: '',
-
             lastLoginDate: '',
             loginNum: '',
             userId: '',
-
             accountBank: '',
             accountNo: '',
             accountPhone: '',
             creditCard: '',
             fpType: '',
             accountAddr: '',
-            // identityType: '0',
-            name: "",
-            phone: "",
+            name: '',
+            phone: '',
             highCheckor: 0,
             idcardType: 0,
             idcardNo: '',
-            email: '',
+            idcardNoNew: '',
+            email: ''
           }
         })
-        // this.$refs.form.resetFields()
         return
       }
-      // 缂栬緫
       this.$nextTick(() => {
         for (const key in this.form) {
           this.form[key] = target[key]
         }
+        this.form.idcardNo = ''
+        this.form.idcardNoNew = ''
       })
     },
     changeValid(e) {
       this.$set(this.form, 'validity', e ? '2099-12-31' : '')
-    },
+    }
   }
 }
 </script>
-<style lang='scss' scoped>
+
+<style lang="scss" scoped>
 @import '@/assets/style/variables.scss';
 
-div {
-  box-sizing: border-box;
+.staff-edit-form {
+  padding-top: 4px;
+
+  ::v-deep .el-form-item {
+    margin-bottom: 14px;
+  }
+
+  ::v-deep .el-form-item__label {
+    color: #606266;
+    font-size: 13px;
+    line-height: 32px;
+    padding-right: 12px;
+  }
+
+  ::v-deep .el-input,
+  ::v-deep .el-select,
+  ::v-deep .el-date-editor {
+    width: 100%;
+  }
 }
 
-.title {
-  width: 100%;
-  font-weight: 500;
-  font-size: 15px;
-  margin-top: 16px;
-}
+.form-section {
+  margin-bottom: 14px;
+  padding: 16px 16px 2px;
+  background: #fff;
+  border: 1px solid #ebeef5;
+  border-radius: 6px;
 
-.list {
-  /* padding-top: 14px; */
-  display: flex;
-  flex-wrap: wrap;
+  &:last-child {
+    margin-bottom: 0;
+  }
 
-  .el-form-item {
-    width: 33.33%;
-    margin-bottom: 12px;
-    padding: 0 12px;
+  &__title {
+    margin-bottom: 14px;
+    padding-bottom: 10px;
+    border-bottom: 1px solid #f0f2f5;
+    font-size: 14px;
+    font-weight: 600;
+    color: #303133;
+    line-height: 22px;
 
-    .la {
-      color: #7f7f7f;
-      margin-top: 2px;
+    &::before {
+      content: '';
+      display: inline-block;
+      width: 3px;
+      height: 14px;
+      margin-right: 8px;
+      background: $primary-color;
+      border-radius: 2px;
+      vertical-align: -2px;
     }
   }
 }
+
+.form-grid {
+  display: grid;
+  grid-template-columns: repeat(2, minmax(0, 1fr));
+  column-gap: 20px;
+
+  .col-full {
+    grid-column: 1 / -1;
+  }
+}
+
+.idcard-field {
+  display: flex;
+  flex-direction: column;
+  gap: 8px;
+
+  &__current {
+    display: flex;
+    align-items: center;
+    min-height: 32px;
+    padding: 0 12px;
+    background: linear-gradient(90deg, #f8fafc 0%, #f5f7fa 100%);
+    border: 1px solid #e4e7ed;
+    border-radius: 4px;
+    font-size: 13px;
+    color: #606266;
+  }
+
+  .el-icon-bank-card {
+    margin-right: 6px;
+    font-size: 15px;
+    color: $primary-color;
+  }
+
+  &__label {
+    flex-shrink: 0;
+    margin-right: 8px;
+    color: #909399;
+    font-size: 12px;
+  }
+
+  &__value {
+    flex: 1;
+    min-width: 0;
+    color: #303133;
+    font-weight: 500;
+    letter-spacing: 0.4px;
+    overflow: hidden;
+    text-overflow: ellipsis;
+    white-space: nowrap;
+  }
+}
 </style>
diff --git a/admin/src/views/client/staffList.vue b/admin/src/views/client/staffList.vue
index fdb031d..4bc1161 100644
--- a/admin/src/views/client/staffList.vue
+++ b/admin/src/views/client/staffList.vue
@@ -10,6 +10,9 @@
       <el-form-item prop="name">
         <el-input v-model="searchForm.name" placeholder="璇疯緭鍏ヤ汉鍛樺鍚�/鎵嬫満鍙�" @keypress.enter.native="search"></el-input>
       </el-form-item>
+      <el-form-item label="韬唤璇佸彿" prop="idcardNo">
+        <el-input v-model="searchForm.idcardNo" placeholder="璇疯緭鍏ヨ韩浠借瘉鍙�" @keypress.enter.native="search"></el-input>
+      </el-form-item>
       <section>
         <el-button type="primary" @click="search">鎼滅储</el-button>
         <el-button type="primary" :loading="isWorking.export" v-permissions="['business:ywcustomer:exportExcel']"
@@ -29,6 +32,7 @@
         <el-table-column prop="customerName" label="瀹㈡埛鍚嶇О" min-width="100px"></el-table-column>
         <el-table-column prop="name" label="鑱旂郴浜�" min-width="100px"></el-table-column>
         <el-table-column prop="phone" label="鑱旂郴鐢佃瘽" min-width="100px"></el-table-column>
+        <el-table-column prop="idcardDecode" label="韬唤璇佸彿" min-width="140px"></el-table-column>
         <el-table-column prop="" label="韬唤" min-width="100px">
           <template slot-scope="{row}">
             <span v-if="row.highCheckor == 0">鑰佹澘/瓒呯骇绠$悊鍛�</span>
@@ -82,6 +86,7 @@
       searchForm: {
         customerId: '',
         name: '',
+        idcardNo: '',
       },
       clientList: []
     }
diff --git a/h5/App.vue b/h5/App.vue
index 526bb3b..2e698cd 100644
--- a/h5/App.vue
+++ b/h5/App.vue
@@ -15,6 +15,7 @@
 <style lang="scss">
 /*姣忎釜椤甸潰鍏叡css */
 @import "uview-ui/index.scss";
+@import "./styles/customer.scss";
 // @import "./uni_modules/uview-ui/index.scss";
 body{
 	font-size: 28rpx;
diff --git a/h5/api/customer.js b/h5/api/customer.js
new file mode 100644
index 0000000..7ca8f0d
--- /dev/null
+++ b/h5/api/customer.js
@@ -0,0 +1,23 @@
+import { http } from '@/utils/service.js'
+
+const prefix = 'visitsAdmin/cloudService/web/customer'
+
+export const customerLogin = (data) => http({ url: `${prefix}/loginByPhone`, method: 'post', data })
+export const customerSendLoginSms = (data) => http({
+  url: `${prefix}/sendLoginSms`,
+  method: 'post',
+  data: { phone: data.phone }
+})
+export const customerGetUserInfo = () => http({ url: `${prefix}/getUserInfo`, method: 'get' })
+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' })
+export const customerDevicePage = (data) => http({ url: `${prefix}/device/page`, method: 'post', data })
+export const customerDeviceDetail = (data) => http({ url: `${prefix}/device/detail`, method: 'get', data })
+export const customerRechargeRecordPage = (data) => http({ url: `${prefix}/rechargeRecord/page`, method: 'post', data })
+export const customerContractPage = (data) => http({ url: `${prefix}/contract/page`, method: 'post', data })
+export const customerContractDetail = (id, billType = 0) => http({ url: `${prefix}/contract/${id}`, method: 'get', data: { billType } })
+export const customerBillPage = (data) => http({ url: `${prefix}/bill/page`, method: 'post', data })
+export const customerBillDetail = (id) => http({ url: `${prefix}/bill/${id}`, method: 'get' })
+export const customerPayCreate = (data) => http({ url: `${prefix}/pay/createOrder`, method: 'post', data })
+export const customerPayQuery = (orderNo) => http({ url: `${prefix}/pay/query/${orderNo}`, method: 'get' })
diff --git a/h5/api/index.js b/h5/api/index.js
index c931054..8e9931f 100644
--- a/h5/api/index.js
+++ b/h5/api/index.js
@@ -2,6 +2,7 @@
 export * from '@/utils/config.js'
 export * from './staff'
 export * from './yw'
+export * from './customer'
 
 
 
diff --git a/h5/pages.json b/h5/pages.json
index f5f619d..0ad9c86 100644
--- a/h5/pages.json
+++ b/h5/pages.json
@@ -2,13 +2,17 @@
 	"easycom": {
 		"^u-(.*)": "uview-ui/components/u-$1/u-$1.vue"
 	},
-	"pages": [
-		{
-			"path": "pages/login",
-			"style": {
-				"navigationBarTitleText": "鐧诲綍"
-			}
-		},
+	"pages": [
+		{
+			"path": "pages/roleSelect",
+			"style": { "navigationBarTitleText": "閫夋嫨韬唤" }
+		},
+		{
+			"path": "pages/login",
+			"style": {
+				"navigationBarTitleText": "鐧诲綍"
+			}
+		},
 		{
 			"path": "pages/index",
 			"style": {
@@ -27,105 +31,114 @@
 			"style": {
 				"navigationBarTitleText": "宸ュ崟璇︽儏"
 			}
-		},
-		{
-			"path" : "pages/workOrder/edit",
-			"style" : 
-			{
-				"navigationBarTitleText" : "鏂板宸ュ崟"
-			}
-		},
-		{
-			"path" : "pages/operation/record",
-			"style" : 
-			{
-				"navigationBarTitleText" : "杩愮淮璁板綍"
-			}
-		},
-		{
-			"path" : "pages/operation/detail",
-			"style" : 
-			{
-				"navigationBarTitleText" : "杩愮淮璇︽儏"
-			}
-		},
-		{
-			"path" : "pages/operation/device",
-			"style" : 
-			{
-				"navigationBarTitleText" : "璁惧杩愮淮"
-			}
-		},
-		{
-			"path" : "pages/polling/task",
-			"style" : 
-			{
-				"navigationBarTitleText" : "宸℃浠诲姟"
-			}
-		},
-		{
-			"path" : "pages/polling/detail",
-			"style" : 
-			{
-				"navigationBarTitleText" : "浠诲姟璇︽儏"
-			}
-		},
-		{
-			"path" : "pages/polling/point",
-			"style" : 
-			{
-				"navigationBarTitleText" : "宸℃鐐�"
-			}
-		},
-		{
-			"path" : "pages/common/memberSel",
-			"style" : 
-			{
-				"navigationBarTitleText" : "閫夋嫨浜哄憳"
-			}
-		},
-		{
-			"path" : "pages/inventory/index",
-			"style" : 
-			{
-				"navigationBarTitleText" : "搴撳瓨鐩樼偣"
-			}
-		},
-		{
-			"path" : "pages/inventory/detail",
-			"style" : 
-			{
-				"navigationBarTitleText" : "鐩樼偣鍗�"
-			}
-		},
-		{
-			"path" : "pages/workOrder/problemEdit",
-			"style" : 
-			{
-				"navigationBarTitleText" : "闂涓婃姤"
-			}
-		},
-		{
-			"path" : "pages/workOrder/result",
-			"style" : 
-			{
-				"navigationBarTitleText" : "闂涓婃姤"
-			}
-		},
-		{
-			"path" : "pages/workOrder/wait",
-			"style" : 
-			{
-				"navigationBarTitleText" : "浠诲姟涓績"
-			}
-		},
-		{
-			"path" : "pages/polling/empty",
-			"style" : 
-			{
-				"navigationBarTitleText" : "鎵爜宸℃"
-			}
-		}
+		},
+		{
+			"path" : "pages/workOrder/edit",
+			"style" : 
+			{
+				"navigationBarTitleText" : "鏂板宸ュ崟"
+			}
+		},
+		{
+			"path" : "pages/operation/record",
+			"style" : 
+			{
+				"navigationBarTitleText" : "杩愮淮璁板綍"
+			}
+		},
+		{
+			"path" : "pages/operation/detail",
+			"style" : 
+			{
+				"navigationBarTitleText" : "杩愮淮璇︽儏"
+			}
+		},
+		{
+			"path" : "pages/operation/device",
+			"style" : 
+			{
+				"navigationBarTitleText" : "璁惧杩愮淮"
+			}
+		},
+		{
+			"path" : "pages/polling/task",
+			"style" : 
+			{
+				"navigationBarTitleText" : "宸℃浠诲姟"
+			}
+		},
+		{
+			"path" : "pages/polling/detail",
+			"style" : 
+			{
+				"navigationBarTitleText" : "浠诲姟璇︽儏"
+			}
+		},
+		{
+			"path" : "pages/polling/point",
+			"style" : 
+			{
+				"navigationBarTitleText" : "宸℃鐐�"
+			}
+		},
+		{
+			"path" : "pages/common/memberSel",
+			"style" : 
+			{
+				"navigationBarTitleText" : "閫夋嫨浜哄憳"
+			}
+		},
+		{
+			"path" : "pages/inventory/index",
+			"style" : 
+			{
+				"navigationBarTitleText" : "搴撳瓨鐩樼偣"
+			}
+		},
+		{
+			"path" : "pages/inventory/detail",
+			"style" : 
+			{
+				"navigationBarTitleText" : "鐩樼偣鍗�"
+			}
+		},
+		{
+			"path" : "pages/workOrder/problemEdit",
+			"style" : 
+			{
+				"navigationBarTitleText" : "闂涓婃姤"
+			}
+		},
+		{
+			"path" : "pages/workOrder/result",
+			"style" : 
+			{
+				"navigationBarTitleText" : "闂涓婃姤"
+			}
+		},
+		{
+			"path" : "pages/workOrder/wait",
+			"style" : 
+			{
+				"navigationBarTitleText" : "浠诲姟涓績"
+			}
+		},
+		{
+			"path": "pages/polling/empty",
+			"style": { "navigationBarTitleText": "鎵爜宸℃" }
+		},
+		{ "path": "pages/customer/login", "style": { "navigationBarTitleText": "鍟嗘埛鐧诲綍", "navigationBarBackgroundColor": "#dce9ff" } },
+		{ "path": "pages/customer/index", "style": { "navigationBarTitleText": "鍟嗘埛宸ヤ綔鍙�", "navigationBarBackgroundColor": "#2080f7", "navigationBarTextStyle": "white" } },
+		{ "path": "pages/customer/electricity/list", "style": { "navigationBarTitleText": "浜ょ數璐�", "navigationBarBackgroundColor": "#f4f6fb" } },
+		{ "path": "pages/customer/electricity/recharge", "style": { "navigationBarTitleText": "鐢佃垂鍏呭��", "navigationBarBackgroundColor": "#f4f6fb" } },
+		{ "path": "pages/customer/conditioner/recharge", "style": { "navigationBarTitleText": "绌鸿皟鍏呭��", "navigationBarBackgroundColor": "#f4f6fb" } },
+		{ "path": "pages/customer/pay/result", "style": { "navigationBarTitleText": "鏀粯缁撴灉", "navigationBarBackgroundColor": "#f4f6fb" } },
+		{ "path": "pages/customer/recharge/record", "style": { "navigationBarTitleText": "鍏呭�艰褰�", "navigationBarBackgroundColor": "#f4f6fb" } },
+		{ "path": "pages/customer/contract/list", "style": { "navigationBarTitleText": "鎴戠殑鍚堝悓", "navigationBarBackgroundColor": "#f4f6fb" } },
+		{ "path": "pages/customer/contract/detail", "style": { "navigationBarTitleText": "鍚堝悓璇︽儏", "navigationBarBackgroundColor": "#f4f6fb" } },
+		{ "path": "pages/customer/bill/list", "style": { "navigationBarTitleText": "鎴戠殑璐﹀崟", "navigationBarBackgroundColor": "#f4f6fb" } },
+		{ "path": "pages/customer/bill/detail", "style": { "navigationBarTitleText": "璐﹀崟璇︽儏", "navigationBarBackgroundColor": "#f4f6fb" } },
+		{ "path": "pages/customer/bill/pay", "style": { "navigationBarTitleText": "璐﹀崟缂磋垂", "navigationBarBackgroundColor": "#f4f6fb" } }
 	],
 	"globalStyle": {
 		"navigationBarTextStyle": "black",
diff --git a/h5/pages/customer/bill/detail.vue b/h5/pages/customer/bill/detail.vue
new file mode 100644
index 0000000..3638804
--- /dev/null
+++ b/h5/pages/customer/bill/detail.vue
@@ -0,0 +1,330 @@
+<template>
+
+  <view :class="['cu-page cu-bill-detail', needPayAmount > 0 ? 'cu-page--with-footer' : '']" v-if="bill">
+
+    <view :class="['cu-bill-detail-hero', heroClass]">
+
+      <view class="cu-bill-detail-hero__bg" />
+
+      <view class="cu-bill-detail-hero__content">
+
+        <view class="cu-bill-detail-hero__top">
+
+          <view>
+
+            <text class="cu-bill-detail-hero__type">{{ costTypeText(bill.costType) }}</text>
+
+            <view class="cu-bill-detail-hero__code">{{ bill.code }}</view>
+
+          </view>
+
+          <text class="cu-bill-detail-hero__status">{{ payText(bill.payStatus) }}</text>
+
+        </view>
+
+
+
+        <view class="cu-bill-detail-hero__amount-wrap">
+
+          <text class="cu-bill-detail-hero__amount-label">{{ needPayAmount > 0 ? '闇�浠橀噾棰�' : '搴斾粯閲戦' }}</text>
+
+          <text class="cu-bill-detail-hero__amount">
+
+            楼{{ formatMoney(needPayAmount > 0 ? needPayAmount : bill.receivableFee) }}
+
+          </text>
+
+          <text v-if="bill.planPayDate" class="cu-bill-detail-hero__due">搴斾粯鏃ユ湡 {{ bill.planPayDate }}</text>
+
+        </view>
+
+      </view>
+
+    </view>
+
+
+
+    <view class="cu-bill-stat-row">
+
+      <view class="cu-bill-stat">
+
+        <text class="cu-bill-stat__label">搴斾粯閲戦</text>
+
+        <text class="cu-bill-stat__value cu-bill-stat__value--danger">楼{{ formatMoney(bill.receivableFee) }}</text>
+
+      </view>
+
+      <view class="cu-bill-stat">
+
+        <text class="cu-bill-stat__label">瀹炰粯閲戦</text>
+
+        <text class="cu-bill-stat__value">楼{{ formatMoney(paidAmount) }}</text>
+
+      </view>
+
+      <view class="cu-bill-stat">
+
+        <text class="cu-bill-stat__label">闇�浠橀噾棰�</text>
+
+        <text :class="['cu-bill-stat__value', needPayAmount > 0 ? 'cu-bill-stat__value--danger' : '']">
+
+          楼{{ formatMoney(needPayAmount) }}
+
+        </text>
+
+      </view>
+
+    </view>
+
+
+
+    <view class="cu-panel cu-bill-panel">
+
+      <view class="cu-panel__title">鍏宠仈鍚堝悓</view>
+
+      <view class="cu-bill-info-grid">
+
+        <view class="cu-bill-info-item">
+
+          <text class="cu-bill-info-item__label">鍚堝悓缂栧彿</text>
+
+          <text class="cu-bill-info-item__value">{{ bill.contractCode || '-' }}</text>
+
+        </view>
+
+        <view class="cu-bill-info-item cu-bill-info-item--full">
+
+          <text class="cu-bill-info-item__label">鍚堝悓鏈夋晥鏈�</text>
+
+          <text class="cu-bill-info-item__value">{{ contractPeriod(bill) }}</text>
+
+        </view>
+
+      </view>
+
+    </view>
+
+    <view class="cu-panel cu-bill-panel">
+      <view class="cu-panel__title">鎴挎簮淇℃伅</view>
+      <view v-if="bill.roomList && bill.roomList.length" class="cu-room-list">
+        <view v-for="(room, idx) in bill.roomList" :key="idx" class="cu-room-item">
+          <text class="cu-room-item__name">{{ formatRoomLine(room) }}</text>
+          <text class="cu-room-item__area">{{ formatArea(room.rentArea) }}</text>
+        </view>
+      </view>
+      <view v-else class="cu-bill-info-grid">
+        <view class="cu-bill-info-item cu-bill-info-item--full">
+          <text class="cu-bill-info-item__value cu-kv__value--muted">{{ bill.roomInfo || '鏆傛棤鎴挎簮淇℃伅' }}</text>
+        </view>
+      </view>
+    </view>
+
+    <view class="cu-panel cu-bill-panel">
+      <view class="cu-panel__title">璐﹀崟淇℃伅</view>
+
+      <view class="cu-bill-info-grid">
+
+        <view class="cu-bill-info-item">
+
+          <text class="cu-bill-info-item__label">璐圭敤绫诲瀷</text>
+
+          <text class="cu-bill-info-item__value">{{ costTypeText(bill.costType) }}</text>
+
+        </view>
+
+        <view class="cu-bill-info-item">
+
+          <text class="cu-bill-info-item__label">鏀粯鐘舵��</text>
+
+          <text class="cu-bill-info-item__value">{{ payText(bill.payStatus) }}</text>
+
+        </view>
+
+        <view class="cu-bill-info-item cu-bill-info-item--full">
+
+          <text class="cu-bill-info-item__label">璁¤垂鍛ㄦ湡</text>
+
+          <text class="cu-bill-info-item__value">{{ bill.startDate }} ~ {{ bill.endDate }}</text>
+
+        </view>
+
+      </view>
+
+    </view>
+
+
+
+    <view class="cu-panel cu-bill-panel">
+      <view class="cu-panel__title">鏀舵敮娴佹按</view>
+      <view v-if="revenues.length" class="cu-revenue-list">
+        <view v-for="r in revenues" :key="r.id" class="cu-revenue-card">
+          <view class="cu-revenue-card__head">
+            <text :class="['cu-revenue-card__type', r.revenueType === 0 ? 'cu-revenue-card__type--out' : '']">
+              {{ revenueTypeText(r.revenueType) }}
+            </text>
+            <text class="cu-revenue-card__amount">楼{{ formatMoney(r.actReceivableFee) }}</text>
+          </view>
+          <view class="cu-revenue-card__row">
+            <text class="cu-revenue-card__label">浠樻鏂�</text>
+            <text class="cu-revenue-card__value">{{ r.customerName || '-' }}</text>
+          </view>
+          <view class="cu-revenue-card__row">
+            <text class="cu-revenue-card__label">鏀舵鏂瑰紡</text>
+            <text class="cu-revenue-card__value">{{ payTypeText(r.payType) }}</text>
+          </view>
+          <view class="cu-revenue-card__row">
+            <text class="cu-revenue-card__label">鍏ヨ处鏃ユ湡</text>
+            <text class="cu-revenue-card__value">{{ formatDate(r.actPayDate) || '-' }}</text>
+          </view>
+          <view class="cu-revenue-card__row">
+            <text class="cu-revenue-card__label">鍒涘缓鏃堕棿</text>
+            <text class="cu-revenue-card__value">{{ formatTime(r.createDate) }}</text>
+          </view>
+          <view v-if="r.remark" class="cu-revenue-card__row">
+            <text class="cu-revenue-card__label">澶囨敞</text>
+            <text class="cu-revenue-card__value">{{ r.remark }}</text>
+          </view>
+        </view>
+      </view>
+      <view v-else class="cu-bill-empty-record cu-bill-empty-record--inset">
+        <text class="cu-bill-empty-record__icon">馃搵</text>
+        <text class="cu-bill-empty-record__text">鏆傛棤鏀舵敮娴佹按</text>
+      </view>
+    </view>
+
+
+
+    <view v-if="needPayAmount > 0" class="cu-page-footer cu-bill-detail-footer">
+
+      <view class="cu-bill-detail-footer__info">
+
+        <text class="cu-bill-detail-footer__label">寰呮敮浠�</text>
+
+        <text class="cu-bill-detail-footer__amount">楼{{ formatMoney(needPayAmount) }}</text>
+
+      </view>
+
+      <view class="cu-btn cu-btn--primary cu-bill-detail-footer__btn" @click="goPay">绔嬪嵆缂磋垂</view>
+
+    </view>
+
+  </view>
+
+</template>
+
+
+
+<script>
+
+import { customerBillDetail } from '@/api'
+
+
+
+const COST_TYPE_MAP = {
+
+  0: '绉熻祦璐�',
+
+  1: '鐗╀笟璐�',
+
+  2: '绉熻祦鎶奸噾',
+
+  3: '鐗╀笟鎶奸噾',
+
+  4: '姘寸數璐�',
+
+  5: '鏉傞」璐�',
+
+  6: '鍏朵粬',
+
+  7: '淇濊瘉閲�'
+
+}
+
+
+
+export default {
+
+  data () { return { id: null, bill: null, revenues: [], paidAmount: 0, needPayAmount: 0 } },
+
+  computed: {
+
+    heroClass () {
+
+      if (!this.bill) return ''
+
+      if (this.bill.payStatus === 1) return 'cu-bill-detail-hero--ok'
+
+      if (this.needPayAmount > 0) return 'cu-bill-detail-hero--warn'
+
+      return ''
+
+    }
+
+  },
+
+  onLoad (q) { this.id = q.id; this.load() },
+
+  methods: {
+
+    load () {
+
+      customerBillDetail(this.id).then(res => {
+        this.bill = res.data.bill
+        this.revenues = res.data.revenues || (res.data.bill && res.data.bill.ywContractRevenueList) || []
+        this.paidAmount = res.data.paidAmount != null ? res.data.paidAmount : (res.data.bill && res.data.bill.actReceivableFee)
+        this.needPayAmount = res.data.needPayAmount != null ? res.data.needPayAmount : (res.data.bill && res.data.bill.needReceivableFee)
+      })
+
+    },
+
+    costTypeText (type) { return COST_TYPE_MAP[type] || '璐﹀崟' },
+
+    formatMoney (val) {
+
+      if (val === null || val === undefined || val === '') return '0.00'
+
+      return Number(val).toFixed(2)
+
+    },
+
+    formatTime (t) { return t ? String(t).replace('T', ' ').substring(0, 19) : '-' },
+
+    contractPeriod (item) {
+      if (!item) return '-'
+      const start = this.formatDate(item.contractStartDate)
+      const end = this.formatDate(item.contractEndDate)
+      if (!start && !end) return '-'
+      return `${start || '-'} ~ ${end || '-'}`
+    },
+    formatDate (val) {
+      if (!val) return ''
+      return String(val).replace('T', ' ').substring(0, 10)
+    },
+    formatArea (area) {
+      if (area === null || area === undefined || area === '') return '-'
+      return `${area}銕
+    },
+    formatRoomLine (room) {
+      if (!room) return '-'
+      const parts = [room.projectName, room.buildingName, room.floorName, room.roomNum].filter(Boolean)
+      return parts.length ? parts.join('/') : '-'
+    },
+    revenueTypeText (type) {
+      if (type === 1) return '鏀舵'
+      return '浠樻'
+    },
+    payTypeText (type) {
+      const map = { 0: '鐜伴噾', 1: '缃戦摱杞处', 2: 'POS鏈�', 3: '鏀粯瀹�', 4: '寰俊', 5: '杞处鏀エ', 6: '鍏朵粬' }
+      return map[type] || '-'
+    },
+    payText (s) { return { 0: '寰呮敮浠�', 1: '宸叉敮浠�', 2: '閮ㄥ垎鏀粯' }[s] || '-' },
+
+    goPay () { uni.navigateTo({ url: `/pages/customer/bill/pay?id=${this.id}&amount=${this.needPayAmount}` }) }
+
+  }
+
+}
+
+</script>
+
+
diff --git a/h5/pages/customer/bill/list.vue b/h5/pages/customer/bill/list.vue
new file mode 100644
index 0000000..3f19a89
--- /dev/null
+++ b/h5/pages/customer/bill/list.vue
@@ -0,0 +1,263 @@
+<template>
+
+  <view class="cu-page cu-bill-page">
+
+    <view class="cu-bill-page__header">
+
+      <view class="cu-tabs cu-bill-tabs">
+
+        <view
+
+          v-for="(t, i) in tabs"
+
+          :key="i"
+
+          :class="['cu-tab', tabIdx === i ? 'cu-tab--active' : '']"
+
+          @click="tabIdx = i; load()"
+
+        >{{ t }}</view>
+
+      </view>
+
+      <view class="cu-bill-summary">
+
+        <text class="cu-bill-summary__count">{{ list.length }}</text>
+
+        <text class="cu-bill-summary__label">绗旇处鍗�</text>
+
+      </view>
+
+    </view>
+
+
+
+    <view class="cu-list-wrap">
+
+      <view
+
+        v-for="item in list"
+
+        :key="item.id"
+
+        class="cu-bill-card"
+
+        @click="goDetail(item.id)"
+
+      >
+
+        <view class="cu-bill-card__accent" :class="accentClass(item)" />
+
+
+
+        <view class="cu-bill-card__body">
+
+          <view class="cu-bill-card__head">
+
+            <view class="cu-bill-card__head-main">
+
+              <text class="cu-bill-card__type">{{ costTypeText(item.costType) }}</text>
+
+              <text class="cu-bill-card__code">{{ item.code }}</text>
+
+            </view>
+
+            <text :class="['cu-status', payStatusClass(item.payStatus)]">{{ payText(item.payStatus) }}</text>
+
+          </view>
+
+
+
+          <view class="cu-bill-card__amount-box">
+
+            <view class="cu-bill-card__amount-main">
+
+              <text class="cu-bill-card__amount-label">搴斾粯閲戦</text>
+
+              <text class="cu-bill-card__amount-value">楼{{ formatMoney(item.receivableFee) }}</text>
+
+            </view>
+
+            <view class="cu-bill-card__amount-side">
+
+              <text class="cu-bill-card__amount-side-label">瀹炰粯</text>
+
+              <text class="cu-bill-card__amount-side-value">楼{{ formatMoney(item.actReceivableFee) }}</text>
+
+            </view>
+
+          </view>
+
+
+
+          <view v-if="isOverdue(item)" class="cu-bill-card__overdue">宸查�炬湡锛岃灏藉揩缂磋垂</view>
+
+
+
+          <view class="cu-bill-card__contract">
+
+            <view class="cu-bill-card__contract-row">
+
+              <text class="cu-bill-card__contract-label">鍚堝悓缂栧彿</text>
+
+              <text class="cu-bill-card__contract-value">{{ item.contractCode || '-' }}</text>
+
+            </view>
+
+            <view class="cu-bill-card__contract-row">
+
+              <text class="cu-bill-card__contract-label">鍚堝悓鏈夋晥鏈�</text>
+
+              <text class="cu-bill-card__contract-value">{{ contractPeriod(item) }}</text>
+
+            </view>
+
+          </view>
+
+
+
+          <view class="cu-bill-card__meta">
+            <view class="cu-bill-card__meta-item cu-bill-card__meta-item--full">
+              <text class="cu-bill-card__meta-label">璁¤垂鍛ㄦ湡</text>
+              <text class="cu-bill-card__meta-value">{{ item.startDate }} ~ {{ item.endDate }}</text>
+            </view>
+          </view>
+
+
+
+          <view class="cu-bill-card__foot">
+
+            <text class="cu-bill-card__foot-hint">鏌ョ湅璐﹀崟鏄庣粏涓庢敮浠樿褰�</text>
+
+            <text class="cu-bill-card__foot-link">璇︽儏 鈫�</text>
+
+          </view>
+
+        </view>
+
+      </view>
+
+      <u-empty v-if="!list.length" text="鏆傛棤璐﹀崟" margin-top="80" />
+
+    </view>
+
+  </view>
+
+</template>
+
+
+
+<script>
+
+import { customerBillPage } from '@/api'
+
+
+
+const COST_TYPE_MAP = {
+
+  0: '绉熻祦璐�',
+
+  1: '鐗╀笟璐�',
+
+  2: '绉熻祦鎶奸噾',
+
+  3: '鐗╀笟鎶奸噾',
+
+  4: '姘寸數璐�',
+
+  5: '鏉傞」璐�',
+
+  6: '鍏朵粬',
+
+  7: '淇濊瘉閲�'
+
+}
+
+
+
+export default {
+
+  data () { return { list: [], tabIdx: 0, tabs: ['鍏ㄩ儴', '寰呮敮浠�', '宸叉敮浠�'] } },
+
+  onShow () { this.load() },
+
+  methods: {
+
+    load () {
+
+      const payTab = this.tabIdx === 0 ? null : (this.tabIdx === 1 ? 0 : 1)
+
+      customerBillPage({ page: 1, capacity: 50, model: { payTab } })
+
+        .then(res => { this.list = (res.data && res.data.records) || [] })
+
+    },
+
+    costTypeText (type) { return COST_TYPE_MAP[type] || '璐﹀崟' },
+
+    formatMoney (val) {
+
+      if (val === null || val === undefined || val === '') return '0.00'
+
+      return Number(val).toFixed(2)
+
+    },
+
+    payText (s) { return { 0: '寰呮敮浠�', 1: '宸叉敮浠�', 2: '閮ㄥ垎鏀粯' }[s] || '-' },
+
+    payStatusClass (s) {
+
+      if (s === 1) return 'cu-status--ok'
+
+      if (s === 0) return 'cu-status--warn'
+
+      return 'cu-status--muted'
+
+    },
+
+    accentClass (item) {
+
+      if (item.payStatus === 1) return 'cu-bill-card__accent--ok'
+
+      if (this.isOverdue(item)) return 'cu-bill-card__accent--danger'
+
+      if (item.payStatus === 0) return 'cu-bill-card__accent--warn'
+
+      return ''
+
+    },
+
+    isOverdue (item) {
+
+      if (!item || item.payStatus === 1 || !item.planPayDate) return false
+
+      const today = new Date()
+
+      today.setHours(0, 0, 0, 0)
+
+      const due = new Date(String(item.planPayDate).replace(/-/g, '/'))
+
+      return due < today
+
+    },
+
+    contractPeriod (item) {
+      if (!item) return '-'
+      const start = this.formatDate(item.contractStartDate)
+      const end = this.formatDate(item.contractEndDate)
+      if (!start && !end) return '-'
+      return `${start || '-'} ~ ${end || '-'}`
+    },
+    formatDate (val) {
+      if (!val) return ''
+      return String(val).replace('T', ' ').substring(0, 10)
+    },
+    goDetail (id) { uni.navigateTo({ url: `/pages/customer/bill/detail?id=${id}` }) }
+
+  }
+
+}
+
+</script>
+
+
diff --git a/h5/pages/customer/bill/pay.vue b/h5/pages/customer/bill/pay.vue
new file mode 100644
index 0000000..9910c28
--- /dev/null
+++ b/h5/pages/customer/bill/pay.vue
@@ -0,0 +1,60 @@
+<template>
+  <view class="cu-page cu-page--with-footer" 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">
+        <text class="cu-detail-hero__amount-value">楼{{ amount || '0.00' }}</text>
+      </view>
+    </view>
+
+    <view class="cu-pay-amount-box">
+      <view class="cu-pay-amount-box__label">璋冩暣缂磋垂閲戦</view>
+      <view class="cu-pay-amount-box__input-wrap">
+        <text class="cu-pay-amount-box__symbol">楼</text>
+        <input v-model="amount" class="cu-pay-amount-box__input" type="digit" placeholder="0.00" />
+      </view>
+      <view class="cu-pay-amount-box__remark">
+        <text class="cu-pay-amount-box__remark-label">澶囨敞</text>
+        <input v-model="remark" placeholder="閫夊~" />
+      </view>
+    </view>
+
+    <view class="cu-page-footer">
+      <view class="cu-btn cu-btn--primary" @click="submit">纭缂磋垂{{ amount ? ' 楼' + amount : '' }}</view>
+    </view>
+  </view>
+</template>
+
+<script>
+import { customerBillDetail, customerPayCreate } from '@/api'
+import { invokeWxPay } from '@/utils/wxpay.js'
+export default {
+  data () { return { billId: null, amount: '', remark: '' } },
+  onLoad (q) {
+    this.billId = q.id
+    this.amount = q.amount || ''
+    if (!this.amount) {
+      customerBillDetail(q.id).then(res => { this.amount = String(res.data.needPayAmount || '') })
+    }
+  },
+  methods: {
+    submit () {
+      if (!this.amount) return uni.showToast({ title: '璇疯緭鍏ラ噾棰�', icon: 'none' })
+      customerPayCreate({
+        orderType: 2,
+        billId: Number(this.billId),
+        amount: Number(this.amount),
+        remark: this.remark,
+        openid: this.$store.state.openId
+      }).then(async res => {
+        try {
+          await invokeWxPay(res.data)
+          uni.redirectTo({ url: `/pages/customer/pay/result?success=1&orderNo=${res.data.orderNo}&type=bill&billId=${this.billId}` })
+        } catch (e) {
+          uni.redirectTo({ url: `/pages/customer/pay/result?success=0&orderNo=${res.data.orderNo}&type=bill` })
+        }
+      })
+    }
+  }
+}
+</script>
diff --git a/h5/pages/customer/conditioner/recharge.vue b/h5/pages/customer/conditioner/recharge.vue
new file mode 100644
index 0000000..9c06412
--- /dev/null
+++ b/h5/pages/customer/conditioner/recharge.vue
@@ -0,0 +1,77 @@
+<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-device-summary__balance">
+        <view class="cu-device-summary__balance-label">褰撳墠璐︽埛浣欓</view>
+        <view class="cu-device-summary__balance-value">{{ device.balance }}</view>
+      </view>
+    </view>
+
+    <view class="cu-pay-amount-box">
+      <view class="cu-pay-amount-box__label">鍏呭�奸噾棰�</view>
+      <view class="cu-pay-amount-box__input-wrap">
+        <text class="cu-pay-amount-box__symbol">楼</text>
+        <input v-model="amount" class="cu-pay-amount-box__input" type="digit" placeholder="0.00" />
+      </view>
+      <view class="cu-quick-amounts">
+        <text
+          v-for="q in quickAmounts"
+          :key="q"
+          :class="['cu-quick-amount', String(amount) === String(q) ? 'cu-quick-amount--active' : '']"
+          @click="amount = String(q)"
+        >{{ q }}鍏�</text>
+      </view>
+      <view class="cu-pay-amount-box__remark">
+        <text class="cu-pay-amount-box__remark-label">澶囨敞</text>
+        <input v-model="remark" placeholder="閫夊~" />
+      </view>
+    </view>
+
+    <view class="cu-page-footer">
+      <view class="cu-btn cu-btn--primary" @click="submit">纭鍏呭�納{ amount ? ' 楼' + amount : '' }}</view>
+    </view>
+  </view>
+</template>
+
+<script>
+import { customerDeviceDetail, customerPayCreate } from '@/api'
+import { invokeWxPay } from '@/utils/wxpay.js'
+export default {
+  data () {
+    return {
+      deviceId: null,
+      device: null,
+      amount: '',
+      remark: '',
+      quickAmounts: [50, 100, 200, 500]
+    }
+  },
+  onLoad (q) { this.deviceId = q.id; this.load() },
+  methods: {
+    load () {
+      customerDeviceDetail({ deviceType: 1, deviceId: this.deviceId }).then(res => { this.device = res.data })
+    },
+    submit () {
+      if (!this.amount) return uni.showToast({ title: '璇疯緭鍏ラ噾棰�', icon: 'none' })
+      customerPayCreate({
+        orderType: 1,
+        amount: Number(this.amount),
+        remark: this.remark,
+        openid: this.$store.state.openId
+      }).then(async res => {
+        try {
+          await invokeWxPay(res.data)
+          uni.redirectTo({ url: `/pages/customer/pay/result?success=1&orderNo=${res.data.orderNo}&type=recharge` })
+        } catch (e) {
+          uni.redirectTo({ url: `/pages/customer/pay/result?success=0&orderNo=${res.data.orderNo}&type=recharge` })
+        }
+      })
+    }
+  }
+}
+</script>
diff --git a/h5/pages/customer/contract/detail.vue b/h5/pages/customer/contract/detail.vue
new file mode 100644
index 0000000..5085398
--- /dev/null
+++ b/h5/pages/customer/contract/detail.vue
@@ -0,0 +1,596 @@
+<template>
+
+  <view class="cu-page cu-page--with-footer" v-if="contract">
+
+    <view :class="['cu-detail-hero', contract.status === 1 ? 'cu-detail-hero--green' : '']">
+
+      <view class="cu-detail-hero__top">
+
+        <view>
+
+          <view class="cu-detail-hero__label">鍚堝悓缂栧彿</view>
+
+          <view class="cu-detail-hero__code">{{ contract.code }}</view>
+
+        </view>
+
+        <text class="cu-status">{{ statusText(contract.status) }}</text>
+
+      </view>
+
+      <view class="cu-detail-hero__amount">
+
+        <text class="cu-detail-hero__amount-label">绉熻祦闈㈢Н</text>
+
+        <text class="cu-detail-hero__amount-value">{{ formatArea(contract.totalArea) }}</text>
+
+      </view>
+
+    </view>
+
+
+
+    <view v-if="contract.billStatusTip" :class="['cu-bill-tip', 'cu-bill-tip--inset', billTipClass(contract.billStatusType)]">
+
+      <text class="cu-bill-tip__icon">{{ billTipIcon(contract.billStatusType) }}</text>
+
+      <text class="cu-bill-tip__text">{{ contract.billStatusTip }}</text>
+
+    </view>
+
+
+
+    <view class="cu-segment">
+
+      <view :class="['cu-segment__item', tab === 0 ? 'cu-segment__item--active' : '']" @click="tab = 0">鍚堝悓淇℃伅</view>
+
+      <view :class="['cu-segment__item', tab === 1 ? 'cu-segment__item--active' : '']" @click="tab = 1">鍏宠仈璐﹀崟</view>
+
+    </view>
+
+
+
+    <view v-if="tab === 0">
+
+      <view class="cu-panel">
+
+        <view class="cu-panel__title">鎴挎簮淇℃伅</view>
+
+        <view v-if="contract.roomList && contract.roomList.length" class="cu-room-list">
+
+          <view v-for="(room, idx) in contract.roomList" :key="idx" class="cu-room-item">
+
+            <text class="cu-room-item__name">{{ formatRoomLine(room) }}</text>
+
+            <text class="cu-room-item__area">{{ formatArea(room.rentArea) }}</text>
+
+          </view>
+
+        </view>
+
+        <view v-else class="cu-kv__item">
+
+          <text class="cu-kv__value cu-kv__value--muted">{{ contract.roomInfo || '鏆傛棤鎴块棿淇℃伅' }}</text>
+
+        </view>
+
+      </view>
+
+
+
+      <view class="cu-panel">
+
+        <view class="cu-panel__title">鍩烘湰淇℃伅</view>
+
+        <view class="cu-kv__item">
+
+          <text class="cu-kv__label">寮�濮嬫棩鏈�</text>
+
+          <text class="cu-kv__value">{{ contract.startDate || '-' }}</text>
+
+        </view>
+
+        <view class="cu-kv__item">
+
+          <text class="cu-kv__label">缁撴潫鏃ユ湡</text>
+
+          <text class="cu-kv__value">{{ contract.endDate || '-' }}</text>
+
+        </view>
+
+        <view class="cu-kv__item">
+
+          <text class="cu-kv__label">绛捐鏃ユ湡</text>
+
+          <text class="cu-kv__value">{{ contract.signDate || '-' }}</text>
+
+        </view>
+
+        <view class="cu-kv__item">
+
+          <text class="cu-kv__label">浠樻鏂瑰紡</text>
+
+          <text class="cu-kv__value">{{ contract.payTypeText || '-' }}</text>
+
+        </view>
+
+        <view class="cu-kv__item">
+
+          <text class="cu-kv__label">鍏嶇鏈�</text>
+
+          <text class="cu-kv__value">{{ contract.freeRentPeriod || '-' }}</text>
+
+        </view>
+
+        <view class="cu-kv__item">
+
+          <text class="cu-kv__label">绉熻祦鎶奸噾</text>
+
+          <text class="cu-kv__value">楼{{ contract.zlDeposit != null ? contract.zlDeposit : '-' }}</text>
+
+        </view>
+
+        <view class="cu-kv__item">
+
+          <text class="cu-kv__label">鍚堝悓鍗曚环</text>
+
+          <text class="cu-kv__value cu-kv__value--danger">楼{{ contract.zlFirstPrice != null ? contract.zlFirstPrice : '-' }}/鏈�</text>
+
+        </view>
+
+      </view>
+
+
+
+      <view class="cu-panel">
+
+        <view class="cu-panel__title">鍚堝悓闄勪欢</view>
+
+        <view v-if="contract.fileList && contract.fileList.length">
+
+          <view
+
+            v-for="file in contract.fileList"
+
+            :key="file.id"
+
+            class="cu-file-item"
+
+            @click="openFile(file)"
+
+          >
+
+            <text class="cu-file-item__icon">馃搸</text>
+
+            <view class="cu-file-item__main">
+
+              <text class="cu-file-item__name">{{ file.name || '闄勪欢' }}</text>
+
+              <text class="cu-file-item__time">{{ file.createDate || '' }}</text>
+
+            </view>
+
+            <text class="cu-file-item__action">涓嬭浇</text>
+
+          </view>
+
+        </view>
+
+        <view v-else class="cu-kv__item">
+
+          <text class="cu-kv__value cu-kv__value--muted">鏆傛棤闄勪欢</text>
+
+        </view>
+
+      </view>
+
+    </view>
+
+
+
+    <template v-else>
+
+      <view class="cu-contract-bill-panel">
+
+        <view class="cu-contract-bill-switch">
+
+          <view
+
+            :class="['cu-contract-bill-switch__item', 'cu-contract-bill-switch__item--pay', billType === 0 ? 'cu-contract-bill-switch__item--active' : '']"
+
+            @click="switchBillType(0)"
+
+          >
+
+            <text class="cu-contract-bill-switch__icon">浠�</text>
+
+            <view class="cu-contract-bill-switch__text">
+
+              <text class="cu-contract-bill-switch__title">浠樻璐﹀崟</text>
+
+              <text class="cu-contract-bill-switch__desc">闇�鍚戠墿涓氱即绾�</text>
+
+            </view>
+
+          </view>
+
+          <view
+
+            :class="['cu-contract-bill-switch__item', 'cu-contract-bill-switch__item--in', billType === 1 ? 'cu-contract-bill-switch__item--active' : '']"
+
+            @click="switchBillType(1)"
+
+          >
+
+            <text class="cu-contract-bill-switch__icon">鏀�</text>
+
+            <view class="cu-contract-bill-switch__text">
+
+              <text class="cu-contract-bill-switch__title">鏀舵璐﹀崟</text>
+
+              <text class="cu-contract-bill-switch__desc">鐗╀笟搴旈��杩�/鏀粯</text>
+
+            </view>
+
+          </view>
+
+        </view>
+
+
+
+        <view class="cu-contract-bill-summary">
+
+          <text class="cu-contract-bill-summary__label">{{ billType === 0 ? '寰呯即璐﹀崟' : '寰呮敹璐﹀崟' }}</text>
+
+          <text class="cu-contract-bill-summary__count">{{ bills.length }} 绗�</text>
+
+        </view>
+
+
+
+        <view v-if="billsLoading" class="cu-contract-bill-loading">
+
+          <text>鍔犺浇涓�...</text>
+
+        </view>
+
+
+
+        <view v-else-if="bills.length" class="cu-contract-bill-list">
+
+          <view
+
+            v-for="b in bills"
+
+            :key="b.id"
+
+            :class="['cu-contract-bill-card', billType === 0 ? 'cu-contract-bill-card--pay' : 'cu-contract-bill-card--in']"
+
+            @click="goBillDetail(b.id)"
+
+          >
+
+            <view class="cu-contract-bill-card__top">
+
+              <view class="cu-contract-bill-card__title-wrap">
+
+                <text class="cu-contract-bill-card__type">{{ costTypeText(b.costType) }}</text>
+
+                <text class="cu-contract-bill-card__code">{{ b.code || '-' }}</text>
+
+              </view>
+
+              <text :class="['cu-contract-bill-card__status', billPayStatusClass(b.payStatus)]">{{ payStatusText(b.payStatus) }}</text>
+
+            </view>
+
+
+
+            <view class="cu-contract-bill-card__amounts">
+
+              <view class="cu-contract-bill-card__amount-item">
+
+                <text class="cu-contract-bill-card__amount-label">{{ receivableLabel }}</text>
+
+                <text class="cu-contract-bill-card__amount-value">楼{{ formatMoney(b.receivableFee) }}</text>
+
+              </view>
+
+              <view class="cu-contract-bill-card__amount-divider" />
+
+              <view class="cu-contract-bill-card__amount-item">
+
+                <text class="cu-contract-bill-card__amount-label">{{ receivedLabel }}</text>
+
+                <text class="cu-contract-bill-card__amount-value cu-contract-bill-card__amount-value--muted">楼{{ formatMoney(b.actReceivableFee) }}</text>
+
+              </view>
+
+            </view>
+
+
+
+            <view v-if="b.isOverdue === 1" class="cu-contract-bill-card__overdue">宸查�炬湡</view>
+
+
+
+            <view class="cu-contract-bill-card__meta">
+
+              <view class="cu-contract-bill-card__meta-row">
+
+                <text class="cu-contract-bill-card__meta-label">璐﹀崟閲戦</text>
+
+                <text class="cu-contract-bill-card__meta-value">楼{{ formatMoney(b.totleFee) }}</text>
+
+              </view>
+
+              <view class="cu-contract-bill-card__meta-row">
+
+                <text class="cu-contract-bill-card__meta-label">璁¤垂鍛ㄦ湡</text>
+
+                <text class="cu-contract-bill-card__meta-value">{{ formatDate(b.startDate) || '-' }} ~ {{ formatDate(b.endDate) || '-' }}</text>
+
+              </view>
+
+              <view class="cu-contract-bill-card__meta-row">
+
+                <text class="cu-contract-bill-card__meta-label">{{ dueDateLabel }}</text>
+
+                <text class="cu-contract-bill-card__meta-value">{{ formatDate(b.planPayDate) || '-' }}</text>
+
+              </view>
+
+              <view class="cu-contract-bill-card__meta-row">
+
+                <text class="cu-contract-bill-card__meta-label">鏄惁閫炬湡</text>
+
+                <text :class="['cu-contract-bill-card__meta-value', b.isOverdue === 1 ? 'cu-contract-bill-card__meta-value--danger' : '']">
+
+                  {{ overdueText(b) }}
+
+                </text>
+
+              </view>
+
+            </view>
+
+
+
+            <view class="cu-contract-bill-card__foot">
+
+              <text>鏌ョ湅璇︽儏</text>
+
+              <text class="cu-contract-bill-card__arrow">鈫�</text>
+
+            </view>
+
+          </view>
+
+        </view>
+
+
+
+        <u-empty v-else :text="billType === 0 ? '鏆傛棤浠樻璐﹀崟' : '鏆傛棤鏀舵璐﹀崟'" margin-top="60" />
+
+      </view>
+
+    </template>
+
+  </view>
+
+</template>
+
+
+
+<script>
+
+import { customerContractDetail } from '@/api'
+
+const COST_TYPE_MAP = {
+  0: '绉熻祦璐�',
+  1: '鐗╀笟璐�',
+  2: '绉熻祦鎶奸噾',
+  3: '鐗╀笟鎶奸噾',
+  4: '姘寸數璐�',
+  5: '鏉傞」璐�',
+  6: '鍏朵粬',
+  7: '淇濊瘉閲�'
+}
+
+export default {
+
+  data () { return { id: null, contract: null, bills: [], tab: 0, billType: 0, billsLoading: false } },
+
+  computed: {
+    receivableLabel () { return this.billType === 0 ? '搴斾粯閲戦' : '搴旀敹閲戦' },
+    receivedLabel () { return this.billType === 0 ? '瀹炰粯閲戦' : '瀹炴敹閲戦' },
+    dueDateLabel () { return this.billType === 0 ? '搴斾粯鏃ユ湡' : '搴旀敹鏃ユ湡' }
+  },
+
+  onLoad (q) { this.id = q.id; this.load() },
+
+  methods: {
+
+    load (reloadContract = true) {
+
+      if (!reloadContract) this.billsLoading = true
+
+      customerContractDetail(this.id, this.billType).then(res => {
+
+        if (reloadContract) this.contract = res.data.contract
+
+        this.bills = res.data.bills || []
+
+      }).finally(() => {
+
+        this.billsLoading = false
+
+      })
+
+    },
+
+    switchBillType (type) {
+
+      if (this.billType === type) return
+
+      this.billType = type
+
+      this.bills = []
+
+      this.load(false)
+
+    },
+
+    costTypeText (type) { return COST_TYPE_MAP[type] || '璐﹀崟' },
+
+    formatMoney (val) {
+
+      if (val === null || val === undefined || val === '') return '0.00'
+
+      return Number(val).toFixed(2)
+
+    },
+
+    formatDate (val) {
+
+      if (!val) return ''
+
+      return String(val).replace('T', ' ').substring(0, 10)
+
+    },
+
+    formatArea (area) {
+
+      if (area === null || area === undefined || area === '') return '-'
+
+      return `${area}銕
+
+    },
+
+    formatRoomLine (room) {
+
+      if (!room) return '-'
+
+      const parts = [room.projectName, room.buildingName, room.floorName, room.roomNum].filter(Boolean)
+
+      return parts.length ? parts.join('/') : '-'
+
+    },
+
+    statusText (s) { return { 0: '寰呮墽琛�', 1: '鎵ц涓�', 2: '宸插埌鏈�', 3: '閫�绉熶腑', 4: '宸查��绉�' }[s] || '-' },
+
+    payStatusText (s) {
+
+      const common = { 1: '宸茬粨娓�', 2: '閮ㄥ垎缁撴竻', 5: '宸插叧闂�' }
+
+      if (common[s]) return common[s]
+
+      if (this.billType === 0) {
+
+        return { 0: '寰呬粯娆�', 3: '寰呬粯娆�', 4: '寰呴��娆�' }[s] || '-'
+
+      }
+
+      return { 0: '寰呮敹娆�', 3: '寰呮敹娆�', 4: '寰呴��娆�' }[s] || '-'
+
+    },
+
+    billPayStatusClass (s) {
+
+      if (s === 1) return 'cu-contract-bill-card__status--ok'
+
+      if (s === 0 || s === 3) return 'cu-contract-bill-card__status--warn'
+
+      if (s === 4) return 'cu-contract-bill-card__status--bad'
+
+      return 'cu-contract-bill-card__status--muted'
+
+    },
+
+    overdueText (item) {
+
+      if (!item) return '-'
+
+      return item.isOverdue === 1 ? '鏄�' : '鍚�'
+
+    },
+
+    goBillDetail (id) { uni.navigateTo({ url: `/pages/customer/bill/detail?id=${id}` }) },
+
+    billTipClass (type) {
+
+      if (type === 'danger') return 'cu-bill-tip--danger'
+
+      if (type === 'warn') return 'cu-bill-tip--warn'
+
+      return 'cu-bill-tip--ok'
+
+    },
+
+    billTipIcon (type) {
+
+      if (type === 'danger') return '鈿狅笍'
+
+      if (type === 'warn') return '鈴�'
+
+      return '鉁�'
+
+    },
+
+    openFile (file) {
+
+      const url = file.fileurlFull || file.fileurl
+
+      if (!url) {
+
+        uni.showToast({ title: '闄勪欢鍦板潃鏃犳晥', icon: 'none' })
+
+        return
+
+      }
+
+      // #ifdef H5
+
+      window.open(url, '_blank')
+
+      // #endif
+
+      // #ifndef H5
+
+      uni.showLoading({ title: '涓嬭浇涓�' })
+
+      uni.downloadFile({
+
+        url,
+
+        success: (res) => {
+
+          if (res.statusCode === 200) {
+
+            uni.openDocument({ filePath: res.tempFilePath, showMenu: true })
+
+          } else {
+
+            uni.showToast({ title: '涓嬭浇澶辫触', icon: 'none' })
+
+          }
+
+        },
+
+        fail: () => uni.showToast({ title: '涓嬭浇澶辫触', icon: 'none' }),
+
+        complete: () => uni.hideLoading()
+
+      })
+
+      // #endif
+
+    }
+
+  }
+
+}
+
+</script>
+
+
diff --git a/h5/pages/customer/contract/list.vue b/h5/pages/customer/contract/list.vue
new file mode 100644
index 0000000..73c3e01
--- /dev/null
+++ b/h5/pages/customer/contract/list.vue
@@ -0,0 +1,103 @@
+<template>

+  <view class="cu-page">

+    <scroll-view scroll-x class="cu-tabs cu-tabs--scroll">

+      <view

+        v-for="(t, i) in tabs"

+        :key="t.value"

+        :class="['cu-tab', tabIdx === i ? 'cu-tab--active' : '']"

+        @click="tabIdx = i; load()"

+      >{{ t.label }}</view>

+    </scroll-view>

+

+    <view class="cu-list-header">

+      <text class="cu-list-header__count">鍏� {{ list.length }} 浠藉悎鍚�</text>

+    </view>

+

+    <view class="cu-list-wrap">

+      <view v-for="item in list" :key="item.id" class="cu-list-card cu-list-card--clickable" @click="goDetail(item.id)">

+        <view class="cu-list-card__head">

+          <view class="cu-list-card__icon cu-list-card__icon--contract">馃搫</view>

+          <view class="cu-list-card__main">

+            <view class="cu-list-card__title-row">

+              <text class="cu-list-card__title">{{ item.code }}</text>

+              <text :class="['cu-status', contractStatusClass(item.status)]">{{ statusText(item.status) }}</text>

+            </view>

+            <text class="cu-list-card__sub">{{ item.roomInfo || '鏆傛棤鎴块棿淇℃伅' }}</text>

+            <view class="cu-period-chip">{{ item.startDate }} ~ {{ item.endDate }}</view>

+          </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">{{ formatArea(item.totalArea) }}</text>

+          </view>

+          <view class="cu-info-cell">

+            <text class="cu-info-cell__label">浠樻鏂瑰紡</text>

+            <text class="cu-info-cell__value">{{ item.payTypeText || '-' }}</text>

+          </view>

+        </view>

+

+        <view v-if="item.billStatusTip" :class="['cu-bill-tip', billTipClass(item.billStatusType)]">

+          <text class="cu-bill-tip__icon">{{ billTipIcon(item.billStatusType) }}</text>

+          <text class="cu-bill-tip__text">{{ item.billStatusTip }}</text>

+        </view>

+

+        <view class="cu-list-card__foot">

+          <text class="cu-time">鐐瑰嚮鏌ョ湅瀹屾暣鍚堝悓淇℃伅</text>

+          <text class="cu-list-card__arrow">璇︽儏 鈫�</text>

+        </view>

+      </view>

+      <u-empty v-if="!list.length" text="鏆傛棤鍚堝悓" margin-top="80" />

+    </view>

+  </view>

+</template>

+

+<script>

+import { customerContractPage } from '@/api'

+export default {

+  data () {

+    return {

+      list: [],

+      tabIdx: 0,

+      tabs: [

+        { label: '鍏ㄩ儴', value: null }, { label: '寰呮墽琛�', value: 0 }, { label: '鎵ц涓�', value: 1 },

+        { label: '宸插埌鏈�', value: 2 }, { label: '閫�绉熶腑', value: 3 }, { label: '宸查��绉�', value: 4 }

+      ]

+    }

+  },

+  onShow () { this.load() },

+  methods: {

+    load () {

+      customerContractPage({ page: 1, capacity: 50, model: { status: this.tabs[this.tabIdx].value } })

+        .then(res => { this.list = (res.data && res.data.records) || [] })

+    },

+    formatArea (area) {

+      if (area === null || area === undefined || area === '') return '-'

+      return `${area}銕

+    },

+    statusText (s) {

+      const map = { 0: '寰呮墽琛�', 1: '鎵ц涓�', 2: '宸插埌鏈�', 3: '閫�绉熶腑', 4: '宸查��绉�' }

+      return map[s] || '-'

+    },

+    contractStatusClass (s) {

+      if (s === 1) return 'cu-status--ok'

+      if (s === 2 || s === 4) return 'cu-status--muted'

+      if (s === 3) return 'cu-status--warn'

+      return 'cu-status--muted'

+    },

+    billTipClass (type) {

+      if (type === 'danger') return 'cu-bill-tip--danger'

+      if (type === 'warn') return 'cu-bill-tip--warn'

+      return 'cu-bill-tip--ok'

+    },

+    billTipIcon (type) {

+      if (type === 'danger') return '鈿狅笍'

+      if (type === 'warn') return '鈴�'

+      return '鉁�'

+    },

+    goDetail (id) { uni.navigateTo({ url: `/pages/customer/contract/detail?id=${id}` }) }

+  }

+}

+</script>

+
\ No newline at end of file
diff --git a/h5/pages/customer/electricity/list.vue b/h5/pages/customer/electricity/list.vue
new file mode 100644
index 0000000..97e7891
--- /dev/null
+++ b/h5/pages/customer/electricity/list.vue
@@ -0,0 +1,96 @@
+<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-list-header">
+      <text class="cu-list-header__count">鍏� {{ list.length }} 鍙拌澶�</text>
+    </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>
+            </view>
+            <text class="cu-list-card__sub">{{ item.roomInfo || '鏆傛棤鎴块棿淇℃伅' }}</text>
+          </view>
+        </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 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>
+          </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>
+        </view>
+
+        <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" />
+    </view>
+  </view>
+</template>
+
+<script>
+import { customerDevicePage } from '@/api'
+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,
+      page: 1
+    }
+  },
+  computed: {
+    typeLabel () { return this.typeOptions[this.typeIdx].label },
+    statusLabel () { return this.statusOptions[this.statusIdx].label }
+  },
+  onShow () { this.load() },
+  methods: {
+    load () {
+      customerDevicePage({
+        page: this.page,
+        capacity: 20,
+        model: {
+          deviceType: this.typeOptions[this.typeIdx].value,
+          statusFilter: this.statusOptions[this.statusIdx].value
+        }
+      }).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() },
+    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 })
+    }
+  }
+}
+</script>
diff --git a/h5/pages/customer/electricity/recharge.vue b/h5/pages/customer/electricity/recharge.vue
new file mode 100644
index 0000000..94bacbc
--- /dev/null
+++ b/h5/pages/customer/electricity/recharge.vue
@@ -0,0 +1,79 @@
+<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-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>
+    </view>
+
+    <view class="cu-pay-amount-box">
+      <view class="cu-pay-amount-box__label">鍏呭�奸噾棰�</view>
+      <view class="cu-pay-amount-box__input-wrap">
+        <text class="cu-pay-amount-box__symbol">楼</text>
+        <input v-model="amount" class="cu-pay-amount-box__input" type="digit" placeholder="0.00" />
+      </view>
+      <view class="cu-quick-amounts">
+        <text
+          v-for="q in quickAmounts"
+          :key="q"
+          :class="['cu-quick-amount', String(amount) === String(q) ? 'cu-quick-amount--active' : '']"
+          @click="amount = String(q)"
+        >{{ q }}鍏�</text>
+      </view>
+      <view class="cu-pay-amount-box__remark">
+        <text class="cu-pay-amount-box__remark-label">澶囨敞</text>
+        <input v-model="remark" placeholder="閫夊~" />
+      </view>
+    </view>
+
+    <view class="cu-page-footer">
+      <view class="cu-btn cu-btn--primary" @click="submit">纭鍏呭�納{ amount ? ' 楼' + amount : '' }}</view>
+    </view>
+  </view>
+</template>
+
+<script>
+import { customerDeviceDetail, customerPayCreate } from '@/api'
+import { invokeWxPay } from '@/utils/wxpay.js'
+export default {
+  data () {
+    return {
+      deviceId: null,
+      device: null,
+      amount: '',
+      remark: '',
+      quickAmounts: [50, 100, 200, 500]
+    }
+  },
+  onLoad (q) { this.deviceId = q.id; this.load() },
+  methods: {
+    load () {
+      customerDeviceDetail({ deviceType: 0, deviceId: this.deviceId }).then(res => { this.device = res.data })
+    },
+    submit () {
+      if (!this.amount) return uni.showToast({ title: '璇疯緭鍏ラ噾棰�', icon: 'none' })
+      customerPayCreate({
+        orderType: 0,
+        electricalId: Number(this.deviceId),
+        amount: Number(this.amount),
+        remark: this.remark,
+        openid: this.$store.state.openId
+      }).then(async res => {
+        try {
+          await invokeWxPay(res.data)
+          uni.redirectTo({ url: `/pages/customer/pay/result?success=1&orderNo=${res.data.orderNo}&type=recharge` })
+        } catch (e) {
+          uni.redirectTo({ url: `/pages/customer/pay/result?success=0&orderNo=${res.data.orderNo}&type=recharge` })
+        }
+      })
+    }
+  }
+}
+</script>
diff --git a/h5/pages/customer/index.vue b/h5/pages/customer/index.vue
new file mode 100644
index 0000000..78c5031
--- /dev/null
+++ b/h5/pages/customer/index.vue
@@ -0,0 +1,81 @@
+<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>
+      </view>
+    </view>
+
+    <view class="cu-home-body">
+      <view v-if="banners.length" class="cu-banner-wrap">
+        <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>
+        <view class="cu-service-item cu-service-item--contract" @click="go('/pages/customer/contract/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--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'
+export default {
+  data () {
+    return { banners: [], home: {} }
+  },
+  computed: {
+    greeting () {
+      const h = new Date().getHours()
+      if (h < 12) return '鏃╀笂濂�'
+      if (h < 18) return '涓嬪崍濂�'
+      return '鏅氫笂濂�'
+    },
+    customerInitial () {
+      const name = (this.home.customerName || '鍟�').trim()
+      return name.charAt(0)
+    }
+  },
+  onShow () {
+    customerBanners().then(res => {
+      this.banners = (res.data || []).map(b => ({ imageUrl: b.imageUrl, title: b.title }))
+    })
+    customerHome().then(res => { this.home = res.data || {} })
+  },
+  methods: {
+    go (url) { uni.navigateTo({ url }) },
+    onSwitchRole () { switchRole() },
+    logout () { goRoleSelect() }
+  }
+}
+</script>
diff --git a/h5/pages/customer/login.vue b/h5/pages/customer/login.vue
new file mode 100644
index 0000000..492b5b7
--- /dev/null
+++ b/h5/pages/customer/login.vue
@@ -0,0 +1,73 @@
+<template>
+  <view class="cu-login">
+    <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="11" type="number" 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 { customerLogin, customerGetUserInfo, customerWxAuthorize, customerSendLoginSms } from '@/api'
+import { devWechatMock } from '@/utils/config.js'
+import { runWechatOAuthFlow } from '@/utils/wechatAuth.js'
+import { requestLoginSmsCode } from '@/utils/loginSms.js'
+import { mapMutations } from 'vuex'
+
+export default {
+  data () {
+    return {
+      form: { phone: '', code: '' },
+      downTime: 0,
+      devMockTip: devWechatMock.enabled ? `寮�鍙戞ā寮忥細妯℃嫙 openid ${devWechatMock.openId}` : ''
+    }
+  },
+  onShow () {
+    uni.setStorageSync('userType', 1)
+    runWechatOAuthFlow({
+      authorizeApi: customerWxAuthorize,
+      onSuccess: (res) => {
+        if (res.data.openid) this.setOpenId(res.data.openid)
+        if (res.data.token) {
+          this.setToken(res.data.token)
+          this.setUserType(1)
+          customerGetUserInfo().then(r => this.setUserInfo(r.data))
+          uni.redirectTo({ url: '/pages/customer/index' })
+        }
+      }
+    })
+  },
+  methods: {
+    ...mapMutations(['setToken', 'setUserInfo', 'setOpenId', 'setUserType']),
+    onLogin () {
+      if (!this.form.phone || !this.form.code) {
+        return uni.showToast({ title: '璇峰~鍐欐墜鏈哄彿鍜岄獙璇佺爜', icon: 'none' })
+      }
+      customerLogin({ ...this.form, openid: this.$store.state.openId, userType: 1 }).then(res => {
+        if (res.code === 200) {
+          this.setToken(res.data)
+          this.setUserType(1)
+          customerGetUserInfo().then(r => this.setUserInfo(r.data))
+          uni.redirectTo({ url: '/pages/customer/index' })
+        }
+      })
+    },
+    sendSms () {
+      requestLoginSmsCode(this, this.form.phone, customerSendLoginSms)
+    }
+  }
+}
+</script>
diff --git a/h5/pages/customer/pay/result.vue b/h5/pages/customer/pay/result.vue
new file mode 100644
index 0000000..fe628e4
--- /dev/null
+++ b/h5/pages/customer/pay/result.vue
@@ -0,0 +1,34 @@
+<template>
+  <view class="cu-result">
+    <view :class="['cu-result__icon', success ? 'cu-result__icon--ok' : 'cu-result__icon--fail']">
+      {{ success ? '鉁�' : '鉁�' }}
+    </view>
+    <view class="cu-result__title">{{ success ? '鏀粯鎴愬姛' : '鏀粯澶辫触' }}</view>
+    <view class="cu-result__sub">
+      {{ success ? (type === 'bill' ? '璐﹀崟鏀粯鎴愬姛锛屾劅璋㈡偍鐨勭即璐�' : '鍏呭�兼垚鍔燂紝棰勮 10 鍒嗛挓鍐呭埌璐�') : '璇风◢鍚庨噸璇曪紝鎴栬仈绯荤鐞嗗憳澶勭悊' }}
+    </view>
+    <view class="cu-btn cu-btn--primary" @click="goRecord">{{ type === 'bill' ? '鏌ョ湅璐﹀崟鏄庣粏' : '鏌ョ湅鍏呭�艰褰�' }}</view>
+    <view class="cu-btn" @click="goHome">杩斿洖涓婚〉</view>
+  </view>
+</template>
+
+<script>
+export default {
+  data () { return { success: false, type: 'recharge', orderNo: '', billId: '' } },
+  onLoad (q) {
+    this.success = q.success === '1'
+    this.type = q.type || 'recharge'
+    this.orderNo = q.orderNo || ''
+    this.billId = q.billId || ''
+  },
+  methods: {
+    goRecord () {
+      const url = this.type === 'bill' && this.billId
+        ? `/pages/customer/bill/detail?id=${this.billId}`
+        : '/pages/customer/recharge/record'
+      uni.redirectTo({ url })
+    },
+    goHome () { uni.reLaunch({ url: '/pages/customer/index' }) }
+  }
+}
+</script>
diff --git a/h5/pages/customer/recharge/record.vue b/h5/pages/customer/recharge/record.vue
new file mode 100644
index 0000000..7b4d931
--- /dev/null
+++ b/h5/pages/customer/recharge/record.vue
@@ -0,0 +1,84 @@
+<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-list-header">
+      <text class="cu-list-header__count">鍏� {{ list.length }} 鏉¤褰�</text>
+    </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>
+            <text class="cu-list-card__sub" v-if="item.address">鎴峰彿 {{ item.address }}</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>
+          </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>
+        </view>
+      </view>
+      <u-empty v-if="!list.length" text="鏆傛棤璁板綍" margin-top="80" />
+    </view>
+  </view>
+</template>
+
+<script>
+import { customerRechargeRecordPage } from '@/api'
+export default {
+  data () {
+    return {
+      list: [],
+      month: '',
+      statusIdx: 0,
+      statusOptions: [
+        { label: '鍏ㄩ儴鐘舵��', value: null },
+        { label: '鍏呭�兼垚鍔�', value: 1 },
+        { label: '鍏呭�煎け璐�', value: 2 },
+        { label: '鍏呭�间腑', value: 0 }
+      ]
+    }
+  },
+  computed: { statusLabel () { return this.statusOptions[this.statusIdx].label } },
+  onShow () { this.load() },
+  methods: {
+    load () {
+      customerRechargeRecordPage({
+        page: 1,
+        capacity: 50,
+        model: { status: this.statusOptions[this.statusIdx].value, month: this.month || null }
+      }).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() },
+    statusClass (s) {
+      if (s === 1) return 'cu-status--ok'
+      if (s === 2) return 'cu-status--bad'
+      return 'cu-status--warn'
+    }
+  }
+}
+</script>
diff --git a/h5/pages/index.vue b/h5/pages/index.vue
index f64142b..ba479e2 100644
--- a/h5/pages/index.vue
+++ b/h5/pages/index.vue
@@ -17,19 +17,23 @@
 		<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 class="name">{{item.name}}</view>
 				<view v-if="item.name == '寰呭姙涓績' && taskNum" class="superscript">{{taskNum}}</view>
 			</view>
 		</view>
-		<view class="loginout" @click="loginOut">閫�鍑虹櫥闄�</view>
+		<view class="footer-actions">
+			<view class="switch-role" @click="switchRole">鍒囨崲瑙掕壊</view>
+			<view class="loginout" @click="loginOut">閫�鍑虹櫥褰�</view>
+		</view>
 	</view>
 </template>
 
 <script>
 	import {
-		logoutPost,
+		logoutPost,
 		myNoticesH5
 	} from '@/api'
+	import { switchRole as doSwitchRole, goRoleSelect } from '@/utils/roleSwitch.js'
 	export default {
 		data() {
 			return {
@@ -51,12 +55,12 @@
 						url: '/pages/operation/device',
 						img: require('@/static/home/ic_fangkebaobe@2x.png'),
 						auth: 'weixin:menu:visitcar'
-					},
-					{
-						name: '搴撳瓨鐩樼偣',
-						url: '/pages/inventory/index',
-						img: require('@/static/home/ic_pandian@2x.png'),
-						auth: 'weixin:menu:visitcar'
+					},
+					{
+						name: '搴撳瓨鐩樼偣',
+						url: '/pages/inventory/index',
+						img: require('@/static/home/ic_pandian@2x.png'),
+						auth: 'weixin:menu:visitcar'
 					},
 				],
 				list2: [{
@@ -70,20 +74,20 @@
 						url: '/pages/operation/record',
 						img: require('@/static/home/ic_wodehuiyi@2x.png'),
 						auth: 'weixin:menu:visitcar'
-					},
-					{
-						name: '寰呭姙涓績',
-						url: '/pages/workOrder/wait',
-						img: require('@/static/home/ic_daiban@2x.png'),
-						auth: 'weixin:menu:visitcar'
 					},
-				],
+					{
+						name: '寰呭姙涓績',
+						url: '/pages/workOrder/wait',
+						img: require('@/static/home/ic_daiban@2x.png'),
+						auth: 'weixin:menu:visitcar'
+					},
+				],
 				taskNum: 0
 			}
 		},
 		onShow() {
-			myNoticesH5({ page: 1, capacity: 1,model: {status: 0}}).then(res => {
-				this.taskNum = res.data.total
+			myNoticesH5({ page: 1, capacity: 1,model: {status: 0}}).then(res => {
+				this.taskNum = res.data.total
 			})
 		},
 		methods: {
@@ -92,16 +96,11 @@
 					url: item.url
 				})
 			},
+			switchRole () {
+				doSwitchRole(logoutPost)
+			},
 			loginOut() {
-				logoutPost().then(res => {
-					this.$store.commit('empty')
-					setTimeout(() => {
-						uni.redirectTo({
-							url: '/pages/login'
-						})
-					}, 300)
-				})
-				// window.location.href= 'https://zhcg.fnwtzx.com/fn_h5'
+				logoutPost().catch(() => {}).finally(() => goRoleSelect())
 			},
 
 		}
@@ -149,7 +148,7 @@
 				display: flex;
 				flex-direction: column;
 				align-items: center;
-				width: 25%;
+				width: 25%;
 				position: relative;
 				image {
 					width: 88rpx;
@@ -160,37 +159,53 @@
 				.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%;
-				}
+				.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%;
+				}
 			}
 		}
 
-		.loginout {
+		.footer-actions {
 			position: fixed;
 			bottom: 88rpx;
-			left: 50%;
-			transform: translate(-50%, 0);
-			width: 152rpx;
+			left: 0;
+			right: 0;
+			display: flex;
+			justify-content: center;
+			align-items: center;
+			gap: 24rpx;
+		}
+
+		.switch-role,
+		.loginout {
 			height: 60rpx;
+			padding: 0 32rpx;
 			border-radius: 30rpx;
-			border: 1rpx solid $primaryColor;
-			color: $primaryColor;
 			font-size: 26rpx;
 			display: flex;
 			justify-content: center;
 			align-items: center;
 		}
+
+		.switch-role {
+			border: 1rpx solid $primaryColor;
+			color: $primaryColor;
+		}
+
+		.loginout {
+			border: 1rpx solid #ccc;
+			color: #666;
+		}
 	}
 </style>
\ No newline at end of file
diff --git a/h5/pages/login.vue b/h5/pages/login.vue
index bfa42da..a7ce852 100644
--- a/h5/pages/login.vue
+++ b/h5/pages/login.vue
@@ -2,6 +2,7 @@
 	<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" />
@@ -33,6 +34,9 @@
 
 		getRecordByUserPoint
 	} from '@/api'
+	import { devWechatMock } from '@/utils/config.js'
+	import { runWechatOAuthFlow } from '@/utils/wechatAuth.js'
+	import { requestLoginSmsCode } from '@/utils/loginSms.js'
 	import {
 		mapState,
 		mapMutations
@@ -48,7 +52,8 @@
 				},
 				ywinfo: {},
 				downTime: 0,
-				code: ''
+				code: '',
+				devMockTip: devWechatMock.enabled ? `寮�鍙戞ā寮忥細妯℃嫙 openid ${devWechatMock.openId}` : ''
 			}
 		},
 		onLoad(option) {
@@ -67,65 +72,44 @@
 			}
 		},
 		onShow() {
-			// return
-			var that = this
-			let url = window.location.href
-			if (url.indexOf('code=') !== -1 || this.code) {
-				let code = ''
-				const query = url.split('?')
-				for (const q of query) {
-					if (q.indexOf('code=') !== -1) {
-						let statusIndex = q.indexOf('&state')
-						code = q.substring(q.indexOf('code=') + 5, statusIndex)
-					}
-				}
-				ywWxAuthorize({
-					code: code || this.code
-				}).then(res => {
-					if (res.code === 200) {
-						// console.log('res', res);
+			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(() => {
+					}
+					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/index"
+										url: "/pages/polling/point?id=" + res.data.id
 									})
-								}, 300)
-							}
+								} else {
+									uni.redirectTo({
+										url: "/pages/polling/empty?message=" + res.message
+									})
+								}
+							})
+						} else {
+							setTimeout(() => {
+								uni.redirectTo({
+									url: "/pages/index"
+								})
+							}, 300)
 						}
 					}
-				})
-			} else {
-				let url = 'https://zhcg.fnwtzx.com/fn_h5'
-				// const appID = 'wx95ac1efb67f0330d'
-								//let url = 'https://dmtest.ahapp.net/yunwei_h5'
-				const appID = 'wx15dfdae9a19177f3'
-				let uri = encodeURIComponent(url)
-				let authURL =
-					`https://open.weixin.qq.com/connect/oauth2/authorize?appid=${appID}&redirect_uri=${uri}&response_type=code&scope=snsapi_base#wechat_redirect`
-				window.location.href = authURL
-			}
-
+				}
+			})
 		},
 		methods: {
 			...mapMutations(["setToken", "setUserInfo"]),
@@ -182,20 +166,7 @@
 
 			},
 			sendSms() {
-				this.downTime = 60
-				let timer = setInterval(() => {
-					if (this.downTime == 0) return clearInterval(timer)
-					this.downTime = this.downTime - 1
-				}, 1000)
-				const {
-					form
-				} = this
-				sendSMsPost({
-					phone: form.phone,
-					type: 0
-				}).then(res => {
-					this.showToast('鐭俊鍙戦�佹垚鍔�')
-				})
+				requestLoginSmsCode(this, this.form.phone, sendSMsPost, { phone: this.form.phone, userType: 0 })
 			},
 		}
 	}
@@ -223,7 +194,17 @@
 
 		.login_title2 {
 			margin-top: 10rpx;
-			margin-bottom: 80rpx;
+			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 {
diff --git a/h5/pages/roleSelect.vue b/h5/pages/roleSelect.vue
new file mode 100644
index 0000000..68df7e0
--- /dev/null
+++ b/h5/pages/roleSelect.vue
@@ -0,0 +1,49 @@
+<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>
+    <view class="card merchant" @click="goMerchant">
+      <view class="name">鍟嗘埛</view>
+      <view class="desc">浜ょ數璐广�佹煡鍚堝悓銆佹煡璐﹀崟</view>
+    </view>
+  </view>
+</template>
+
+<script>
+export default {
+  onLoad (option) {
+    if (option && option.switch === '1') return
+    const userType = uni.getStorageSync('userType')
+    const token = uni.getStorageSync('token')
+    if (userType === 0 && token) {
+      uni.redirectTo({ url: '/pages/index' })
+    } else if (userType === 1 && token) {
+      uni.redirectTo({ url: '/pages/customer/index' })
+    }
+  },
+  methods: {
+    goOps () {
+      uni.setStorageSync('userType', 0)
+      uni.redirectTo({ url: '/pages/login' })
+    },
+    goMerchant () {
+      uni.setStorageSync('userType', 1)
+      uni.redirectTo({ url: '/pages/customer/login' })
+    }
+  }
+}
+</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/store/index.js b/h5/store/index.js
index a0a9693..4b7a6b3 100644
--- a/h5/store/index.js
+++ b/h5/store/index.js
@@ -19,14 +19,14 @@
 const store = new Vuex.Store({
 
 	state: {
-		// openId: openId || '061kuG0006hxcS13TT200w9VIp4kuG09',
-		openId: openId || '123123',
+		openId: openId || '',
 		member: member || null,
 		statusbarHeight: statusbarHeight || '0',
 		navHeight: navHeight || '0',
 		token: token || null,
 		time: time || null,
 		userInfo: userInfo || {},
+		userType: uni.getStorageSync('userType'),
 		driverInfo: driverInfo || {},
 		height: height || '0',
 		sessionKey: sessionKey || '',
@@ -68,6 +68,10 @@
 			state.userInfo = obj
 			uni.setStorageSync('userInfo', obj)
 		},
+		setUserType(state, val) {
+			state.userType = val
+			uni.setStorageSync('userType', val)
+		},
 		// 璁剧疆鍙告満淇℃伅
 		setDriverInfo(state, obj) {
 			state.driverInfo = obj
@@ -83,9 +87,11 @@
 			state.token = ''
 			state.userInfo = {}
 			state.driverInfo = {}
+			state.userType = null
 			uni.removeStorageSync('userInfo')
 			uni.removeStorageSync('driverInfo')
 			uni.removeStorageSync('token')
+			uni.removeStorageSync('userType')
 		}
 	},
 	actions: {
diff --git a/h5/styles/customer.scss b/h5/styles/customer.scss
new file mode 100644
index 0000000..4ab120a
--- /dev/null
+++ b/h5/styles/customer.scss
@@ -0,0 +1,2024 @@
+/* 鍟嗘埛绔� H5 缁熶竴 UI */
+
+$cu-primary: #2080f7;
+$cu-primary-dark: #1659ac;
+$cu-primary-light: #e8f2ff;
+$cu-success: #19be6b;
+$cu-warning: #ff9900;
+$cu-danger: #fa3534;
+$cu-text: #1a1a2e;
+$cu-text-secondary: #5c6370;
+$cu-text-muted: #9aa3b2;
+$cu-bg: #f4f6fb;
+$cu-card-bg: #ffffff;
+$cu-radius: 24rpx;
+$cu-radius-sm: 16rpx;
+$cu-shadow: 0 8rpx 32rpx rgba(15, 35, 95, 0.06);
+
+.cu-page {
+  min-height: 100vh;
+  background: $cu-bg;
+  box-sizing: border-box;
+}
+
+.cu-page--padded {
+  padding: 24rpx;
+}
+
+.cu-page--white {
+  background: #fff;
+}
+
+/* 棣栭〉椤堕儴 */
+.cu-hero {
+  padding: 32rpx 32rpx 48rpx;
+  background: linear-gradient(145deg, #2080f7 0%, #4a9bff 55%, #6eb3ff 100%);
+  border-radius: 0 0 40rpx 40rpx;
+}
+
+.cu-hero__greet {
+  display: flex;
+  align-items: center;
+}
+
+.cu-avatar {
+  width: 88rpx;
+  height: 88rpx;
+  margin-right: 24rpx;
+  border-radius: 50%;
+  background: rgba(255, 255, 255, 0.25);
+  border: 2rpx solid rgba(255, 255, 255, 0.5);
+  color: #fff;
+  font-size: 36rpx;
+  font-weight: 600;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  flex-shrink: 0;
+}
+
+.cu-hero__hi {
+  font-size: 26rpx;
+  color: rgba(255, 255, 255, 0.85);
+  margin-bottom: 6rpx;
+}
+
+.cu-hero__name {
+  font-size: 36rpx;
+  font-weight: 600;
+  color: #fff;
+}
+
+.cu-home-body {
+  margin-top: -28rpx;
+  padding: 0 24rpx 48rpx;
+}
+
+.cu-banner-wrap {
+  margin-bottom: 28rpx;
+  border-radius: $cu-radius;
+  overflow: hidden;
+  box-shadow: $cu-shadow;
+}
+
+.cu-section-title {
+  font-size: 32rpx;
+  font-weight: 600;
+  color: $cu-text;
+  margin-bottom: 24rpx;
+  padding-left: 8rpx;
+}
+
+.cu-service-grid {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 20rpx;
+}
+
+.cu-service-item {
+  width: calc(50% - 10rpx);
+  background: $cu-card-bg;
+  border-radius: $cu-radius;
+  padding: 32rpx 24rpx;
+  box-shadow: $cu-shadow;
+  display: flex;
+  flex-direction: column;
+  align-items: flex-start;
+}
+
+.cu-service-item__icon {
+  width: 72rpx;
+  height: 72rpx;
+  border-radius: 20rpx;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  font-size: 36rpx;
+  margin-bottom: 16rpx;
+}
+
+.cu-service-item__label {
+  font-size: 30rpx;
+  font-weight: 600;
+  color: $cu-text;
+}
+
+.cu-service-item__desc {
+  font-size: 22rpx;
+  color: $cu-text-muted;
+  margin-top: 6rpx;
+}
+
+.cu-service-item--electric .cu-service-item__icon { background: #fff7e6; }
+.cu-service-item--contract .cu-service-item__icon { background: #e8f7ef; }
+.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-footer-btn {
+  font-size: 28rpx;
+  padding: 18rpx 48rpx;
+  border-radius: 999rpx;
+  background: #fff;
+  color: $cu-text-secondary;
+  box-shadow: $cu-shadow;
+}
+
+.cu-footer-btn--primary {
+  color: $cu-primary;
+  border: 1rpx solid rgba(32, 128, 247, 0.35);
+}
+
+/* 鍗$墖 */
+.cu-card {
+  background: $cu-card-bg;
+  border-radius: $cu-radius;
+  padding: 28rpx;
+  margin-bottom: 20rpx;
+  box-shadow: $cu-shadow;
+}
+
+.cu-card--clickable:active {
+  opacity: 0.92;
+  transform: scale(0.995);
+}
+
+.cu-card__title {
+  font-size: 30rpx;
+  font-weight: 600;
+  color: $cu-text;
+  margin-bottom: 20rpx;
+  padding-bottom: 16rpx;
+  border-bottom: 1rpx solid #f0f2f5;
+}
+
+.cu-row {
+  display: flex;
+  align-items: center;
+}
+
+.cu-row--between {
+  justify-content: space-between;
+}
+
+.cu-name {
+  font-size: 30rpx;
+  font-weight: 600;
+  color: $cu-text;
+}
+
+.cu-line {
+  font-size: 26rpx;
+  color: $cu-text-secondary;
+  margin-top: 12rpx;
+  line-height: 1.5;
+}
+
+.cu-line__label {
+  color: $cu-text-muted;
+}
+
+.cu-card__footer {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-top: 20rpx;
+  padding-top: 16rpx;
+  border-top: 1rpx solid #f5f6f8;
+}
+
+.cu-time {
+  font-size: 22rpx;
+  color: $cu-text-muted;
+}
+
+.cu-link {
+  font-size: 28rpx;
+  color: $cu-primary;
+  font-weight: 500;
+}
+
+/* 鐘舵�� */
+.cu-status {
+  font-size: 24rpx;
+  padding: 4rpx 16rpx;
+  border-radius: 999rpx;
+  font-weight: 500;
+}
+
+.cu-status--ok {
+  color: $cu-success;
+  background: rgba(25, 190, 107, 0.12);
+}
+
+.cu-status--bad {
+  color: $cu-danger;
+  background: rgba(250, 53, 52, 0.1);
+}
+
+.cu-status--warn {
+  color: $cu-warning;
+  background: rgba(255, 153, 0, 0.12);
+}
+
+.cu-status--muted {
+  color: $cu-text-muted;
+  background: #f0f2f5;
+}
+
+.cu-text-danger {
+  color: $cu-danger !important;
+  font-weight: 600;
+}
+
+/* 鏍囩 */
+.cu-tag {
+  display: inline-block;
+  background: rgba(250, 53, 52, 0.08);
+  color: $cu-danger;
+  font-size: 22rpx;
+  padding: 6rpx 14rpx;
+  border-radius: 8rpx;
+  margin-right: 8rpx;
+  margin-bottom: 8rpx;
+}
+
+/* 绛涢�� */
+.cu-filters {
+  display: flex;
+  gap: 16rpx;
+  margin-bottom: 20rpx;
+  padding: 0 24rpx;
+  padding-top: 24rpx;
+}
+
+.cu-filter {
+  background: #fff;
+  padding: 16rpx 28rpx;
+  border-radius: 999rpx;
+  font-size: 26rpx;
+  color: $cu-text-secondary;
+  box-shadow: $cu-shadow;
+}
+
+/* Tab */
+.cu-tabs {
+  display: flex;
+  padding: 24rpx;
+  gap: 16rpx;
+  background: $cu-bg;
+}
+
+.cu-tabs--scroll {
+  white-space: nowrap;
+  flex-wrap: nowrap;
+}
+
+scroll-view.cu-tabs {
+  width: 100%;
+  box-sizing: border-box;
+}
+
+scroll-view.cu-tabs .cu-tab {
+  display: inline-block;
+  vertical-align: middle;
+  margin-right: 16rpx;
+}
+
+scroll-view.cu-tabs .cu-tab:last-child {
+  margin-right: 24rpx;
+}
+
+.cu-tab {
+  flex-shrink: 0;
+  padding: 14rpx 28rpx;
+  font-size: 26rpx;
+  color: $cu-text-secondary;
+  background: #fff;
+  border-radius: 999rpx;
+  box-shadow: $cu-shadow;
+}
+
+.cu-tab--active {
+  color: #fff;
+  background: linear-gradient(135deg, $cu-primary, #4a9bff);
+  box-shadow: 0 6rpx 20rpx rgba(32, 128, 247, 0.28);
+}
+
+.cu-list-wrap {
+  padding: 0 24rpx 24rpx;
+}
+
+/* 琛ㄥ崟 */
+.cu-form-line {
+  display: flex;
+  align-items: center;
+  margin: 20rpx 0;
+  font-size: 28rpx;
+  color: $cu-text;
+}
+
+.cu-form-line__label {
+  width: 160rpx;
+  flex-shrink: 0;
+  color: $cu-text-secondary;
+}
+
+.cu-form-line input {
+  flex: 1;
+  height: 72rpx;
+  padding: 0 20rpx;
+  background: #f8fafc;
+  border-radius: 12rpx;
+  font-size: 28rpx;
+}
+
+.cu-form-suffix {
+  margin-left: 12rpx;
+  color: $cu-text-muted;
+  font-size: 26rpx;
+}
+
+/* 鎸夐挳 */
+.cu-btn {
+  height: 96rpx;
+  line-height: 96rpx;
+  text-align: center;
+  border-radius: 999rpx;
+  font-size: 32rpx;
+  font-weight: 500;
+  margin-top: 40rpx;
+  background: #fff;
+  color: $cu-text-secondary;
+  border: 1rpx solid #e8ecf0;
+}
+
+.cu-btn--primary {
+  background: linear-gradient(135deg, $cu-primary 0%, #4a9bff 100%);
+  color: #fff;
+  border: none;
+  box-shadow: 0 12rpx 32rpx rgba(32, 128, 247, 0.35);
+}
+
+.cu-btn--block {
+  margin-left: 24rpx;
+  margin-right: 24rpx;
+}
+
+.cu-btn:active {
+  opacity: 0.9;
+}
+
+/* 鐧诲綍椤� */
+.cu-login {
+  min-height: 100vh;
+  padding: 120rpx 48rpx 48rpx;
+  background: linear-gradient(180deg, #dce9ff 0%, #f4f6fb 45%, #fff 100%);
+}
+
+.cu-login__brand {
+  margin-bottom: 64rpx;
+}
+
+.cu-login__title {
+  font-size: 52rpx;
+  font-weight: 700;
+  color: $cu-text;
+  margin-bottom: 12rpx;
+}
+
+.cu-login__sub {
+  font-size: 28rpx;
+  color: $cu-text-muted;
+}
+
+.cu-login__tip {
+  font-size: 24rpx;
+  color: $cu-warning;
+  margin-bottom: 32rpx;
+  line-height: 1.5;
+  padding: 16rpx 20rpx;
+  background: rgba(255, 153, 0, 0.1);
+  border-radius: 12rpx;
+}
+
+.cu-input-wrap {
+  display: flex;
+  align-items: center;
+  background: #fff;
+  border-radius: 999rpx;
+  padding: 0 32rpx;
+  height: 100rpx;
+  margin-bottom: 24rpx;
+  box-shadow: $cu-shadow;
+}
+
+.cu-input-wrap input {
+  flex: 1;
+  font-size: 30rpx;
+}
+
+.cu-sms-btn {
+  color: $cu-primary;
+  font-size: 28rpx;
+  font-weight: 500;
+  flex-shrink: 0;
+  padding-left: 20rpx;
+}
+
+.cu-sms-btn--disabled {
+  color: $cu-text-muted;
+}
+
+/* 鏀粯缁撴灉 */
+.cu-result {
+  min-height: 100vh;
+  padding: 160rpx 48rpx;
+  text-align: center;
+  background: linear-gradient(180deg, #f4f6fb 0%, #fff 40%);
+}
+
+.cu-result__icon {
+  width: 140rpx;
+  height: 140rpx;
+  line-height: 140rpx;
+  border-radius: 50%;
+  margin: 0 auto 40rpx;
+  color: #fff;
+  font-size: 72rpx;
+  font-weight: 600;
+}
+
+.cu-result__icon--ok {
+  background: linear-gradient(145deg, #19be6b, #3dd68c);
+  box-shadow: 0 16rpx 40rpx rgba(25, 190, 107, 0.35);
+}
+
+.cu-result__icon--fail {
+  background: linear-gradient(145deg, #fa3534, #ff6b6b);
+  box-shadow: 0 16rpx 40rpx rgba(250, 53, 52, 0.3);
+}
+
+.cu-result__title {
+  font-size: 44rpx;
+  font-weight: 700;
+  color: $cu-text;
+}
+
+.cu-result__sub {
+  margin: 20rpx 0 80rpx;
+  color: $cu-text-muted;
+  font-size: 28rpx;
+  line-height: 1.6;
+}
+
+/* ========== 鍒楄〃椤靛寮� ========== */
+.cu-list-header {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  padding: 8rpx 24rpx 16rpx;
+}
+
+.cu-list-header__count {
+  font-size: 24rpx;
+  color: $cu-text-muted;
+}
+
+.cu-list-card {
+  background: $cu-card-bg;
+  border-radius: $cu-radius;
+  padding: 0;
+  margin-bottom: 20rpx;
+  box-shadow: $cu-shadow;
+  overflow: hidden;
+}
+
+.cu-list-card--clickable:active {
+  opacity: 0.95;
+}
+
+.cu-list-card--inset {
+  margin: 20rpx 24rpx 0;
+}
+
+
+.cu-list-card__head {
+  display: flex;
+  align-items: flex-start;
+  padding: 28rpx 28rpx 20rpx;
+}
+
+.cu-list-card__icon {
+  width: 80rpx;
+  height: 80rpx;
+  border-radius: 20rpx;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  font-size: 38rpx;
+  margin-right: 20rpx;
+  flex-shrink: 0;
+}
+
+.cu-list-card__icon--electric { background: linear-gradient(135deg, #fff7e6, #ffe8b3); }
+.cu-list-card__icon--conditioner { background: linear-gradient(135deg, #e8f7ef, #c8f0dc); }
+.cu-list-card__icon--contract { background: linear-gradient(135deg, #eef2ff, #d6e4ff); }
+.cu-list-card__icon--bill { background: linear-gradient(135deg, #fce8f3, #f5d0e8); }
+.cu-list-card__icon--record { background: linear-gradient(135deg, #e8f4ff, #cce5ff); }
+
+.cu-list-card__main {
+  flex: 1;
+  min-width: 0;
+}
+
+.cu-list-card__title-row {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  gap: 12rpx;
+  margin-bottom: 8rpx;
+}
+
+.cu-list-card__title {
+  font-size: 30rpx;
+  font-weight: 600;
+  color: $cu-text;
+  flex: 1;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+
+.cu-list-card__sub {
+  display: block;
+  font-size: 24rpx;
+  color: $cu-text-muted;
+  line-height: 1.4;
+}
+
+.cu-list-card__meta {
+  display: flex;
+  flex-direction: column;
+  gap: 8rpx;
+  margin-top: 8rpx;
+}
+
+.cu-list-card__tags {
+  padding: 0 28rpx 12rpx;
+}
+
+.cu-info-grid {
+  display: flex;
+  flex-wrap: wrap;
+  margin: 0 20rpx 20rpx;
+  background: #f8fafc;
+  border-radius: 16rpx;
+  overflow: hidden;
+}
+
+.cu-info-cell {
+  width: 50%;
+  padding: 20rpx 24rpx;
+  box-sizing: border-box;
+  border-bottom: 1rpx solid #f0f2f5;
+}
+
+.cu-info-cell:nth-child(odd) {
+  border-right: 1rpx solid #f0f2f5;
+}
+
+.cu-info-cell:nth-last-child(-n+2) {
+  border-bottom: none;
+}
+
+.cu-info-cell--full {
+  width: 100%;
+  border-right: none !important;
+}
+
+.cu-info-cell__label {
+  display: block;
+  font-size: 22rpx;
+  color: $cu-text-muted;
+  margin-bottom: 8rpx;
+}
+
+.cu-info-cell__value {
+  display: block;
+  font-size: 28rpx;
+  font-weight: 600;
+  color: $cu-text;
+}
+
+.cu-info-cell__value--danger {
+  color: $cu-danger;
+}
+
+.cu-info-cell__value--primary {
+  color: $cu-primary;
+}
+
+.cu-list-card__foot {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  padding: 16rpx 28rpx 24rpx;
+  border-top: 1rpx solid #f5f6f8;
+}
+
+.cu-list-card__arrow {
+  font-size: 26rpx;
+  color: $cu-primary;
+  font-weight: 500;
+}
+
+.cu-period-chip {
+  display: inline-flex;
+  align-items: center;
+  margin-top: 12rpx;
+  padding: 8rpx 16rpx;
+  background: $cu-primary-light;
+  border-radius: 8rpx;
+  font-size: 22rpx;
+  color: $cu-primary;
+}
+
+/* ========== 璇︽儏椤靛寮� ========== */
+.cu-page--with-footer {
+  padding-bottom: 160rpx;
+}
+
+.cu-detail-hero {
+  margin: 24rpx 24rpx 0;
+  padding: 36rpx 32rpx;
+  border-radius: $cu-radius;
+  background: linear-gradient(145deg, #2080f7 0%, #4a9bff 100%);
+  color: #fff;
+  box-shadow: 0 12rpx 40rpx rgba(32, 128, 247, 0.3);
+}
+
+.cu-detail-hero--warm {
+  background: linear-gradient(145deg, #ff8f3d 0%, #ffb347 100%);
+  box-shadow: 0 12rpx 40rpx rgba(255, 143, 61, 0.28);
+}
+
+.cu-detail-hero--green {
+  background: linear-gradient(145deg, #19be6b 0%, #3dd68c 100%);
+  box-shadow: 0 12rpx 40rpx rgba(25, 190, 107, 0.28);
+}
+
+.cu-detail-hero__top {
+  display: flex;
+  align-items: flex-start;
+  justify-content: space-between;
+  margin-bottom: 24rpx;
+}
+
+.cu-detail-hero__label {
+  font-size: 24rpx;
+  opacity: 0.85;
+  margin-bottom: 8rpx;
+}
+
+.cu-detail-hero__code {
+  font-size: 32rpx;
+  font-weight: 600;
+  letter-spacing: 0.5rpx;
+}
+
+.cu-detail-hero__amount {
+  margin-top: 8rpx;
+}
+
+.cu-detail-hero__amount-label {
+  display: block;
+  font-size: 24rpx;
+  opacity: 0.85;
+  margin-bottom: 8rpx;
+}
+
+.cu-detail-hero__amount-value {
+  font-size: 56rpx;
+  font-weight: 700;
+  line-height: 1.2;
+}
+
+.cu-detail-hero__amount-unit {
+  font-size: 28rpx;
+  font-weight: 500;
+  margin-left: 4rpx;
+}
+
+.cu-detail-hero .cu-status {
+  background: rgba(255, 255, 255, 0.22);
+  color: #fff;
+  flex-shrink: 0;
+}
+
+.cu-segment {
+  display: flex;
+  margin: 24rpx 24rpx 0;
+  padding: 6rpx;
+  background: #e8ecf2;
+  border-radius: 16rpx;
+}
+
+.cu-segment__item {
+  flex: 1;
+  text-align: center;
+  padding: 18rpx 0;
+  font-size: 28rpx;
+  color: $cu-text-secondary;
+  border-radius: 12rpx;
+  transition: all 0.2s;
+}
+
+.cu-segment__item--active {
+  background: #fff;
+  color: $cu-primary;
+  font-weight: 600;
+  box-shadow: 0 4rpx 12rpx rgba(15, 35, 95, 0.08);
+}
+
+.cu-panel {
+  margin: 20rpx 24rpx 0;
+  background: $cu-card-bg;
+  border-radius: $cu-radius;
+  box-shadow: $cu-shadow;
+  overflow: hidden;
+}
+
+.cu-panel__title {
+  padding: 24rpx 28rpx 16rpx;
+  font-size: 28rpx;
+  font-weight: 600;
+  color: $cu-text;
+}
+
+.cu-kv__item {
+  display: flex;
+  align-items: flex-start;
+  justify-content: space-between;
+  padding: 22rpx 28rpx;
+  border-top: 1rpx solid #f5f6f8;
+  gap: 24rpx;
+}
+
+.cu-kv__label {
+  flex-shrink: 0;
+  font-size: 26rpx;
+  color: $cu-text-muted;
+  min-width: 160rpx;
+}
+
+.cu-kv__value {
+  flex: 1;
+  text-align: right;
+  font-size: 28rpx;
+  color: $cu-text;
+  font-weight: 500;
+  word-break: break-all;
+}
+
+.cu-kv__value--danger {
+  color: $cu-danger;
+  font-weight: 600;
+}
+
+.cu-kv__value--muted {
+  color: $cu-text-muted;
+  font-weight: 400;
+  text-align: left;
+}
+
+.cu-bill-tip {
+  display: flex;
+  align-items: flex-start;
+  gap: 12rpx;
+  margin: 0 28rpx 20rpx;
+  padding: 20rpx 24rpx;
+  border-radius: 12rpx;
+  font-size: 24rpx;
+  line-height: 1.5;
+}
+
+.cu-bill-tip--inset {
+  margin: 0 24rpx 20rpx;
+}
+
+.cu-list-card .cu-bill-tip {
+  margin: 0 28rpx 16rpx;
+}
+
+.cu-bill-tip--ok {
+  background: #edf9f0;
+  color: #2f9a4f;
+}
+
+.cu-bill-tip--warn {
+  background: #fff8e8;
+  color: #d48806;
+}
+
+.cu-bill-tip--danger {
+  background: #fff1f0;
+  color: #cf1322;
+}
+
+.cu-bill-tip__icon {
+  flex-shrink: 0;
+  font-size: 28rpx;
+  line-height: 1.4;
+}
+
+.cu-bill-tip__text {
+  flex: 1;
+}
+
+.cu-room-list {
+  padding: 0 28rpx 20rpx;
+}
+
+.cu-room-item {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  gap: 20rpx;
+  padding: 18rpx 0;
+  border-top: 1rpx solid #f5f6f8;
+}
+
+.cu-room-item__name {
+  flex: 1;
+  font-size: 26rpx;
+  color: $cu-text;
+  word-break: break-all;
+}
+
+.cu-room-item__area {
+  flex-shrink: 0;
+  font-size: 26rpx;
+  color: $cu-primary;
+  font-weight: 500;
+}
+
+.cu-file-item {
+  display: flex;
+  align-items: center;
+  gap: 16rpx;
+  padding: 22rpx 28rpx;
+  border-top: 1rpx solid #f5f6f8;
+}
+
+.cu-file-item__icon {
+  font-size: 32rpx;
+}
+
+.cu-file-item__main {
+  flex: 1;
+  min-width: 0;
+}
+
+.cu-file-item__name {
+  display: block;
+  font-size: 28rpx;
+  color: $cu-text;
+  word-break: break-all;
+}
+
+.cu-file-item__time {
+  display: block;
+  margin-top: 6rpx;
+  font-size: 22rpx;
+  color: $cu-text-muted;
+}
+
+.cu-file-item__action {
+  flex-shrink: 0;
+  font-size: 26rpx;
+  color: $cu-primary;
+}
+
+.cu-timeline {
+  padding: 8rpx 28rpx 20rpx;
+}
+
+.cu-timeline__item {
+  display: flex;
+  align-items: flex-start;
+  padding: 20rpx 0;
+  border-bottom: 1rpx solid #f5f6f8;
+}
+
+.cu-timeline__item:last-child {
+  border-bottom: none;
+}
+
+.cu-timeline__dot {
+  width: 16rpx;
+  height: 16rpx;
+  border-radius: 50%;
+  background: $cu-primary;
+  margin-top: 10rpx;
+  margin-right: 20rpx;
+  flex-shrink: 0;
+  box-shadow: 0 0 0 6rpx rgba(32, 128, 247, 0.15);
+}
+
+.cu-timeline__body {
+  flex: 1;
+}
+
+.cu-timeline__title {
+  font-size: 28rpx;
+  font-weight: 600;
+  color: $cu-text;
+  margin-bottom: 6rpx;
+}
+
+.cu-timeline__time {
+  font-size: 24rpx;
+  color: $cu-text-muted;
+}
+
+.cu-page-footer {
+  position: fixed;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  padding: 20rpx 24rpx calc(20rpx + env(safe-area-inset-bottom));
+  background: rgba(255, 255, 255, 0.96);
+  backdrop-filter: blur(12px);
+  box-shadow: 0 -8rpx 32rpx rgba(15, 35, 95, 0.08);
+  z-index: 100;
+}
+
+.cu-page-footer .cu-btn {
+  margin-top: 0;
+}
+
+/* 鍏呭��/缂磋垂椤� */
+.cu-device-summary {
+  margin: 24rpx 24rpx 0;
+  padding: 28rpx;
+  background: $cu-card-bg;
+  border-radius: $cu-radius;
+  box-shadow: $cu-shadow;
+}
+
+.cu-device-summary__balance {
+  margin-top: 20rpx;
+  padding: 24rpx;
+  background: linear-gradient(135deg, #fff5f5, #fff);
+  border-radius: 16rpx;
+  border: 1rpx solid rgba(250, 53, 52, 0.12);
+  text-align: center;
+}
+
+.cu-device-summary__balance-label {
+  font-size: 24rpx;
+  color: $cu-text-muted;
+  margin-bottom: 8rpx;
+}
+
+.cu-device-summary__balance-value {
+  font-size: 48rpx;
+  font-weight: 700;
+  color: $cu-danger;
+}
+
+.cu-pay-amount-box {
+  margin: 24rpx 24rpx 0;
+  padding: 40rpx 28rpx;
+  background: $cu-card-bg;
+  border-radius: $cu-radius;
+  box-shadow: $cu-shadow;
+  text-align: center;
+}
+
+.cu-pay-amount-box__label {
+  font-size: 26rpx;
+  color: $cu-text-muted;
+  margin-bottom: 16rpx;
+}
+
+.cu-pay-amount-box__input-wrap {
+  display: flex;
+  align-items: baseline;
+  justify-content: center;
+  margin-bottom: 24rpx;
+}
+
+.cu-pay-amount-box__symbol {
+  font-size: 40rpx;
+  font-weight: 600;
+  color: $cu-text;
+  margin-right: 8rpx;
+}
+
+.cu-pay-amount-box__input {
+  font-size: 72rpx;
+  font-weight: 700;
+  color: $cu-text;
+  text-align: center;
+  min-width: 200rpx;
+  height: 88rpx;
+  line-height: 88rpx;
+}
+
+.cu-pay-amount-box__remark {
+  display: flex;
+  align-items: center;
+  margin-top: 8rpx;
+  padding: 20rpx 24rpx;
+  background: #f8fafc;
+  border-radius: 12rpx;
+  font-size: 26rpx;
+}
+
+.cu-pay-amount-box__remark-label {
+  color: $cu-text-muted;
+  margin-right: 16rpx;
+  flex-shrink: 0;
+}
+
+.cu-pay-amount-box__remark input {
+  flex: 1;
+  font-size: 26rpx;
+  text-align: left;
+}
+
+.cu-quick-amounts {
+  display: flex;
+  gap: 16rpx;
+  margin-top: 20rpx;
+  margin-bottom: 32rpx;
+  flex-wrap: wrap;
+  justify-content: center;
+}
+
+.cu-quick-amount {
+  padding: 12rpx 28rpx;
+  background: #f0f4fa;
+  border-radius: 999rpx;
+  font-size: 26rpx;
+  color: $cu-text-secondary;
+}
+
+.cu-quick-amount--active {
+  background: $cu-primary-light;
+  color: $cu-primary;
+  font-weight: 600;
+}
+
+/* ========== 璐﹀崟鍒楄〃 / 璇︽儏 ========== */
+.cu-bill-page__header {
+  padding: 16rpx 24rpx 8rpx;
+}
+
+.cu-bill-tabs {
+  margin-bottom: 20rpx;
+}
+
+.cu-bill-summary {
+  display: flex;
+  align-items: baseline;
+  gap: 8rpx;
+  padding: 0 8rpx 12rpx;
+}
+
+.cu-bill-summary__count {
+  font-size: 44rpx;
+  font-weight: 700;
+  color: $cu-text;
+  line-height: 1;
+}
+
+.cu-bill-summary__label {
+  font-size: 26rpx;
+  color: $cu-text-muted;
+}
+
+.cu-bill-card {
+  position: relative;
+  margin: 0 24rpx 20rpx;
+  background: $cu-card-bg;
+  border-radius: $cu-radius;
+  box-shadow: $cu-shadow;
+  overflow: hidden;
+}
+
+.cu-bill-card__accent {
+  position: absolute;
+  left: 0;
+  top: 0;
+  bottom: 0;
+  width: 8rpx;
+  background: $cu-primary;
+}
+
+.cu-bill-card__accent--ok {
+  background: $cu-success;
+}
+
+.cu-bill-card__accent--warn {
+  background: $cu-warning;
+}
+
+.cu-bill-card__accent--danger {
+  background: $cu-danger;
+}
+
+.cu-bill-card__body {
+  padding: 28rpx 28rpx 24rpx 32rpx;
+}
+
+.cu-bill-card__head {
+  display: flex;
+  align-items: flex-start;
+  justify-content: space-between;
+  gap: 16rpx;
+  margin-bottom: 24rpx;
+}
+
+.cu-bill-card__head-main {
+  flex: 1;
+  min-width: 0;
+}
+
+.cu-bill-card__type {
+  display: inline-flex;
+  padding: 6rpx 14rpx;
+  background: #f3f6fc;
+  border-radius: 8rpx;
+  font-size: 22rpx;
+  color: $cu-primary;
+  font-weight: 500;
+  margin-bottom: 10rpx;
+}
+
+.cu-bill-card__code {
+  display: block;
+  font-size: 30rpx;
+  font-weight: 600;
+  color: $cu-text;
+  word-break: break-all;
+  line-height: 1.4;
+}
+
+.cu-bill-card__amount-box {
+  display: flex;
+  align-items: flex-end;
+  justify-content: space-between;
+  gap: 20rpx;
+  padding: 24rpx;
+  background: linear-gradient(135deg, #f8fbff 0%, #f4f7fd 100%);
+  border-radius: 16rpx;
+  border: 1rpx solid rgba(32, 128, 247, 0.08);
+  margin-bottom: 20rpx;
+}
+
+.cu-bill-card__amount-label,
+.cu-bill-card__amount-side-label {
+  display: block;
+  font-size: 22rpx;
+  color: $cu-text-muted;
+  margin-bottom: 8rpx;
+}
+
+.cu-bill-card__amount-value {
+  font-size: 40rpx;
+  font-weight: 700;
+  color: $cu-danger;
+  line-height: 1.1;
+}
+
+.cu-bill-card__amount-side {
+  text-align: right;
+  flex-shrink: 0;
+}
+
+.cu-bill-card__amount-side-value {
+  font-size: 28rpx;
+  font-weight: 600;
+  color: $cu-text-secondary;
+}
+
+.cu-bill-card__overdue {
+  margin: -8rpx 0 16rpx;
+  padding: 12rpx 16rpx;
+  background: #fff1f0;
+  border-radius: 10rpx;
+  font-size: 24rpx;
+  color: $cu-danger;
+}
+
+.cu-bill-card__contract {
+  padding: 20rpx;
+  background: #fafbfc;
+  border-radius: 14rpx;
+  margin-bottom: 16rpx;
+}
+
+.cu-bill-card__contract-row {
+  display: flex;
+  align-items: flex-start;
+  gap: 16rpx;
+  font-size: 24rpx;
+  line-height: 1.5;
+}
+
+.cu-bill-card__contract-row + .cu-bill-card__contract-row {
+  margin-top: 12rpx;
+}
+
+.cu-bill-card__contract-label {
+  flex-shrink: 0;
+  width: 140rpx;
+  color: $cu-text-muted;
+}
+
+.cu-bill-card__contract-value {
+  flex: 1;
+  color: $cu-text-secondary;
+  word-break: break-all;
+}
+
+.cu-bill-card__meta {
+  display: flex;
+  gap: 16rpx;
+  margin-bottom: 20rpx;
+}
+
+.cu-bill-card__meta-item {
+  flex: 1;
+  padding: 16rpx;
+  background: #fff;
+  border: 1rpx solid #eef1f6;
+  border-radius: 12rpx;
+}
+
+.cu-bill-card__meta-item--full {
+  flex: none;
+  width: 100%;
+}
+
+.cu-bill-card__meta-label {
+  display: block;
+  font-size: 22rpx;
+  color: $cu-text-muted;
+  margin-bottom: 8rpx;
+}
+
+.cu-bill-card__meta-value {
+  display: block;
+  font-size: 24rpx;
+  color: $cu-text;
+  line-height: 1.4;
+  word-break: break-all;
+}
+
+.cu-bill-card__foot {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  padding-top: 18rpx;
+  border-top: 1rpx solid #f0f2f6;
+}
+
+.cu-bill-card__foot-hint {
+  font-size: 24rpx;
+  color: $cu-text-muted;
+}
+
+.cu-bill-card__foot-link {
+  font-size: 26rpx;
+  color: $cu-primary;
+  font-weight: 500;
+}
+
+.cu-bill-card--inset {
+  margin: 0 24rpx 24rpx;
+}
+
+.cu-contract-bill-panel {
+  padding: 0 24rpx 32rpx;
+}
+
+.cu-contract-bill-switch {
+  display: flex;
+  gap: 16rpx;
+  margin-bottom: 20rpx;
+}
+
+.cu-contract-bill-switch__item {
+  flex: 1;
+  display: flex;
+  align-items: center;
+  gap: 16rpx;
+  padding: 24rpx 20rpx;
+  border-radius: 16rpx;
+  background: #f5f7fb;
+  border: 2rpx solid transparent;
+  transition: all 0.2s ease;
+}
+
+.cu-contract-bill-switch__item--pay.cu-contract-bill-switch__item--active {
+  background: linear-gradient(135deg, #fff7f0 0%, #ffe8d6 100%);
+  border-color: #ffb36b;
+  box-shadow: 0 8rpx 24rpx rgba(255, 143, 61, 0.15);
+}
+
+.cu-contract-bill-switch__item--in.cu-contract-bill-switch__item--active {
+  background: linear-gradient(135deg, #edf9f0 0%, #d8f3e0 100%);
+  border-color: #5ec98a;
+  box-shadow: 0 8rpx 24rpx rgba(47, 154, 79, 0.15);
+}
+
+.cu-contract-bill-switch__icon {
+  width: 56rpx;
+  height: 56rpx;
+  line-height: 56rpx;
+  text-align: center;
+  border-radius: 14rpx;
+  font-size: 28rpx;
+  font-weight: 700;
+  color: #fff;
+  flex-shrink: 0;
+}
+
+.cu-contract-bill-switch__item--pay .cu-contract-bill-switch__icon {
+  background: linear-gradient(135deg, #ff8f3d, #ffb347);
+}
+
+.cu-contract-bill-switch__item--in .cu-contract-bill-switch__icon {
+  background: linear-gradient(135deg, #2f9a4f, #5ec98a);
+}
+
+.cu-contract-bill-switch__text {
+  min-width: 0;
+}
+
+.cu-contract-bill-switch__title {
+  display: block;
+  font-size: 28rpx;
+  font-weight: 600;
+  color: $cu-text;
+  line-height: 1.3;
+}
+
+.cu-contract-bill-switch__desc {
+  display: block;
+  margin-top: 6rpx;
+  font-size: 22rpx;
+  color: $cu-text-muted;
+  line-height: 1.3;
+}
+
+.cu-contract-bill-summary {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  margin-bottom: 20rpx;
+  padding: 0 4rpx;
+}
+
+.cu-contract-bill-summary__label {
+  font-size: 28rpx;
+  font-weight: 600;
+  color: $cu-text;
+}
+
+.cu-contract-bill-summary__count {
+  font-size: 24rpx;
+  color: $cu-text-muted;
+}
+
+.cu-contract-bill-loading {
+  padding: 80rpx 0;
+  text-align: center;
+  font-size: 26rpx;
+  color: $cu-text-muted;
+}
+
+.cu-contract-bill-list {
+  display: flex;
+  flex-direction: column;
+  gap: 20rpx;
+}
+
+.cu-contract-bill-card {
+  background: #fff;
+  border-radius: 18rpx;
+  overflow: hidden;
+  box-shadow: 0 8rpx 28rpx rgba(15, 35, 75, 0.06);
+  border: 1rpx solid #eef1f6;
+}
+
+.cu-contract-bill-card--pay {
+  border-top: 6rpx solid #ff8f3d;
+}
+
+.cu-contract-bill-card--in {
+  border-top: 6rpx solid #2f9a4f;
+}
+
+.cu-contract-bill-card__top {
+  display: flex;
+  align-items: flex-start;
+  justify-content: space-between;
+  gap: 16rpx;
+  padding: 24rpx 24rpx 0;
+}
+
+.cu-contract-bill-card__title-wrap {
+  min-width: 0;
+  flex: 1;
+}
+
+.cu-contract-bill-card__type {
+  display: block;
+  font-size: 30rpx;
+  font-weight: 600;
+  color: $cu-text;
+}
+
+.cu-contract-bill-card__code {
+  display: block;
+  margin-top: 8rpx;
+  font-size: 24rpx;
+  color: $cu-text-muted;
+  word-break: break-all;
+}
+
+.cu-contract-bill-card__status {
+  flex-shrink: 0;
+  padding: 8rpx 16rpx;
+  border-radius: 999rpx;
+  font-size: 22rpx;
+  font-weight: 500;
+  background: #f0f2f6;
+  color: $cu-text-muted;
+}
+
+.cu-contract-bill-card__status--ok {
+  background: #edf9f0;
+  color: #2f9a4f;
+}
+
+.cu-contract-bill-card__status--warn {
+  background: #fff7e6;
+  color: #e68a00;
+}
+
+.cu-contract-bill-card__status--bad {
+  background: #fff1f0;
+  color: $cu-danger;
+}
+
+.cu-contract-bill-card__status--muted {
+  background: #f0f2f6;
+  color: $cu-text-muted;
+}
+
+.cu-contract-bill-card__amounts {
+  display: flex;
+  align-items: stretch;
+  margin: 20rpx 24rpx 0;
+  padding: 20rpx;
+  border-radius: 14rpx;
+  background: #f8fafc;
+}
+
+.cu-contract-bill-card__amount-item {
+  flex: 1;
+  min-width: 0;
+}
+
+.cu-contract-bill-card__amount-divider {
+  width: 1rpx;
+  margin: 0 20rpx;
+  background: #e3e8ef;
+}
+
+.cu-contract-bill-card__amount-label {
+  display: block;
+  font-size: 22rpx;
+  color: $cu-text-muted;
+  margin-bottom: 8rpx;
+}
+
+.cu-contract-bill-card__amount-value {
+  display: block;
+  font-size: 32rpx;
+  font-weight: 700;
+  color: $cu-text;
+}
+
+.cu-contract-bill-card__amount-value--muted {
+  font-size: 28rpx;
+  font-weight: 600;
+  color: #5c6b7f;
+}
+
+.cu-contract-bill-card--pay .cu-contract-bill-card__amount-value {
+  color: #e68a00;
+}
+
+.cu-contract-bill-card--in .cu-contract-bill-card__amount-value {
+  color: #2f9a4f;
+}
+
+.cu-contract-bill-card__overdue {
+  margin: 16rpx 24rpx 0;
+  padding: 10rpx 16rpx;
+  border-radius: 8rpx;
+  background: #fff1f0;
+  color: $cu-danger;
+  font-size: 22rpx;
+  font-weight: 500;
+}
+
+.cu-contract-bill-card__meta {
+  margin: 20rpx 24rpx 0;
+  padding-top: 20rpx;
+  border-top: 1rpx dashed #e8edf3;
+}
+
+.cu-contract-bill-card__meta-row {
+  display: flex;
+  align-items: flex-start;
+  justify-content: space-between;
+  gap: 24rpx;
+  margin-bottom: 14rpx;
+}
+
+.cu-contract-bill-card__meta-row:last-child {
+  margin-bottom: 0;
+}
+
+.cu-contract-bill-card__meta-label {
+  flex-shrink: 0;
+  font-size: 24rpx;
+  color: $cu-text-muted;
+}
+
+.cu-contract-bill-card__meta-value {
+  text-align: right;
+  font-size: 24rpx;
+  color: $cu-text;
+  line-height: 1.5;
+  word-break: break-all;
+}
+
+.cu-contract-bill-card__meta-value--danger {
+  color: $cu-danger;
+  font-weight: 600;
+}
+
+.cu-contract-bill-card__foot {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  margin-top: 20rpx;
+  padding: 18rpx 24rpx;
+  background: #fafbfd;
+  font-size: 24rpx;
+  color: $cu-primary;
+}
+
+.cu-contract-bill-card__arrow {
+  font-weight: 600;
+}
+
+.cu-contract-bill-toolbar {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  padding: 8rpx 24rpx 20rpx;
+}
+
+.cu-contract-bill-tabs {
+  display: flex;
+  background: #eef2f8;
+  border-radius: 12rpx;
+  padding: 6rpx;
+}
+
+.cu-contract-bill-tabs__item {
+  min-width: 148rpx;
+  padding: 14rpx 24rpx;
+  text-align: center;
+  font-size: 26rpx;
+  color: $cu-text-muted;
+  border-radius: 10rpx;
+}
+
+.cu-contract-bill-tabs__item--active {
+  background: #fff;
+  color: $cu-primary;
+  font-weight: 600;
+  box-shadow: 0 4rpx 12rpx rgba(32, 128, 247, 0.12);
+}
+
+.cu-contract-bill-toolbar__count {
+  font-size: 24rpx;
+  color: $cu-text-muted;
+}
+
+.cu-contract-bill-grid {
+  display: flex;
+  flex-wrap: wrap;
+  margin-top: 20rpx;
+  padding-top: 20rpx;
+  border-top: 1rpx solid #eef1f6;
+}
+
+.cu-contract-bill-grid__item {
+  width: 50%;
+  margin-bottom: 18rpx;
+  padding-right: 12rpx;
+  box-sizing: border-box;
+}
+
+.cu-contract-bill-grid__item--full {
+  width: 100%;
+  padding-right: 0;
+}
+
+.cu-contract-bill-grid__label {
+  display: block;
+  font-size: 22rpx;
+  color: $cu-text-muted;
+  margin-bottom: 8rpx;
+}
+
+.cu-contract-bill-grid__value {
+  display: block;
+  font-size: 26rpx;
+  color: $cu-text;
+  line-height: 1.4;
+  word-break: break-all;
+}
+
+.cu-contract-bill-grid__value--danger {
+  color: $cu-danger;
+  font-weight: 600;
+}
+
+.cu-bill-detail-hero {
+  position: relative;
+  margin: 24rpx 24rpx 0;
+  border-radius: $cu-radius;
+  overflow: hidden;
+  box-shadow: 0 16rpx 48rpx rgba(32, 128, 247, 0.22);
+}
+
+.cu-bill-detail-hero__bg {
+  position: absolute;
+  inset: 0;
+  background: linear-gradient(145deg, #2080f7 0%, #4a9bff 55%, #6eb3ff 100%);
+}
+
+.cu-bill-detail-hero--warn .cu-bill-detail-hero__bg {
+  background: linear-gradient(145deg, #ff8f3d 0%, #ffb347 100%);
+  box-shadow: 0 16rpx 48rpx rgba(255, 143, 61, 0.22);
+}
+
+.cu-bill-detail-hero--ok .cu-bill-detail-hero__bg {
+  background: linear-gradient(145deg, #19be6b 0%, #3dd68c 100%);
+}
+
+.cu-bill-detail-hero__content {
+  position: relative;
+  z-index: 1;
+  padding: 36rpx 32rpx;
+  color: #fff;
+}
+
+.cu-bill-detail-hero__top {
+  display: flex;
+  align-items: flex-start;
+  justify-content: space-between;
+  gap: 16rpx;
+  margin-bottom: 28rpx;
+}
+
+.cu-bill-detail-hero__type {
+  display: inline-flex;
+  padding: 6rpx 14rpx;
+  background: rgba(255, 255, 255, 0.18);
+  border-radius: 8rpx;
+  font-size: 22rpx;
+  margin-bottom: 12rpx;
+}
+
+.cu-bill-detail-hero__code {
+  font-size: 30rpx;
+  font-weight: 600;
+  line-height: 1.4;
+  word-break: break-all;
+}
+
+.cu-bill-detail-hero__status {
+  flex-shrink: 0;
+  padding: 8rpx 18rpx;
+  background: rgba(255, 255, 255, 0.22);
+  border-radius: 999rpx;
+  font-size: 24rpx;
+}
+
+.cu-bill-detail-hero__amount-label {
+  display: block;
+  font-size: 24rpx;
+  opacity: 0.88;
+  margin-bottom: 10rpx;
+}
+
+.cu-bill-detail-hero__amount {
+  display: block;
+  font-size: 60rpx;
+  font-weight: 700;
+  line-height: 1.1;
+}
+
+.cu-bill-detail-hero__due {
+  display: block;
+  margin-top: 14rpx;
+  font-size: 24rpx;
+  opacity: 0.9;
+}
+
+.cu-bill-stat-row {
+  display: flex;
+  gap: 16rpx;
+  margin: 20rpx 24rpx 0;
+}
+
+.cu-bill-stat {
+  flex: 1;
+  padding: 22rpx 16rpx;
+  background: $cu-card-bg;
+  border-radius: 16rpx;
+  box-shadow: $cu-shadow;
+  text-align: center;
+}
+
+.cu-bill-stat__label {
+  display: block;
+  font-size: 22rpx;
+  color: $cu-text-muted;
+  margin-bottom: 10rpx;
+}
+
+.cu-bill-stat__value {
+  display: block;
+  font-size: 28rpx;
+  font-weight: 700;
+  color: $cu-text;
+}
+
+.cu-bill-stat__value--danger {
+  color: $cu-danger;
+}
+
+.cu-bill-panel {
+  margin-top: 20rpx;
+}
+
+.cu-bill-info-grid {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 16rpx;
+  padding: 0 28rpx 24rpx;
+}
+
+.cu-bill-info-item {
+  width: calc(50% - 8rpx);
+  padding: 18rpx 20rpx;
+  background: #fafbfc;
+  border-radius: 14rpx;
+}
+
+.cu-bill-info-item--full {
+  width: 100%;
+}
+
+.cu-bill-info-item__label {
+  display: block;
+  font-size: 22rpx;
+  color: $cu-text-muted;
+  margin-bottom: 8rpx;
+}
+
+.cu-bill-info-item__value {
+  display: block;
+  font-size: 26rpx;
+  color: $cu-text;
+  font-weight: 500;
+  line-height: 1.4;
+  word-break: break-all;
+}
+
+.cu-bill-timeline {
+  padding: 0 28rpx 12rpx;
+}
+
+.cu-bill-timeline__item {
+  display: flex;
+  align-items: flex-start;
+  gap: 18rpx;
+  padding: 22rpx 0;
+  border-bottom: 1rpx solid #f0f2f6;
+}
+
+.cu-bill-timeline__item:last-child {
+  border-bottom: none;
+}
+
+.cu-bill-timeline__icon {
+  width: 64rpx;
+  height: 64rpx;
+  border-radius: 18rpx;
+  background: linear-gradient(135deg, #eef2ff, #d6e4ff);
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  font-size: 30rpx;
+  flex-shrink: 0;
+}
+
+.cu-bill-timeline__main {
+  flex: 1;
+  min-width: 0;
+}
+
+.cu-bill-timeline__title {
+  font-size: 28rpx;
+  font-weight: 600;
+  color: $cu-text;
+  margin-bottom: 6rpx;
+}
+
+.cu-bill-timeline__amount {
+  font-size: 32rpx;
+  font-weight: 700;
+  color: $cu-success;
+  margin-bottom: 6rpx;
+}
+
+.cu-bill-timeline__time {
+  font-size: 24rpx;
+  color: $cu-text-muted;
+}
+
+.cu-bill-empty-record {
+  margin: 20rpx 24rpx 0;
+  padding: 48rpx 24rpx;
+  background: $cu-card-bg;
+  border-radius: $cu-radius;
+  box-shadow: $cu-shadow;
+  text-align: center;
+}
+
+.cu-bill-empty-record__icon {
+  display: block;
+  font-size: 48rpx;
+  margin-bottom: 12rpx;
+}
+
+.cu-bill-empty-record__text {
+  font-size: 26rpx;
+  color: $cu-text-muted;
+}
+
+.cu-bill-empty-record--inset {
+  margin: 0;
+  box-shadow: none;
+  padding: 32rpx 24rpx 40rpx;
+}
+
+.cu-revenue-list {
+  padding: 0 28rpx 24rpx;
+}
+
+.cu-revenue-card {
+  padding: 24rpx;
+  background: #fafbfc;
+  border-radius: 16rpx;
+  border: 1rpx solid #eef1f6;
+  margin-bottom: 16rpx;
+}
+
+.cu-revenue-card:last-child {
+  margin-bottom: 0;
+}
+
+.cu-revenue-card__head {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  gap: 16rpx;
+  margin-bottom: 18rpx;
+  padding-bottom: 16rpx;
+  border-bottom: 1rpx solid #eef1f6;
+}
+
+.cu-revenue-card__type {
+  padding: 6rpx 14rpx;
+  background: #edf9f0;
+  color: #2f9a4f;
+  border-radius: 8rpx;
+  font-size: 22rpx;
+  font-weight: 500;
+}
+
+.cu-revenue-card__type--out {
+  background: #fff1f0;
+  color: $cu-danger;
+}
+
+.cu-revenue-card__amount {
+  font-size: 32rpx;
+  font-weight: 700;
+  color: $cu-text;
+}
+
+.cu-revenue-card__row {
+  display: flex;
+  align-items: flex-start;
+  gap: 16rpx;
+  padding: 10rpx 0;
+  font-size: 24rpx;
+  line-height: 1.5;
+}
+
+.cu-revenue-card__label {
+  flex-shrink: 0;
+  width: 140rpx;
+  color: $cu-text-muted;
+}
+
+.cu-revenue-card__value {
+  flex: 1;
+  color: $cu-text-secondary;
+  word-break: break-all;
+}
+
+.cu-bill-detail-footer {
+  display: flex;
+  align-items: center;
+  gap: 20rpx;
+}
+
+.cu-bill-detail-footer__info {
+  flex: 1;
+  min-width: 0;
+}
+
+.cu-bill-detail-footer__label {
+  display: block;
+  font-size: 22rpx;
+  color: $cu-text-muted;
+  margin-bottom: 4rpx;
+}
+
+.cu-bill-detail-footer__amount {
+  display: block;
+  font-size: 36rpx;
+  font-weight: 700;
+  color: $cu-danger;
+}
+
+.cu-bill-detail-footer__btn {
+  flex-shrink: 0;
+  min-width: 220rpx;
+  margin-top: 0 !important;
+}
diff --git a/h5/utils/config.js b/h5/utils/config.js
index c779cdc..32bb700 100644
--- a/h5/utils/config.js
+++ b/h5/utils/config.js
@@ -1,8 +1,23 @@
  // export const baseUrl = 'gateway_interface/'
-//export const baseUrl = 'http://localhost:10010/gateway_interface/'
-export const baseUrl = 'https://zhcg.fnwtzx.com/gateway_interface/'
+export const baseUrl = 'http://localhost:10010/'
+//export const baseUrl = 'https://zhcg.fnwtzx.com/gateway_interface/'
 //export const baseUrl = 'https://dmtest.ahapp.net/gateway_interface/'
 
+/** 寰俊鍏紬鍙� AppId锛圤Auth 璺宠浆鐢紝鐢熶骇鐜淇濇寔鐪熷疄閰嶇疆锛� */
+export const wxAppId = 'wx15dfdae9a19177f3'
+/** OAuth 鍥炶皟鍦板潃锛岄渶涓庡叕浼楀彿鍚庡彴閰嶇疆涓�鑷� */
+export const wxRedirectUri = 'https://zhcg.fnwtzx.com/fn_h5'
+
+/**
+ * 寮�鍙戠幆澧冨井淇� OAuth 妯℃嫙锛堣烦杩囧井淇℃祻瑙堝櫒闄愬埗锛�
+ * 鐢熶骇鏋勫缓/涓婄嚎鍓嶅姟蹇呰缃� enabled: false
+ */
+export const devWechatMock = {
+  enabled: true,
+  openId: 'dev_h5_openid_merchant_001',
+  mockCode: 'DEV_MOCK'
+}
+
 export const uploadAvatar = `${baseUrl}visitsAdmin/cloudService/web/public/uploadFtp.do`
 export const uploadUrl = `${baseUrl}visitsAdmin/cloudService/public/uploadBatch`
 
diff --git a/h5/utils/loginSms.js b/h5/utils/loginSms.js
new file mode 100644
index 0000000..2d00ebe
--- /dev/null
+++ b/h5/utils/loginSms.js
@@ -0,0 +1,32 @@
+/**
+ * H5 鐧诲綍楠岃瘉鐮侊細鍏堟牎楠屾墜鏈哄彿锛屾垚鍔熷悗鍐嶅�掕鏃�
+ * @param {Object} vm 椤甸潰瀹炰緥锛堝惈 downTime锛�
+ * @param {string} phone 鎵嬫満鍙�
+ * @param {Function} sendApi 鍙戦�佹帴鍙�
+ * @param {Object} [payload] 璇锋眰浣擄紝榛樿 { phone }
+ */
+export function requestLoginSmsCode (vm, phone, sendApi, payload) {
+  if (!phone) {
+    uni.showToast({ title: '璇疯緭鍏ユ墜鏈哄彿', icon: 'none' })
+    return
+  }
+  const data = payload || { phone }
+  if (!data.phone) {
+    data.phone = phone
+  }
+  sendApi(data).then(res => {
+    if (res.code !== 200) {
+      // service.js 宸插闈� 200 寮� toast锛屾澶勪粎闃绘鍊掕鏃�
+      return
+    }
+    uni.showToast({ title: '宸插彂閫�', icon: 'none' })
+    vm.downTime = 60
+    const timer = setInterval(() => {
+      if (vm.downTime <= 0) {
+        clearInterval(timer)
+        return
+      }
+      vm.downTime--
+    }, 1000)
+  })
+}
diff --git a/h5/utils/roleSwitch.js b/h5/utils/roleSwitch.js
new file mode 100644
index 0000000..9933ca5
--- /dev/null
+++ b/h5/utils/roleSwitch.js
@@ -0,0 +1,19 @@
+import store from '@/store/index.js'
+
+/** 娓呴櫎鐧诲綍鎬佸苟杩斿洖瑙掕壊閫夋嫨椤� */
+export function goRoleSelect () {
+  store.commit('empty')
+  uni.reLaunch({ url: '/pages/roleSelect' })
+}
+
+/**
+ * 鍒囨崲瑙掕壊锛堝彲閫夊厛璋冨悗绔� logout锛�
+ * @param {Function|null} logoutApi 杩斿洖 Promise 鐨勯��鍑烘帴鍙o紝濡� logoutPost
+ */
+export function switchRole (logoutApi) {
+  if (logoutApi) {
+    logoutApi().catch(() => {}).finally(() => goRoleSelect())
+  } else {
+    goRoleSelect()
+  }
+}
diff --git a/h5/utils/service.js b/h5/utils/service.js
index 6df5176..f3b1672 100644
--- a/h5/utils/service.js
+++ b/h5/utils/service.js
@@ -25,20 +25,30 @@
 					let data = res.data
 					// 鎺у埗鍙版樉绀烘暟鎹俊鎭�
 					uni.hideLoading()
-					// 鐧诲綍杩囨湡
-					if (data.code !== 200) {
-						setTimeout(() => {
-							uni.showToast({
-								title: data.message,
-								icon: "none",
-								duration: 2000
-							})
+					// Spring Boot / Gateway 榛樿閿欒浣擄紙HTTP 500 鏃舵棤 code 瀛楁锛�
+					if (data && data.status && data.code == null) {
+						const errMsg = data.message || data.error || '鏈嶅姟寮傚父锛岃绋嶅悗閲嶈瘯'
+						uni.showToast({
+							title: errMsg,
+							icon: 'none',
+							duration: 2500
 						})
-						if (data.code === 500 || data.code === 5112) {
+						return resolve({ code: data.status, message: errMsg })
+					}
+					// 涓氬姟澶辫触
+					if (data.code !== 200) {
+						const msg = data.message || '鎿嶄綔澶辫触'
+						uni.showToast({
+							title: msg,
+							icon: 'none',
+							duration: 2500
+						})
+						// 浠呮湭鐧诲綍(5112)璺宠浆鐧诲綍椤碉紝閬垮厤鍟嗘埛鍙戠爜绛変笟鍔¢敊璇璺宠浆
+						if (data.code === 5112) {
+							const userType = uni.getStorageSync('userType')
 							uni.clearStorageSync()
-							return uni.navigateTo({
-								url: '/pages/login'
-							})
+							const loginUrl = userType === 1 ? '/pages/customer/login' : '/pages/login'
+							return uni.navigateTo({ url: loginUrl })
 						}
 						return resolve(data)
 					}
diff --git a/h5/utils/wechatAuth.js b/h5/utils/wechatAuth.js
new file mode 100644
index 0000000..f6987a8
--- /dev/null
+++ b/h5/utils/wechatAuth.js
@@ -0,0 +1,55 @@
+import { devWechatMock, wxAppId, wxRedirectUri } from './config.js'
+
+/** 浠庡綋鍓� URL 瑙f瀽寰俊 OAuth code */
+export function extractOAuthCodeFromUrl () {
+  if (typeof window === 'undefined') return ''
+  const href = window.location.href
+  if (href.indexOf('code=') === -1) return ''
+  const parts = href.split('?')
+  for (const q of parts) {
+    if (q.indexOf('code=') !== -1) {
+      const stateIdx = q.indexOf('&state')
+      return q.substring(q.indexOf('code=') + 5, stateIdx > -1 ? stateIdx : q.length)
+    }
+  }
+  return ''
+}
+
+/** 璺宠浆寰俊 OAuth锛堜粎闈炴ā鎷熺幆澧冿級 */
+export function redirectToWechatOAuth () {
+  const uri = encodeURIComponent(wxRedirectUri)
+  window.location.href =
+    `https://open.weixin.qq.com/connect/oauth2/authorize?appid=${wxAppId}&redirect_uri=${uri}&response_type=code&scope=snsapi_base#wechat_redirect`
+}
+
+/**
+ * 寰俊 OAuth锛氬紑鍙戠幆澧冪敤妯℃嫙 openid锛岀敓浜х幆澧冭蛋寰俊鎺堟潈
+ * @param {Object} options
+ * @param {Function} options.authorizeApi (data) => Promise
+ * @param {Function} options.onSuccess (res) => void
+ * @param {string} [options.fallbackCode] 椤甸潰鍐呯紦瀛樼殑 code
+ */
+export function runWechatOAuthFlow ({ authorizeApi, onSuccess, fallbackCode = '' }) {
+  if (typeof window === 'undefined') return
+
+  const handleAuthorize = (code) => {
+    if (!code) return
+    authorizeApi({ code }).then(res => {
+      if (res.code === 200) onSuccess(res)
+    })
+  }
+
+  if (devWechatMock.enabled) {
+    const mockOpenId = uni.getStorageSync('openId') || devWechatMock.openId
+    uni.setStorageSync('openId', mockOpenId)
+    handleAuthorize(devWechatMock.mockCode)
+    return
+  }
+
+  const code = extractOAuthCodeFromUrl() || fallbackCode
+  if (code) {
+    handleAuthorize(code)
+    return
+  }
+  redirectToWechatOAuth()
+}
diff --git a/h5/utils/wxpay.js b/h5/utils/wxpay.js
new file mode 100644
index 0000000..a39fc7f
--- /dev/null
+++ b/h5/utils/wxpay.js
@@ -0,0 +1,22 @@
+export function invokeWxPay (payParams) {
+  return new Promise((resolve, reject) => {
+    const onBridgeReady = () => {
+      window.WeixinJSBridge.invoke('getBrandWCPayRequest', {
+        appId: payParams.appId,
+        timeStamp: payParams.timeStamp,
+        nonceStr: payParams.nonceStr,
+        package: payParams.package,
+        signType: payParams.signType || 'MD5',
+        paySign: payParams.paySign
+      }, (res) => {
+        if (res.err_msg === 'get_brand_wcpay_request:ok') resolve(res)
+        else reject(res)
+      })
+    }
+    if (typeof window.WeixinJSBridge === 'undefined') {
+      document.addEventListener('WeixinJSBridgeReady', onBridgeReady, false)
+    } else {
+      onBridgeReady()
+    }
+  })
+}
diff --git a/server/db/business.yw_customer_h5.sql b/server/db/business.yw_customer_h5.sql
new file mode 100644
index 0000000..c00a1f0
--- /dev/null
+++ b/server/db/business.yw_customer_h5.sql
@@ -0,0 +1,70 @@
+-- 鍟嗘埛 H5锛氬井淇℃敮浠樿鍗曘�佽疆鎾浘銆佸鎴烽娆″厖鍊兼爣璁般�佸叧鑱旀墿灞�
+
+CREATE TABLE IF NOT EXISTS `yw_wx_pay_order` (
+  `id` int NOT NULL AUTO_INCREMENT,
+  `creator` int DEFAULT NULL,
+  `create_date` datetime DEFAULT NULL,
+  `editor` int DEFAULT NULL,
+  `edit_date` datetime DEFAULT NULL,
+  `isdeleted` tinyint DEFAULT 0,
+  `remark` varchar(500) DEFAULT NULL,
+  `order_no` varchar(64) NOT NULL COMMENT '鍟嗘埛璁㈠崟鍙�',
+  `customer_id` int NOT NULL COMMENT '浠樻鍟嗘埛',
+  `order_type` tinyint NOT NULL COMMENT '0鐢佃〃鍏呭�� 1绌鸿皟鍏呭�� 2璐﹀崟缂磋垂',
+  `biz_ref_id` int DEFAULT NULL COMMENT 'electrical_id/customer_id/bill_id',
+  `biz_record_id` int DEFAULT NULL COMMENT 'yw_electrical_charge.id 鎴� yw_contract_revenue.id',
+  `amount` decimal(12,2) NOT NULL,
+  `status` tinyint DEFAULT 0 COMMENT '0寰呮敮浠� 1鎴愬姛 2澶辫触 3鍏抽棴',
+  `wx_transaction_id` varchar(64) DEFAULT NULL,
+  `pay_time` datetime DEFAULT NULL,
+  `openid` varchar(128) DEFAULT NULL,
+  `request_snapshot` text COMMENT '涓嬪崟鍙傛暟JSON',
+  `status_info` varchar(500) DEFAULT NULL,
+  PRIMARY KEY (`id`),
+  UNIQUE KEY `uk_order_no` (`order_no`),
+  KEY `idx_customer` (`customer_id`),
+  KEY `idx_status` (`status`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='寰俊H5鏀粯璁㈠崟';
+
+CREATE TABLE IF NOT EXISTS `yw_h5_banner` (
+  `id` int NOT NULL AUTO_INCREMENT,
+  `creator` int DEFAULT NULL,
+  `create_date` datetime DEFAULT NULL,
+  `editor` int DEFAULT NULL,
+  `edit_date` datetime DEFAULT NULL,
+  `isdeleted` tinyint DEFAULT 0,
+  `remark` varchar(500) DEFAULT NULL,
+  `title` varchar(200) DEFAULT NULL,
+  `image_url` varchar(500) NOT NULL,
+  `link_url` varchar(500) DEFAULT NULL,
+  `sortnum` int DEFAULT 0,
+  `status` tinyint DEFAULT 0 COMMENT '0鍚敤 1绂佺敤',
+  `scope` tinyint DEFAULT 1 COMMENT '1鍟嗘埛宸ヤ綔鍙�',
+  PRIMARY KEY (`id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='H5杞挱鍥�';
+
+SET @db = DATABASE();
+
+SET @sql = IF((SELECT COUNT(*) FROM information_schema.COLUMNS
+   WHERE TABLE_SCHEMA = @db AND TABLE_NAME = 'yw_customer' AND COLUMN_NAME = 'first_recharge_done') = 0,
+  'ALTER TABLE `yw_customer` ADD COLUMN `first_recharge_done` tinyint DEFAULT 0 COMMENT ''鏄惁宸插畬鎴愰娆″厖鍊煎埗'' AFTER `openid`',
+  '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_customer_electrical' AND COLUMN_NAME = 'bind_source') = 0,
+  'ALTER TABLE `yw_customer_electrical` ADD COLUMN `bind_source` tinyint DEFAULT 1 COMMENT ''0鎵嬪姩 1鍚堝悓鑷姩'' AFTER `electrical_id`, ADD COLUMN `contract_id` int DEFAULT NULL AFTER `bind_source`',
+  '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 = 'wx_order_no') = 0,
+  'ALTER TABLE `yw_electrical_charge` ADD COLUMN `wx_order_no` varchar(64) DEFAULT NULL COMMENT ''寰俊鏀粯璁㈠崟鍙�'' 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_contract_revenue' AND COLUMN_NAME = 'wx_order_no') = 0,
+  'ALTER TABLE `yw_contract_revenue` ADD COLUMN `wx_order_no` varchar(64) DEFAULT NULL COMMENT ''寰俊鏀粯璁㈠崟鍙�'' AFTER `bill_id`',
+  'SELECT 1');
+PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
diff --git a/server/db/business.yw_h5_banner.menu.sql b/server/db/business.yw_h5_banner.menu.sql
new file mode 100644
index 0000000..38f97a4
--- /dev/null
+++ b/server/db/business.yw_h5_banner.menu.sql
@@ -0,0 +1,35 @@
+-- H5 杞挱鍥剧鐞嗚彍鍗曪紙鎸傚湪銆屽晢鎴峰厖鍊笺�嶄笅锛屽彲閲嶅鎵ц锛�
+
+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`, 'H5杞挱鍥�', '/business/ywh5banner', '鍟嗘埛H5宸ヤ綔鍙拌疆鎾浘缁存姢', 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/ywh5banner')
+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/ywh5banner' 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
+  );
+
+INSERT INTO `SYSTEM_ROLE_PERMISSION` (`ROLE_ID`, `PERMISSION_ID`, `CREATE_TIME`, `UPDATE_TIME`, `CREATE_USER`, `UPDATE_USER`, `DELETED`)
+SELECT r.`ID`, p.`ID`, CURRENT_TIMESTAMP, NULL, 1, NULL, 0
+FROM `SYSTEM_ROLE` r
+INNER JOIN `SYSTEM_PERMISSION` p ON p.`CODE` IN (
+    'business:ywh5banner:query',
+    'business:ywh5banner:create',
+    'business:ywh5banner:update',
+    'business:ywh5banner:delete'
+) AND p.`DELETED` = 0
+WHERE r.`DELETED` = 0 AND (r.`CODE` = 'admin' OR r.`NAME` IN ('瓒呯骇绠$悊鍛�', '绠$悊鍛�'))
+  AND NOT EXISTS (
+    SELECT 1 FROM `SYSTEM_ROLE_PERMISSION` rp
+    WHERE rp.`ROLE_ID` = r.`ID` AND rp.`PERMISSION_ID` = p.`ID` AND rp.`DELETED` = 0
+  );
diff --git a/server/db/business.yw_h5_banner.permissions.sql b/server/db/business.yw_h5_banner.permissions.sql
new file mode 100644
index 0000000..1d1d319
--- /dev/null
+++ b/server/db/business.yw_h5_banner.permissions.sql
@@ -0,0 +1,17 @@
+-- H5 鍟嗘埛宸ヤ綔鍙拌疆鎾浘锛氭寜閽潈闄愶紙鍙噸澶嶆墽琛岋級
+
+INSERT INTO `SYSTEM_PERMISSION` (`CODE`, `NAME`, `REMARK`, `FIXED`, `CREATE_USER`, `CREATE_TIME`, `UPDATE_USER`, `UPDATE_TIME`, `DELETED`)
+SELECT 'business:ywh5banner:create', '鏂板缓H5杞挱鍥�', '', 0, 1, CURRENT_TIMESTAMP, NULL, NULL, 0
+WHERE NOT EXISTS (SELECT 1 FROM `SYSTEM_PERMISSION` p WHERE p.`CODE` = 'business:ywh5banner:create' AND p.`DELETED` = 0);
+
+INSERT INTO `SYSTEM_PERMISSION` (`CODE`, `NAME`, `REMARK`, `FIXED`, `CREATE_USER`, `CREATE_TIME`, `UPDATE_USER`, `UPDATE_TIME`, `DELETED`)
+SELECT 'business:ywh5banner:delete', '鍒犻櫎H5杞挱鍥�', '', 0, 1, CURRENT_TIMESTAMP, NULL, NULL, 0
+WHERE NOT EXISTS (SELECT 1 FROM `SYSTEM_PERMISSION` p WHERE p.`CODE` = 'business:ywh5banner:delete' AND p.`DELETED` = 0);
+
+INSERT INTO `SYSTEM_PERMISSION` (`CODE`, `NAME`, `REMARK`, `FIXED`, `CREATE_USER`, `CREATE_TIME`, `UPDATE_USER`, `UPDATE_TIME`, `DELETED`)
+SELECT 'business:ywh5banner:update', '淇敼H5杞挱鍥�', '', 0, 1, CURRENT_TIMESTAMP, NULL, NULL, 0
+WHERE NOT EXISTS (SELECT 1 FROM `SYSTEM_PERMISSION` p WHERE p.`CODE` = 'business:ywh5banner:update' AND p.`DELETED` = 0);
+
+INSERT INTO `SYSTEM_PERMISSION` (`CODE`, `NAME`, `REMARK`, `FIXED`, `CREATE_USER`, `CREATE_TIME`, `UPDATE_USER`, `UPDATE_TIME`, `DELETED`)
+SELECT 'business:ywh5banner:query', '鏌ヨH5杞挱鍥�', '', 0, 1, CURRENT_TIMESTAMP, NULL, NULL, 0
+WHERE NOT EXISTS (SELECT 1 FROM `SYSTEM_PERMISSION` p WHERE p.`CODE` = 'business:ywh5banner:query' AND p.`DELETED` = 0);
diff --git a/server/pom.xml b/server/pom.xml
index fec4ff3..1f9c6ef 100644
--- a/server/pom.xml
+++ b/server/pom.xml
@@ -11,11 +11,11 @@
   <name>dmvisit</name>
   <description></description>
   <modules> 
-      <module>visits</module>
+      <module>emaysms</module>
       <module>system_service</module>
+      <module>visits</module>
       <module>system_timer</module>
       <module>system_gateway</module>
-      <module>emaysms</module>
   </modules>
   <parent>
     <groupId>org.springframework.boot</groupId>
diff --git a/server/system_service/src/main/java/com/doumee/config/cloudfilter/LoginHandlerInterceptor.java b/server/system_service/src/main/java/com/doumee/config/cloudfilter/LoginHandlerInterceptor.java
index abd19e5..52c092d 100644
--- a/server/system_service/src/main/java/com/doumee/config/cloudfilter/LoginHandlerInterceptor.java
+++ b/server/system_service/src/main/java/com/doumee/config/cloudfilter/LoginHandlerInterceptor.java
@@ -1,10 +1,12 @@
 package com.doumee.config.cloudfilter;
 
+import com.alibaba.fastjson.JSON;
 import com.alibaba.fastjson.JSONObject;
 import com.doumee.config.annotation.CloudRequiredPermission;
 import com.doumee.config.annotation.LoginNoRequired;
 import com.doumee.core.constants.ResponseStatus;
 import com.doumee.core.exception.BusinessException;
+import com.doumee.core.model.ApiResponse;
 import com.doumee.core.model.LoginUserInfo;
 import com.doumee.core.utils.Constants;
 import org.apache.commons.lang3.StringUtils;
@@ -47,7 +49,7 @@
                 //鑾峰彇token
                 Cookie[]  cookies =   request.getCookies();
                 String token = request.getHeader(Constants.HEADER_USER_TOKEN);  // 浠� http 璇锋眰澶翠腑鍙栧嚭 token
-                if(StringUtils.isBlank(token)){
+                if(StringUtils.isBlank(token) && cookies != null){
                     for(Cookie c :cookies){
                         if(StringUtils.equals(c.getName(),Constants.HEADER_USER_TOKEN)){
                             token = c.getValue();
@@ -74,7 +76,7 @@
                                 }
                             }
                             if (!hasPermission) {
-                                throw new BusinessException(ResponseStatus.NOT_ALLOWED.getCode(), "娌℃湁璇ユ搷浣滄潈闄�");
+                                return writeBusinessError(response, ResponseStatus.NOT_ALLOWED.getCode(), "娌℃湁璇ユ搷浣滄潈闄�");
                             }
                         }
                     }
@@ -89,16 +91,23 @@
                     }catch (Exception e){
                     }*/
                 } else {
-                    throw new BusinessException(ResponseStatus.NO_LOGIN.getCode(),"鏈櫥褰�");
+                    return writeBusinessError(response, ResponseStatus.NO_LOGIN.getCode(), "鏈櫥褰�");
                 }
             }
-        }else{
-            throw new BusinessException(ResponseStatus.NO_LOGIN.getCode(),"鏈櫥褰�");
+        } else {
+            return writeBusinessError(response, ResponseStatus.NO_LOGIN.getCode(), "鏈櫥褰�");
         }
 
         return true;
     }
 
+    private boolean writeBusinessError(HttpServletResponse response, Integer code, String message) throws IOException {
+        response.setStatus(HttpServletResponse.SC_OK);
+        response.setHeader("content-type", "application/json;charset=UTF-8");
+        response.getWriter().write(JSON.toJSONString(ApiResponse.failed(code, message)));
+        return false;
+    }
+
     private String getRequestBody(HttpServletRequest request) {
         // 瀹炵幇浠巖equest鑾峰彇璇锋眰浣撶殑閫昏緫
         String body = null;
diff --git a/server/visits/dmvisit_service/src/main/java/com/doumee/config/handler/GlobalExceptionHandler.java b/server/system_service/src/main/java/com/doumee/config/handler/GlobalExceptionHandler.java
similarity index 69%
rename from server/visits/dmvisit_service/src/main/java/com/doumee/config/handler/GlobalExceptionHandler.java
rename to server/system_service/src/main/java/com/doumee/config/handler/GlobalExceptionHandler.java
index 73838f3..eec03e0 100644
--- a/server/visits/dmvisit_service/src/main/java/com/doumee/config/handler/GlobalExceptionHandler.java
+++ b/server/system_service/src/main/java/com/doumee/config/handler/GlobalExceptionHandler.java
@@ -6,6 +6,7 @@
 import lombok.extern.slf4j.Slf4j;
 import org.apache.commons.lang3.StringUtils;
 import org.apache.shiro.authz.UnauthorizedException;
+import org.springframework.http.converter.HttpMessageNotReadableException;
 import org.springframework.validation.BindingResult;
 import org.springframework.validation.FieldError;
 import org.springframework.web.bind.MethodArgumentNotValidException;
@@ -17,50 +18,42 @@
 
 /**
  * 鍏ㄥ眬寮傚父澶勭悊
- * @author doumee
- * @date 2023/03/21 14:49
  */
 @Slf4j
 @RestControllerAdvice
 public class GlobalExceptionHandler {
 
-    /**
-     * 涓氬姟寮傚父澶勭悊
-     */
     @ExceptionHandler(BusinessException.class)
-    public <T> ApiResponse<T> handleBusinessException (BusinessException e) {
-        log.error(e.getMessage(), e);
+    public <T> ApiResponse<T> handleBusinessException(BusinessException e) {
+        log.warn("BusinessException: {}", e.getMessage());
         return ApiResponse.failed(e.getCode(), e.getMessage());
     }
 
-    /**
-     * 鏃犳潈闄愬紓甯稿鐞�
-     */
     @ExceptionHandler(UnauthorizedException.class)
-    public <T> ApiResponse<T> handleUnauthorizedException (UnauthorizedException e) {
+    public <T> ApiResponse<T> handleUnauthorizedException(UnauthorizedException e) {
         log.error(e.getMessage(), e);
         return ApiResponse.failed("娌℃湁鎿嶄綔鏉冮檺");
     }
 
-    /**
-     * 鍙傛暟楠岃瘉鏈�氳繃寮傚父澶勭悊
-     */
     @ExceptionHandler(MethodArgumentNotValidException.class)
-    public <T> ApiResponse<T> handleMethodArgumentNotValidException (MethodArgumentNotValidException e) {
+    public <T> ApiResponse<T> handleMethodArgumentNotValidException(MethodArgumentNotValidException e) {
         log.error(e.getMessage(), e);
         BindingResult bindingResult = e.getBindingResult();
         List<String> errors = new ArrayList<>();
-        for(FieldError fieldError : bindingResult.getFieldErrors()){
+        for (FieldError fieldError : bindingResult.getFieldErrors()) {
             errors.add(fieldError.getDefaultMessage());
         }
         return ApiResponse.failed(ResponseStatus.BAD_REQUEST.getCode(), StringUtils.join(errors));
     }
 
-    /**
-     * 鍏跺畠寮傚父澶勭悊
-     */
+    @ExceptionHandler(HttpMessageNotReadableException.class)
+    public <T> ApiResponse<T> handleHttpMessageNotReadableException(HttpMessageNotReadableException e) {
+        log.error(e.getMessage(), e);
+        return ApiResponse.failed(ResponseStatus.BAD_REQUEST.getCode(), "璇锋眰鍙傛暟鏍煎紡閿欒");
+    }
+
     @ExceptionHandler(Exception.class)
-    public <T> ApiResponse<T> handleException (Exception e) {
+    public <T> ApiResponse<T> handleException(Exception e) {
         log.error(e.getMessage(), e);
         return ApiResponse.failed(ResponseStatus.SERVER_ERROR, e);
     }
diff --git a/server/system_service/src/main/java/com/doumee/dao/business/model/SmsEmail.java b/server/system_service/src/main/java/com/doumee/dao/business/model/SmsEmail.java
index f28469d..e2eabe4 100644
--- a/server/system_service/src/main/java/com/doumee/dao/business/model/SmsEmail.java
+++ b/server/system_service/src/main/java/com/doumee/dao/business/model/SmsEmail.java
@@ -82,6 +82,10 @@
     @ExcelColumn(name="绫诲瀷 0鐭俊 1閭欢")
     private Integer type;
 
+    @ApiModelProperty(value = "H5鐧诲綍瑙掕壊 0杩愮淮 1鍟嗘埛")
+    @TableField(exist = false)
+    private Integer userType;
+
     @ApiModelProperty(value = "鍏宠仈瀵硅薄缂栫爜", example = "1")
     @ExcelColumn(name="鍏宠仈瀵硅薄缂栫爜")
     private Integer objId;
diff --git a/server/visits/dmvisit_admin/src/main/java/com/doumee/cloud/admin/YwH5BannerCloudController.java b/server/visits/dmvisit_admin/src/main/java/com/doumee/cloud/admin/YwH5BannerCloudController.java
new file mode 100644
index 0000000..b4972a8
--- /dev/null
+++ b/server/visits/dmvisit_admin/src/main/java/com/doumee/cloud/admin/YwH5BannerCloudController.java
@@ -0,0 +1,78 @@
+package com.doumee.cloud.admin;
+
+import com.doumee.api.BaseController;
+import com.doumee.config.annotation.CloudRequiredPermission;
+import com.doumee.core.annotation.pr.PreventRepeat;
+import com.doumee.core.model.ApiResponse;
+import com.doumee.core.model.PageData;
+import com.doumee.core.model.PageWrap;
+import com.doumee.core.utils.Constants;
+import com.doumee.dao.business.model.YwH5Banner;
+import com.doumee.service.business.YwH5BannerService;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+@Api(tags = "H5杞挱鍥�")
+@RestController
+@RequestMapping(Constants.CLOUD_SERVICE_URL_INDEX + "/business/ywH5Banner")
+public class YwH5BannerCloudController extends BaseController {
+
+    @Autowired
+    private YwH5BannerService ywH5BannerService;
+
+    @PreventRepeat
+    @ApiOperation("鏂板缓")
+    @PostMapping("/create")
+    @CloudRequiredPermission("business:ywh5banner:create")
+    public ApiResponse<Integer> create(@RequestBody YwH5Banner ywH5Banner,
+                                       @RequestHeader(Constants.HEADER_USER_TOKEN) String token) {
+        ywH5Banner.setLoginUserInfo(this.getLoginUser(token));
+        return ApiResponse.success(ywH5BannerService.create(ywH5Banner));
+    }
+
+    @ApiOperation("鏍规嵁ID鍒犻櫎")
+    @GetMapping("/delete/{id}")
+    @CloudRequiredPermission("business:ywh5banner:delete")
+    public ApiResponse<Void> deleteById(@PathVariable Integer id,
+                                        @RequestHeader(Constants.HEADER_USER_TOKEN) String token) {
+        ywH5BannerService.deleteById(id, this.getLoginUser(token));
+        return ApiResponse.success(null);
+    }
+
+    @ApiOperation("鎵归噺鍒犻櫎")
+    @GetMapping("/delete/batch")
+    @CloudRequiredPermission("business:ywh5banner:delete")
+    public ApiResponse<Void> deleteByIdInBatch(@RequestParam String ids,
+                                               @RequestHeader(Constants.HEADER_USER_TOKEN) String token) {
+        ywH5BannerService.deleteByIdInBatch(this.getIdList(ids), this.getLoginUser(token));
+        return ApiResponse.success(null);
+    }
+
+    @ApiOperation("鏍规嵁ID淇敼")
+    @PostMapping("/updateById")
+    @CloudRequiredPermission("business:ywh5banner:update")
+    public ApiResponse<Void> updateById(@RequestBody YwH5Banner ywH5Banner,
+                                        @RequestHeader(Constants.HEADER_USER_TOKEN) String token) {
+        ywH5Banner.setLoginUserInfo(this.getLoginUser(token));
+        ywH5BannerService.updateById(ywH5Banner);
+        return ApiResponse.success(null);
+    }
+
+    @ApiOperation("鍒嗛〉鏌ヨ")
+    @PostMapping("/page")
+    @CloudRequiredPermission("business:ywh5banner:query")
+    public ApiResponse<PageData<YwH5Banner>> findPage(@RequestBody PageWrap<YwH5Banner> pageWrap,
+                                                      @RequestHeader(Constants.HEADER_USER_TOKEN) String token) {
+        return ApiResponse.success(ywH5BannerService.findPage(pageWrap));
+    }
+
+    @ApiOperation("鏍规嵁ID鏌ヨ")
+    @GetMapping("/{id}")
+    @CloudRequiredPermission("business:ywh5banner:query")
+    public ApiResponse<YwH5Banner> findById(@PathVariable Integer id,
+                                            @RequestHeader(Constants.HEADER_USER_TOKEN) String token) {
+        return ApiResponse.success(ywH5BannerService.findById(id));
+    }
+}
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
new file mode 100644
index 0000000..0cf420e
--- /dev/null
+++ b/server/visits/dmvisit_admin/src/main/java/com/doumee/cloud/web/YwCustomerH5Controller.java
@@ -0,0 +1,212 @@
+package com.doumee.cloud.web;
+
+import com.doumee.api.BaseController;
+import com.doumee.config.annotation.LoginNoRequired;
+import com.doumee.core.annotation.pr.PreventRepeat;
+import com.doumee.core.constants.ResponseStatus;
+import com.doumee.core.exception.BusinessException;
+import com.doumee.core.model.ApiResponse;
+import com.doumee.core.model.LoginUserInfo;
+import com.doumee.core.model.PageData;
+import com.doumee.core.model.PageWrap;
+import com.doumee.core.utils.Constants;
+import com.doumee.dao.business.dto.YwCustomerRechargeRecordVO;
+import com.doumee.dao.business.dto.h5.*;
+import com.doumee.dao.business.model.YwContract;
+import com.doumee.dao.business.model.YwContractBill;
+import com.doumee.dao.business.model.YwH5Banner;
+import com.doumee.dao.business.model.YwWxPayOrder;
+import com.doumee.dao.system.dto.LoginPhoneDTO;
+import com.doumee.service.business.SmsEmailService;
+import com.doumee.service.business.YwCustomerH5AuthService;
+import com.doumee.service.business.YwCustomerH5BizService;
+import com.doumee.service.business.YwCustomerWxPayService;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+import javax.servlet.http.HttpServletRequest;
+import java.util.List;
+import java.util.Map;
+
+@Api(tags = "銆愬叕浼楀彿銆戝晢鎴稨5")
+@RestController
+@Slf4j
+@RequestMapping(Constants.CLOUD_SERVICE_URL_INDEX + "/web/customer")
+public class YwCustomerH5Controller extends BaseController {
+
+    @Autowired
+    private YwCustomerH5BizService ywCustomerH5BizService;
+    @Autowired
+    private YwCustomerWxPayService ywCustomerWxPayService;
+    @Autowired
+    private YwCustomerH5AuthService ywCustomerH5AuthService;
+    @Autowired
+    private SmsEmailService smsEmailService;
+
+    private LoginUserInfo requireCustomerUser(String token) {
+        LoginUserInfo user = getLoginUser(token);
+        if (user == null || !Constants.equalsInteger(user.getH5UserType(), LoginUserInfo.H5_USER_CUSTOMER)) {
+            throw new RuntimeException("鐧诲綍宸插け鏁�");
+        }
+        return user;
+    }
+
+    @LoginNoRequired
+    @PreventRepeat
+    @ApiOperation("鍟嗘埛鐧诲綍鍙戦�侀獙璇佺爜")
+    @PostMapping("/sendLoginSms")
+    public ApiResponse<Integer> sendLoginSms(@RequestBody(required = false) LoginPhoneDTO dto) {
+        try {
+            if (dto == null || StringUtils.isBlank(dto.getPhone())) {
+                return ApiResponse.failed(ResponseStatus.BAD_REQUEST.getCode(), "鎵嬫満鍙蜂笉鑳戒负绌�");
+            }
+            return ApiResponse.success(smsEmailService.sendMerchantLoginSms(dto.getPhone().trim()));
+        } catch (BusinessException e) {
+            return ApiResponse.failed(e.getCode(), e.getMessage());
+        } catch (Throwable e) {
+            log.error("sendLoginSms failed", e);
+            return ApiResponse.failed(ResponseStatus.SERVER_ERROR.getCode(), "鍙戦�侀獙璇佺爜澶辫触锛岃绋嶅悗閲嶈瘯");
+        }
+    }
+
+    @LoginNoRequired
+    @PreventRepeat
+    @ApiOperation("鍟嗘埛鐭俊楠岃瘉鐮佺櫥褰�")
+    @PostMapping("/loginByPhone")
+    public ApiResponse<String> loginByPhone(@Validated @RequestBody LoginPhoneDTO dto) {
+        try {
+            return ApiResponse.success(ywCustomerH5AuthService.loginByPhone(dto));
+        } catch (BusinessException e) {
+            return ApiResponse.failed(e.getCode(), e.getMessage());
+        } catch (Exception e) {
+            return ApiResponse.failed(ResponseStatus.SERVER_ERROR);
+        }
+    }
+
+    @ApiOperation("鑾峰彇褰撳墠鍟嗘埛鐧诲綍淇℃伅")
+    @GetMapping("/getUserInfo")
+    public ApiResponse<LoginUserInfo> getUserInfo(@RequestHeader(Constants.HEADER_USER_TOKEN) String token) {
+        LoginUserInfo user = this.getLoginUser(token);
+        if (user == null || !Constants.equalsInteger(user.getH5UserType(), LoginUserInfo.H5_USER_CUSTOMER)) {
+            return ApiResponse.failed("鐧诲綍宸插け鏁�");
+        }
+        return ApiResponse.success(ywCustomerH5AuthService.buildLoginUserInfo(user.getCustomerId()));
+    }
+
+    @ApiOperation("宸ヤ綔鍙拌疆鎾浘")
+    @GetMapping("/banners")
+    @LoginNoRequired
+    public ApiResponse<List<YwH5Banner>> banners() {
+        return ApiResponse.success(ywCustomerH5BizService.listBanners());
+    }
+
+    @ApiOperation("宸ヤ綔鍙伴椤�")
+    @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()));
+    }
+
+    @ApiOperation("浜ょ數璐硅澶囧垪琛�")
+    @PostMapping("/device/page")
+    public ApiResponse<PageData<CustomerDeviceH5VO>> devicePage(
+            @RequestBody PageWrap<CustomerDeviceQueryDTO> pageWrap,
+            @RequestHeader(Constants.HEADER_USER_TOKEN) String token) {
+        LoginUserInfo user = requireCustomerUser(token);
+        return ApiResponse.success(ywCustomerH5BizService.devicePage(pageWrap, user.getCustomerId()));
+    }
+
+    @ApiOperation("璁惧璇︽儏")
+    @GetMapping("/device/detail")
+    public ApiResponse<CustomerDeviceH5VO> deviceDetail(
+            @RequestParam Integer deviceType,
+            @RequestParam Integer deviceId,
+            @RequestHeader(Constants.HEADER_USER_TOKEN) String token) {
+        LoginUserInfo user = requireCustomerUser(token);
+        return ApiResponse.success(ywCustomerH5BizService.deviceDetail(deviceType, deviceId, user.getCustomerId()));
+    }
+
+    @ApiOperation("鍏呭�艰褰�")
+    @PostMapping("/rechargeRecord/page")
+    public ApiResponse<PageData<YwCustomerRechargeRecordVO>> rechargeRecordPage(
+            @RequestBody PageWrap<CustomerRechargeRecordH5QueryDTO> pageWrap,
+            @RequestHeader(Constants.HEADER_USER_TOKEN) String token) {
+        LoginUserInfo user = requireCustomerUser(token);
+        return ApiResponse.success(ywCustomerH5BizService.rechargeRecordPage(pageWrap, user.getCustomerId()));
+    }
+
+    @ApiOperation("鍚堝悓鍒楄〃")
+    @PostMapping("/contract/page")
+    public ApiResponse<PageData<YwContract>> contractPage(
+            @RequestBody PageWrap<CustomerContractQueryDTO> pageWrap,
+            @RequestHeader(Constants.HEADER_USER_TOKEN) String token) {
+        LoginUserInfo user = requireCustomerUser(token);
+        return ApiResponse.success(ywCustomerH5BizService.contractPage(pageWrap, user.getCustomerId()));
+    }
+
+    @ApiOperation("鍚堝悓璇︽儏")
+    @GetMapping("/contract/{id}")
+    public ApiResponse<Map<String, Object>> contractDetail(
+            @PathVariable("id") Integer id,
+            @RequestParam(value = "billType", required = false, defaultValue = "0") Integer billType,
+            @RequestHeader(Constants.HEADER_USER_TOKEN) String token) {
+        LoginUserInfo user = requireCustomerUser(token);
+        return ApiResponse.success(ywCustomerH5BizService.contractDetail(id, user.getCustomerId(), billType));
+    }
+
+    @ApiOperation("璐﹀崟鍒楄〃")
+    @PostMapping("/bill/page")
+    public ApiResponse<PageData<YwContractBill>> billPage(
+            @RequestBody PageWrap<CustomerBillQueryDTO> pageWrap,
+            @RequestHeader(Constants.HEADER_USER_TOKEN) String token) {
+        LoginUserInfo user = requireCustomerUser(token);
+        return ApiResponse.success(ywCustomerH5BizService.billPage(pageWrap, user.getCustomerId()));
+    }
+
+    @ApiOperation("璐﹀崟璇︽儏")
+    @GetMapping("/bill/{id}")
+    public ApiResponse<Map<String, Object>> billDetail(
+            @PathVariable("id") Integer id,
+            @RequestHeader(Constants.HEADER_USER_TOKEN) String token) {
+        LoginUserInfo user = requireCustomerUser(token);
+        return ApiResponse.success(ywCustomerH5BizService.billDetail(id, user.getCustomerId()));
+    }
+
+    @ApiOperation("鍒涘缓鏀粯璁㈠崟")
+    @PostMapping("/pay/createOrder")
+    public ApiResponse<Map<String, String>> createOrder(
+            @RequestBody CustomerPayCreateDTO dto,
+            @RequestHeader(Constants.HEADER_USER_TOKEN) String token,
+            HttpServletRequest request) {
+        LoginUserInfo user = requireCustomerUser(token);
+        String ip = request.getHeader("X-Forwarded-For");
+        if (ip != null && ip.contains(",")) {
+            ip = ip.split(",")[0].trim();
+        }
+        if (ip == null) {
+            ip = request.getRemoteAddr();
+        }
+        return ApiResponse.success(ywCustomerWxPayService.createOrder(dto, user, ip));
+    }
+
+    @ApiOperation("鏌ヨ鏀粯缁撴灉")
+    @GetMapping("/pay/query/{orderNo}")
+    public ApiResponse<YwWxPayOrder> queryPay(
+            @PathVariable String orderNo,
+            @RequestHeader(Constants.HEADER_USER_TOKEN) String token) {
+        LoginUserInfo user = requireCustomerUser(token);
+        return ApiResponse.success(ywCustomerWxPayService.queryOrder(orderNo, user.getCustomerId()));
+    }
+
+    @ApiOperation("寰俊鏀粯鍥炶皟")
+    @PostMapping("/pay/notify")
+    @LoginNoRequired
+    public String payNotify(@RequestBody String xmlBody) {
+        return ywCustomerWxPayService.handleNotify(xmlBody);
+    }
+}
diff --git a/server/visits/dmvisit_admin/src/main/java/com/doumee/cloud/web/YwCustomerWebController.java b/server/visits/dmvisit_admin/src/main/java/com/doumee/cloud/web/YwCustomerWebController.java
deleted file mode 100644
index 2e38ea4..0000000
--- a/server/visits/dmvisit_admin/src/main/java/com/doumee/cloud/web/YwCustomerWebController.java
+++ /dev/null
@@ -1,50 +0,0 @@
-package com.doumee.cloud.web;
-
-import com.doumee.api.BaseController;
-import com.doumee.config.annotation.LoginNoRequired;
-import com.doumee.core.annotation.pr.PreventRepeat;
-import com.doumee.core.constants.ResponseStatus;
-import com.doumee.core.exception.BusinessException;
-import com.doumee.core.model.ApiResponse;
-import com.doumee.core.model.LoginUserInfo;
-import com.doumee.core.utils.Constants;
-import com.doumee.dao.system.dto.LoginPhoneDTO;
-import com.doumee.service.business.YwCustomerH5AuthService;
-import io.swagger.annotations.Api;
-import io.swagger.annotations.ApiOperation;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.validation.annotation.Validated;
-import org.springframework.web.bind.annotation.*;
-
-@Api(tags = "銆愬叕浼楀彿銆戝晢鎴风櫥褰�")
-@RestController
-@LoginNoRequired
-@RequestMapping(Constants.CLOUD_SERVICE_URL_INDEX + "/web/customer")
-public class YwCustomerWebController extends BaseController {
-
-    @Autowired
-    private YwCustomerH5AuthService ywCustomerH5AuthService;
-
-    @PreventRepeat
-    @ApiOperation("鍟嗘埛鐭俊楠岃瘉鐮佺櫥褰�")
-    @PostMapping("/loginByPhone")
-    public ApiResponse<String> loginByPhone(@Validated @RequestBody LoginPhoneDTO dto) {
-        try {
-            return ApiResponse.success(ywCustomerH5AuthService.loginByPhone(dto));
-        } catch (BusinessException e) {
-            return ApiResponse.failed(e.getCode(), e.getMessage());
-        } catch (Exception e) {
-            return ApiResponse.failed(ResponseStatus.SERVER_ERROR);
-        }
-    }
-
-    @ApiOperation("鑾峰彇褰撳墠鍟嗘埛鐧诲綍淇℃伅")
-    @GetMapping("/getUserInfo")
-    public ApiResponse<LoginUserInfo> getUserInfo(@RequestHeader(Constants.HEADER_USER_TOKEN) String token) {
-        LoginUserInfo user = this.getLoginUser(token);
-        if (user == null || !Constants.equalsInteger(user.getH5UserType(), LoginUserInfo.H5_USER_CUSTOMER)) {
-            return ApiResponse.failed("鐧诲綍宸插け鏁�");
-        }
-        return ApiResponse.success(ywCustomerH5AuthService.buildLoginUserInfo(user.getCustomerId()));
-    }
-}
diff --git a/server/visits/dmvisit_admin/src/main/resources/application.yml b/server/visits/dmvisit_admin/src/main/resources/application.yml
index 8a4ad1c..0417ffa 100644
--- a/server/visits/dmvisit_admin/src/main/resources/application.yml
+++ b/server/visits/dmvisit_admin/src/main/resources/application.yml
@@ -84,4 +84,10 @@
     pwdParamName: password  #鐢ㄦ埛鐧诲綍璁よ瘉瀵嗙爜鍙傛暟鍚嶇О
     useDefaultController: true # 鏄惁浣跨敤榛樿鐨凧wtAuthController
 
+# H5 寰俊 OAuth 寮�鍙戞ā鎷燂紙鐢熶骇鐜鍔″繀 mock-enabled: false锛�
+h5:
+  wechat:
+    mock-enabled: true
+    mock-openid: dev_h5_openid_merchant_001
+    mock-code: DEV_MOCK
 
diff --git a/server/visits/dmvisit_admin/src/main/resources/bootstrap.yml b/server/visits/dmvisit_admin/src/main/resources/bootstrap.yml
index 2df12af..9dc10e9 100644
--- a/server/visits/dmvisit_admin/src/main/resources/bootstrap.yml
+++ b/server/visits/dmvisit_admin/src/main/resources/bootstrap.yml
@@ -1,6 +1,6 @@
 spring:
   profiles:
-    active: pro
+    active: dev
   application:
     name: visitsAdmin
     # 瀹夊叏閰嶇疆
diff --git a/server/visits/dmvisit_service/src/main/java/com/doumee/core/haikang/model/HKConstants.java b/server/visits/dmvisit_service/src/main/java/com/doumee/core/haikang/model/HKConstants.java
index 532b31e..0b00076 100644
--- a/server/visits/dmvisit_service/src/main/java/com/doumee/core/haikang/model/HKConstants.java
+++ b/server/visits/dmvisit_service/src/main/java/com/doumee/core/haikang/model/HKConstants.java
@@ -1,7 +1,6 @@
 package com.doumee.core.haikang.model;
 
 import com.doumee.core.utils.Constants;
-import javafx.scene.effect.BlendMode;
 import lombok.Data;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
diff --git a/server/visits/dmvisit_service/src/main/java/com/doumee/core/wx/WxJsapiPayUtil.java b/server/visits/dmvisit_service/src/main/java/com/doumee/core/wx/WxJsapiPayUtil.java
new file mode 100644
index 0000000..5cbdae0
--- /dev/null
+++ b/server/visits/dmvisit_service/src/main/java/com/doumee/core/wx/WxJsapiPayUtil.java
@@ -0,0 +1,135 @@
+package com.doumee.core.wx;
+
+import cn.hutool.core.util.RandomUtil;
+import cn.hutool.core.util.XmlUtil;
+import cn.hutool.crypto.digest.DigestUtil;
+import cn.hutool.http.HttpUtil;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Component;
+
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.util.*;
+
+/**
+ * 寰俊鍏紬鍙� JSAPI 鏀粯锛圴2锛�
+ */
+@Component
+@Slf4j
+public class WxJsapiPayUtil {
+
+    private static final String UNIFIED_ORDER_URL = "https://api.mch.weixin.qq.com/pay/unifiedorder";
+    private static final String ORDER_QUERY_URL = "https://api.mch.weixin.qq.com/pay/orderquery";
+
+    @Value("${wx.pay.appId:}")
+    private String appId;
+    @Value("${wx.pay.mchId:}")
+    private String mchId;
+    @Value("${wx.pay.mchKey:}")
+    private String mchKey;
+    @Value("${wx.pay.notifyUrl:}")
+    private String notifyUrl;
+
+    public Map<String, String> createJsapiOrder(String orderNo, String openid, String body, BigDecimal amountYuan, String clientIp) {
+        int totalFee = amountYuan.multiply(BigDecimal.valueOf(100)).setScale(0, RoundingMode.HALF_UP).intValue();
+        Map<String, String> params = new TreeMap<>();
+        params.put("appid", appId);
+        params.put("mch_id", mchId);
+        params.put("nonce_str", RandomUtil.randomString(32));
+        params.put("body", StringUtils.defaultIfBlank(body, "鍟嗘埛缂磋垂"));
+        params.put("out_trade_no", orderNo);
+        params.put("total_fee", String.valueOf(totalFee));
+        params.put("spbill_create_ip", StringUtils.defaultIfBlank(clientIp, "127.0.0.1"));
+        params.put("notify_url", notifyUrl);
+        params.put("trade_type", "JSAPI");
+        params.put("openid", openid);
+        params.put("sign", sign(params));
+
+        String xml = mapToXml(params);
+        String respXml = HttpUtil.post(UNIFIED_ORDER_URL, xml);
+        Map<String, String> resp = xmlToMap(respXml);
+        if (!"SUCCESS".equals(resp.get("return_code")) || !"SUCCESS".equals(resp.get("result_code"))) {
+            throw new RuntimeException(StringUtils.defaultIfBlank(resp.get("err_code_des"), resp.get("return_msg")));
+        }
+        String prepayId = resp.get("prepay_id");
+        return buildJsapiPayParams(prepayId);
+    }
+
+    public Map<String, String> queryOrder(String orderNo) {
+        Map<String, String> params = new TreeMap<>();
+        params.put("appid", appId);
+        params.put("mch_id", mchId);
+        params.put("out_trade_no", orderNo);
+        params.put("nonce_str", RandomUtil.randomString(32));
+        params.put("sign", sign(params));
+        String respXml = HttpUtil.post(ORDER_QUERY_URL, mapToXml(params));
+        return xmlToMap(respXml);
+    }
+
+    public boolean verifyNotifySign(Map<String, String> data) {
+        if (data == null || !data.containsKey("sign")) {
+            return false;
+        }
+        String sign = data.get("sign");
+        Map<String, String> copy = new TreeMap<>(data);
+        copy.remove("sign");
+        return sign.equals(sign(copy));
+    }
+
+    public Map<String, String> parseNotifyXml(String xml) {
+        return xmlToMap(xml);
+    }
+
+    private Map<String, String> buildJsapiPayParams(String prepayId) {
+        Map<String, String> pay = new LinkedHashMap<>();
+        pay.put("appId", appId);
+        pay.put("timeStamp", String.valueOf(System.currentTimeMillis() / 1000));
+        pay.put("nonceStr", RandomUtil.randomString(32));
+        pay.put("package", "prepay_id=" + prepayId);
+        pay.put("signType", "MD5");
+        Map<String, String> signMap = new TreeMap<>();
+        signMap.put("appId", pay.get("appId"));
+        signMap.put("timeStamp", pay.get("timeStamp"));
+        signMap.put("nonceStr", pay.get("nonceStr"));
+        signMap.put("package", pay.get("package"));
+        signMap.put("signType", pay.get("signType"));
+        pay.put("paySign", sign(signMap));
+        return pay;
+    }
+
+    private String sign(Map<String, String> params) {
+        StringBuilder sb = new StringBuilder();
+        for (Map.Entry<String, String> e : params.entrySet()) {
+            if (StringUtils.isNotBlank(e.getValue())) {
+                sb.append(e.getKey()).append("=").append(e.getValue()).append("&");
+            }
+        }
+        sb.append("key=").append(mchKey);
+        return DigestUtil.md5Hex(sb.toString()).toUpperCase();
+    }
+
+    private String mapToXml(Map<String, String> params) {
+        StringBuilder sb = new StringBuilder("<xml>");
+        for (Map.Entry<String, String> e : params.entrySet()) {
+            sb.append("<").append(e.getKey()).append("><![CDATA[")
+                    .append(e.getValue()).append("]]></").append(e.getKey()).append(">");
+        }
+        sb.append("</xml>");
+        return sb.toString();
+    }
+
+    @SuppressWarnings("unchecked")
+    private Map<String, String> xmlToMap(String xml) {
+        Map<String, String> map = new HashMap<>();
+        if (StringUtils.isBlank(xml)) {
+            return map;
+        }
+        Map<String, Object> raw = XmlUtil.xmlToMap(xml);
+        for (Map.Entry<String, Object> e : raw.entrySet()) {
+            map.put(e.getKey(), e.getValue() == null ? null : String.valueOf(e.getValue()));
+        }
+        return map;
+    }
+}
diff --git a/server/visits/dmvisit_service/src/main/java/com/doumee/dao/business/YwH5BannerMapper.java b/server/visits/dmvisit_service/src/main/java/com/doumee/dao/business/YwH5BannerMapper.java
new file mode 100644
index 0000000..72e57d3
--- /dev/null
+++ b/server/visits/dmvisit_service/src/main/java/com/doumee/dao/business/YwH5BannerMapper.java
@@ -0,0 +1,7 @@
+package com.doumee.dao.business;
+
+import com.doumee.dao.business.model.YwH5Banner;
+import com.github.yulichang.base.MPJBaseMapper;
+
+public interface YwH5BannerMapper extends MPJBaseMapper<YwH5Banner> {
+}
diff --git a/server/visits/dmvisit_service/src/main/java/com/doumee/dao/business/YwWxPayOrderMapper.java b/server/visits/dmvisit_service/src/main/java/com/doumee/dao/business/YwWxPayOrderMapper.java
new file mode 100644
index 0000000..839a53f
--- /dev/null
+++ b/server/visits/dmvisit_service/src/main/java/com/doumee/dao/business/YwWxPayOrderMapper.java
@@ -0,0 +1,7 @@
+package com.doumee.dao.business;
+
+import com.doumee.dao.business.model.YwWxPayOrder;
+import com.github.yulichang.base.MPJBaseMapper;
+
+public interface YwWxPayOrderMapper extends MPJBaseMapper<YwWxPayOrder> {
+}
diff --git a/server/visits/dmvisit_service/src/main/java/com/doumee/dao/business/vo/EditRecordDataVO.java b/server/visits/dmvisit_service/src/main/java/com/doumee/dao/business/dto/EditRecordDataVO.java
similarity index 67%
rename from server/visits/dmvisit_service/src/main/java/com/doumee/dao/business/vo/EditRecordDataVO.java
rename to server/visits/dmvisit_service/src/main/java/com/doumee/dao/business/dto/EditRecordDataVO.java
index 8221220..de13c2c 100644
--- a/server/visits/dmvisit_service/src/main/java/com/doumee/dao/business/vo/EditRecordDataVO.java
+++ b/server/visits/dmvisit_service/src/main/java/com/doumee/dao/business/dto/EditRecordDataVO.java
@@ -1,17 +1,12 @@
-package com.doumee.dao.business.vo;
+package com.doumee.dao.business.dto;
 
-import com.doumee.dao.business.model.Approve;
 import io.swagger.annotations.ApiModelProperty;
 import lombok.Data;
 
 import java.util.Date;
-import java.util.List;
 
 /**
- * Created by IntelliJ IDEA.
- *
- * @Author : Rk
- * @create 2024/5/23 14:56
+ * 鎿嶄綔璁板綍
  */
 @Data
 public class EditRecordDataVO {
diff --git a/server/visits/dmvisit_service/src/main/java/com/doumee/dao/business/dto/YwCustomerRechargeRecordQueryDTO.java b/server/visits/dmvisit_service/src/main/java/com/doumee/dao/business/dto/YwCustomerRechargeRecordQueryDTO.java
index 0411ce7..21dfca4 100644
--- a/server/visits/dmvisit_service/src/main/java/com/doumee/dao/business/dto/YwCustomerRechargeRecordQueryDTO.java
+++ b/server/visits/dmvisit_service/src/main/java/com/doumee/dao/business/dto/YwCustomerRechargeRecordQueryDTO.java
@@ -10,6 +10,7 @@
 public class YwCustomerRechargeRecordQueryDTO {
 
     private String customerName;
+    private Integer customerId;
     private Integer type;
     private Integer status;
 
diff --git a/server/visits/dmvisit_service/src/main/java/com/doumee/dao/business/dto/h5/CustomerBillQueryDTO.java b/server/visits/dmvisit_service/src/main/java/com/doumee/dao/business/dto/h5/CustomerBillQueryDTO.java
new file mode 100644
index 0000000..2706afe
--- /dev/null
+++ b/server/visits/dmvisit_service/src/main/java/com/doumee/dao/business/dto/h5/CustomerBillQueryDTO.java
@@ -0,0 +1,8 @@
+package com.doumee.dao.business.dto.h5;
+
+import lombok.Data;
+
+@Data
+public class CustomerBillQueryDTO {
+    private Integer payTab;
+}
diff --git a/server/visits/dmvisit_service/src/main/java/com/doumee/dao/business/dto/h5/CustomerContractQueryDTO.java b/server/visits/dmvisit_service/src/main/java/com/doumee/dao/business/dto/h5/CustomerContractQueryDTO.java
new file mode 100644
index 0000000..1102ba7
--- /dev/null
+++ b/server/visits/dmvisit_service/src/main/java/com/doumee/dao/business/dto/h5/CustomerContractQueryDTO.java
@@ -0,0 +1,8 @@
+package com.doumee.dao.business.dto.h5;
+
+import lombok.Data;
+
+@Data
+public class CustomerContractQueryDTO {
+    private Integer status;
+}
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
new file mode 100644
index 0000000..9c3e768
--- /dev/null
+++ b/server/visits/dmvisit_service/src/main/java/com/doumee/dao/business/dto/h5/CustomerDeviceH5VO.java
@@ -0,0 +1,24 @@
+package com.doumee.dao.business.dto.h5;
+
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.util.Date;
+import java.util.List;
+
+@Data
+public class CustomerDeviceH5VO {
+    private Integer deviceType;
+    private Integer deviceId;
+    private String deviceName;
+    private String statusText;
+    private Integer statusCode;
+    private List<String> alarmTags;
+    private String roomInfo;
+    private List<String> roomList;
+    private String meterAccountNo;
+    private BigDecimal totalUsage;
+    private BigDecimal balance;
+    private Boolean balanceLow;
+    private Date updateTime;
+}
diff --git a/server/visits/dmvisit_service/src/main/java/com/doumee/dao/business/dto/h5/CustomerDeviceQueryDTO.java b/server/visits/dmvisit_service/src/main/java/com/doumee/dao/business/dto/h5/CustomerDeviceQueryDTO.java
new file mode 100644
index 0000000..40aab43
--- /dev/null
+++ b/server/visits/dmvisit_service/src/main/java/com/doumee/dao/business/dto/h5/CustomerDeviceQueryDTO.java
@@ -0,0 +1,9 @@
+package com.doumee.dao.business.dto.h5;
+
+import lombok.Data;
+
+@Data
+public class CustomerDeviceQueryDTO {
+    private Integer deviceType;
+    private Integer statusFilter;
+}
diff --git a/server/visits/dmvisit_service/src/main/java/com/doumee/dao/business/dto/h5/CustomerPayCreateDTO.java b/server/visits/dmvisit_service/src/main/java/com/doumee/dao/business/dto/h5/CustomerPayCreateDTO.java
new file mode 100644
index 0000000..cbf19d4
--- /dev/null
+++ b/server/visits/dmvisit_service/src/main/java/com/doumee/dao/business/dto/h5/CustomerPayCreateDTO.java
@@ -0,0 +1,16 @@
+package com.doumee.dao.business.dto.h5;
+
+import lombok.Data;
+
+import java.math.BigDecimal;
+
+@Data
+public class CustomerPayCreateDTO {
+    /** 0鐢佃〃 1绌鸿皟 2璐﹀崟 */
+    private Integer orderType;
+    private Integer electricalId;
+    private Integer billId;
+    private BigDecimal amount;
+    private String remark;
+    private String openid;
+}
diff --git a/server/visits/dmvisit_service/src/main/java/com/doumee/dao/business/dto/h5/CustomerRechargeRecordH5QueryDTO.java b/server/visits/dmvisit_service/src/main/java/com/doumee/dao/business/dto/h5/CustomerRechargeRecordH5QueryDTO.java
new file mode 100644
index 0000000..f1b5ac3
--- /dev/null
+++ b/server/visits/dmvisit_service/src/main/java/com/doumee/dao/business/dto/h5/CustomerRechargeRecordH5QueryDTO.java
@@ -0,0 +1,13 @@
+package com.doumee.dao.business.dto.h5;
+
+import lombok.Data;
+
+import java.util.Date;
+
+@Data
+public class CustomerRechargeRecordH5QueryDTO {
+    private Integer status;
+    private Date createTimeBegin;
+    private Date createTimeEnd;
+    private String month;
+}
diff --git a/server/visits/dmvisit_service/src/main/java/com/doumee/dao/business/model/YwContract.java b/server/visits/dmvisit_service/src/main/java/com/doumee/dao/business/model/YwContract.java
index e476672..a3872bb 100644
--- a/server/visits/dmvisit_service/src/main/java/com/doumee/dao/business/model/YwContract.java
+++ b/server/visits/dmvisit_service/src/main/java/com/doumee/dao/business/model/YwContract.java
@@ -310,6 +310,24 @@
     @TableField(exist = false)
     private YwContractBillDTO zlBillDTO;
 
+    @ApiModelProperty(value = "鎴块棿淇℃伅鎽樿锛圚5锛�")
+    @TableField(exist = false)
+    private String roomInfo;
 
+    @ApiModelProperty(value = "浠樻鏂瑰紡鏂囨锛圚5锛�")
+    @TableField(exist = false)
+    private String payTypeText;
+
+    @ApiModelProperty(value = "璐﹀崟鐘舵�佹彁绀猴紙H5锛�")
+    @TableField(exist = false)
+    private String billStatusTip;
+
+    @ApiModelProperty(value = "璐﹀崟鐘舵�佹彁绀虹被鍨� ok/warn/danger锛圚5锛�")
+    @TableField(exist = false)
+    private String billStatusType;
+
+    @ApiModelProperty(value = "鍏嶇鏈熸枃妗堬紙H5锛�")
+    @TableField(exist = false)
+    private String freeRentPeriod;
 
 }
diff --git a/server/visits/dmvisit_service/src/main/java/com/doumee/dao/business/model/YwContractBill.java b/server/visits/dmvisit_service/src/main/java/com/doumee/dao/business/model/YwContractBill.java
index dadcb8f..d39f034 100644
--- a/server/visits/dmvisit_service/src/main/java/com/doumee/dao/business/model/YwContractBill.java
+++ b/server/visits/dmvisit_service/src/main/java/com/doumee/dao/business/model/YwContractBill.java
@@ -179,6 +179,16 @@
     @TableField(exist = false)
     private String contractCode;
 
+    @ApiModelProperty(value = "鍚堝悓寮�濮嬫棩鏈�")
+    @TableField(exist = false)
+    @JsonFormat(pattern = "yyyy-MM-dd")
+    private Date contractStartDate;
+
+    @ApiModelProperty(value = "鍚堝悓缁撴潫鏃ユ湡")
+    @TableField(exist = false)
+    @JsonFormat(pattern = "yyyy-MM-dd")
+    private Date contractEndDate;
+
     @ApiModelProperty(value = "鍚堝悓鐘舵��", example = "1")
     @TableField(exist = false)
     private Integer contractStatus;
@@ -254,5 +264,12 @@
     @TableField(exist = false)
     private Integer wyPayType;
 
+    @ApiModelProperty(value = "鎴块棿淇℃伅鎽樿锛圚5锛�")
+    @TableField(exist = false)
+    private String roomInfo;
+
+    @ApiModelProperty(value = "鎴挎簮瀵硅薄闆嗗悎锛圚5锛�")
+    @TableField(exist = false)
+    private List<YwRoom> roomList;
 
 }
diff --git a/server/visits/dmvisit_service/src/main/java/com/doumee/dao/business/model/YwContractRevenue.java b/server/visits/dmvisit_service/src/main/java/com/doumee/dao/business/model/YwContractRevenue.java
index 70281cd..22574c0 100644
--- a/server/visits/dmvisit_service/src/main/java/com/doumee/dao/business/model/YwContractRevenue.java
+++ b/server/visits/dmvisit_service/src/main/java/com/doumee/dao/business/model/YwContractRevenue.java
@@ -2,9 +2,8 @@
 
 import com.baomidou.mybatisplus.annotation.TableField;
 import com.doumee.core.annotation.excel.ExcelColumn;
-import com.doumee.core.constants.OperaType;
 import com.doumee.core.model.LoginUserModel;
-import com.doumee.dao.business.vo.EditRecordDataVO;
+import com.doumee.dao.business.dto.EditRecordDataVO;
 import com.doumee.dao.system.model.Multifile;
 import io.swagger.annotations.ApiModel;
 import io.swagger.annotations.ApiModelProperty;
@@ -14,7 +13,6 @@
 import lombok.Data;
 import com.fasterxml.jackson.annotation.JsonFormat;
 
-import javax.validation.constraints.NotBlank;
 import java.util.Date;
 import java.math.BigDecimal;
 import java.util.List;
@@ -83,6 +81,9 @@
     @ApiModelProperty(value = "璐﹀崟涓婚敭锛堝叧鑱攜w_contract_bill锛�", example = "1")
     private Integer billId;
 
+    @ApiModelProperty(value = "寰俊鏀粯璁㈠崟鍙�")
+    private String wxOrderNo;
+
     @ApiModelProperty(value = "鏀舵敮绫诲瀷锛�0=鏀跺叆锛�1=鏀嚭", example = "1")
     @ExcelColumn(name="鏀舵敮绫诲瀷",index = 4,width = 10,valueMapping = "0=鏀跺叆;1=鏀嚭")
     private Integer revenueType;
diff --git a/server/visits/dmvisit_service/src/main/java/com/doumee/dao/business/model/YwCustomer.java b/server/visits/dmvisit_service/src/main/java/com/doumee/dao/business/model/YwCustomer.java
index 1fb1190..eab0b90 100644
--- a/server/visits/dmvisit_service/src/main/java/com/doumee/dao/business/model/YwCustomer.java
+++ b/server/visits/dmvisit_service/src/main/java/com/doumee/dao/business/model/YwCustomer.java
@@ -72,6 +72,9 @@
     @ApiModelProperty(value = "寰俊openid")
     private String openid;
 
+    @ApiModelProperty(value = "鏄惁宸插畬鎴愰娆″厖鍊煎埗 0鍚� 1鏄�")
+    private Integer firstRechargeDone;
+
     @ApiModelProperty(value = "韬唤璇佸彿锛堝姞瀵嗭級")
     @ExcelColumn(name="韬唤璇佸彿锛堝姞瀵嗭級")
     private String idcardNo;
diff --git a/server/visits/dmvisit_service/src/main/java/com/doumee/dao/business/model/YwCustomerElectrical.java b/server/visits/dmvisit_service/src/main/java/com/doumee/dao/business/model/YwCustomerElectrical.java
index 8772028..26dc2af 100644
--- a/server/visits/dmvisit_service/src/main/java/com/doumee/dao/business/model/YwCustomerElectrical.java
+++ b/server/visits/dmvisit_service/src/main/java/com/doumee/dao/business/model/YwCustomerElectrical.java
@@ -29,4 +29,10 @@
 
     @ApiModelProperty("鐢佃〃 ID")
     private Integer electricalId;
+
+    @ApiModelProperty("0鎵嬪姩 1鍚堝悓鑷姩")
+    private Integer bindSource;
+
+    @ApiModelProperty("鏉ユ簮鍚堝悓ID")
+    private Integer contractId;
 }
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 520a07b..7970889 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,8 @@
     @ApiModelProperty("瀹㈡埛涓婚敭锛堝叧鑱攜w_customer)")
     @ExcelColumn(name="瀹㈡埛涓婚敭锛堝叧鑱攜w_customer)",index=14 ,width=10)
     private Integer customerId;
+    @ApiModelProperty("寰俊鏀粯璁㈠崟鍙�")
+    private String wxOrderNo;
     @ApiModelProperty("鍏ヨ处鏃ユ湡")
     @ExcelColumn(name="鍏ヨ处鏃ユ湡",index=15 ,width=10)
     private Date incomeTime;
diff --git a/server/visits/dmvisit_service/src/main/java/com/doumee/dao/business/model/YwH5Banner.java b/server/visits/dmvisit_service/src/main/java/com/doumee/dao/business/model/YwH5Banner.java
new file mode 100644
index 0000000..73fa302
--- /dev/null
+++ b/server/visits/dmvisit_service/src/main/java/com/doumee/dao/business/model/YwH5Banner.java
@@ -0,0 +1,31 @@
+package com.doumee.dao.business.model;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import com.doumee.core.model.LoginUserModel;
+import io.swagger.annotations.ApiModel;
+import lombok.Data;
+
+import java.util.Date;
+
+@Data
+@ApiModel("H5杞挱鍥�")
+@TableName("yw_h5_banner")
+public class YwH5Banner extends LoginUserModel {
+
+    @TableId(type = IdType.AUTO)
+    private Integer id;
+    private Integer creator;
+    private Date createDate;
+    private Integer editor;
+    private Date editDate;
+    private Integer isdeleted;
+    private String remark;
+    private String title;
+    private String imageUrl;
+    private String linkUrl;
+    private Integer sortnum;
+    private Integer status;
+    private Integer scope;
+}
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
new file mode 100644
index 0000000..21687ad
--- /dev/null
+++ b/server/visits/dmvisit_service/src/main/java/com/doumee/dao/business/model/YwWxPayOrder.java
@@ -0,0 +1,54 @@
+package com.doumee.dao.business.model;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import com.doumee.core.model.LoginUserModel;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.util.Date;
+
+@Data
+@ApiModel("寰俊H5鏀粯璁㈠崟")
+@TableName("yw_wx_pay_order")
+public class YwWxPayOrder extends LoginUserModel {
+
+    @TableId(type = IdType.AUTO)
+    private Integer id;
+    private Integer creator;
+    private Date createDate;
+    private Integer editor;
+    private Date editDate;
+    private Integer isdeleted;
+    private String remark;
+
+    @ApiModelProperty("鍟嗘埛璁㈠崟鍙�")
+    private String orderNo;
+    @ApiModelProperty("浠樻鍟嗘埛")
+    private Integer customerId;
+    @ApiModelProperty("0鐢佃〃 1绌鸿皟 2璐﹀崟")
+    private Integer orderType;
+    @ApiModelProperty("涓氬姟寮曠敤ID")
+    private Integer bizRefId;
+    @ApiModelProperty("涓氬姟璁板綍ID")
+    private Integer bizRecordId;
+    private BigDecimal amount;
+    @ApiModelProperty("0寰呮敮浠� 1鎴愬姛 2澶辫触 3鍏抽棴")
+    private Integer status;
+    private String wxTransactionId;
+    private Date payTime;
+    private String openid;
+    private String requestSnapshot;
+    private String statusInfo;
+
+    public static final int TYPE_ELECTRICAL = 0;
+    public static final int TYPE_CONDITIONER = 1;
+    public static final int TYPE_BILL = 2;
+    public static final int STATUS_WAIT = 0;
+    public static final int STATUS_SUCCESS = 1;
+    public static final int STATUS_FAIL = 2;
+    public static final int STATUS_CLOSED = 3;
+}
diff --git a/server/visits/dmvisit_service/src/main/java/com/doumee/service/business/SmsEmailService.java b/server/visits/dmvisit_service/src/main/java/com/doumee/service/business/SmsEmailService.java
index 94f8efa..ce5be08 100644
--- a/server/visits/dmvisit_service/src/main/java/com/doumee/service/business/SmsEmailService.java
+++ b/server/visits/dmvisit_service/src/main/java/com/doumee/service/business/SmsEmailService.java
@@ -21,6 +21,9 @@
      */
     Integer create(SmsEmail smsEmail);
     Integer sendSms(SmsEmail smsEmail);
+
+    /** 鍟嗘埛 H5 鐧诲綍楠岃瘉鐮侊紙浠呮牎楠� yw_customer锛� */
+    Integer sendMerchantLoginSms(String phone);
     void validateCode(String code,String phone);
     /**
      * 涓婚敭鍒犻櫎
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
new file mode 100644
index 0000000..9b2dbfa
--- /dev/null
+++ b/server/visits/dmvisit_service/src/main/java/com/doumee/service/business/YwCustomerDeviceAutoBindService.java
@@ -0,0 +1,18 @@
+package com.doumee.service.business;
+
+import com.doumee.core.model.LoginUserInfo;
+
+/**
+ * 鏍规嵁鍟嗘埛绉熻祦鍚堝悓鑷姩鍏宠仈鐢佃〃/绌鸿皟璁惧
+ */
+public interface YwCustomerDeviceAutoBindService {
+
+    /** 鎸夊悎鍚屽悓姝ヨ澶囧叧鑱旓紙鍒涘缓/鐢熸晥鏃惰皟鐢級 */
+    void syncByContractId(Integer contractId, LoginUserInfo user);
+
+    /** 鎸夊晢鎴峰悓姝ユ墍鏈夋湁鏁堝悎鍚屼笅鐨勮澶� */
+    void syncByCustomerId(Integer customerId, LoginUserInfo user);
+
+    /** 鍚堝悓閫�绉�/鍒版湡鏃惰В闄よ嚜鍔ㄥ叧鑱� */
+    void unbindByContractId(Integer contractId, LoginUserInfo user);
+}
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 6800ebb..ca0cae6 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
@@ -12,4 +12,7 @@
     String loginByOpenId(String openId);
 
     LoginUserInfo buildLoginUserInfo(Integer customerId);
+
+    /** 鍙戦獙璇佺爜鍓嶆牎楠岋細鍟嗘埛鎵嬫満鍙烽』瀵瑰簲鏈夋晥 yw_customer锛堝惈鑱旂郴浜� member.phone锛� */
+    void assertActiveCustomerByPhone(String phone);
 }
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
new file mode 100644
index 0000000..30760de
--- /dev/null
+++ b/server/visits/dmvisit_service/src/main/java/com/doumee/service/business/YwCustomerH5BizService.java
@@ -0,0 +1,36 @@
+package com.doumee.service.business;
+
+import com.doumee.core.model.LoginUserInfo;
+import com.doumee.core.model.PageData;
+import com.doumee.core.model.PageWrap;
+import com.doumee.dao.business.dto.YwCustomerRechargeRecordVO;
+import com.doumee.dao.business.dto.h5.*;
+import com.doumee.dao.business.model.YwContract;
+import com.doumee.dao.business.model.YwContractBill;
+import com.doumee.dao.business.model.YwH5Banner;
+
+import java.util.List;
+import java.util.Map;
+
+public interface YwCustomerH5BizService {
+
+    List<YwH5Banner> listBanners();
+
+    Map<String, Object> home(Integer customerId);
+
+    PageData<CustomerDeviceH5VO> devicePage(PageWrap<CustomerDeviceQueryDTO> pageWrap, Integer customerId);
+
+    CustomerDeviceH5VO deviceDetail(Integer deviceType, Integer deviceId, Integer customerId);
+
+    PageData<YwCustomerRechargeRecordVO> rechargeRecordPage(PageWrap<CustomerRechargeRecordH5QueryDTO> pageWrap, Integer customerId);
+
+    PageData<YwContract> contractPage(PageWrap<CustomerContractQueryDTO> pageWrap, Integer customerId);
+
+    Map<String, Object> contractDetail(Integer contractId, Integer customerId, Integer billType);
+
+    PageData<YwContractBill> billPage(PageWrap<CustomerBillQueryDTO> pageWrap, Integer customerId);
+
+    Map<String, Object> billDetail(Integer billId, Integer customerId);
+
+    void applyFirstRechargeIfNeeded(Integer customerId, LoginUserInfo user);
+}
diff --git a/server/visits/dmvisit_service/src/main/java/com/doumee/service/business/YwCustomerWxPayService.java b/server/visits/dmvisit_service/src/main/java/com/doumee/service/business/YwCustomerWxPayService.java
new file mode 100644
index 0000000..27a2314
--- /dev/null
+++ b/server/visits/dmvisit_service/src/main/java/com/doumee/service/business/YwCustomerWxPayService.java
@@ -0,0 +1,16 @@
+package com.doumee.service.business;
+
+import com.doumee.core.model.LoginUserInfo;
+import com.doumee.dao.business.dto.h5.CustomerPayCreateDTO;
+import com.doumee.dao.business.model.YwWxPayOrder;
+
+import java.util.Map;
+
+public interface YwCustomerWxPayService {
+
+    Map<String, String> createOrder(CustomerPayCreateDTO dto, LoginUserInfo user, String clientIp);
+
+    YwWxPayOrder queryOrder(String orderNo, Integer customerId);
+
+    String handleNotify(String xmlBody);
+}
diff --git a/server/visits/dmvisit_service/src/main/java/com/doumee/service/business/YwH5BannerService.java b/server/visits/dmvisit_service/src/main/java/com/doumee/service/business/YwH5BannerService.java
new file mode 100644
index 0000000..73a72f3
--- /dev/null
+++ b/server/visits/dmvisit_service/src/main/java/com/doumee/service/business/YwH5BannerService.java
@@ -0,0 +1,25 @@
+package com.doumee.service.business;
+
+import com.doumee.core.model.LoginUserInfo;
+import com.doumee.core.model.PageData;
+import com.doumee.core.model.PageWrap;
+import com.doumee.dao.business.model.YwH5Banner;
+
+import java.util.List;
+
+public interface YwH5BannerService {
+
+    Integer create(YwH5Banner model);
+
+    void deleteById(Integer id, LoginUserInfo user);
+
+    void deleteByIdInBatch(List<Integer> ids, LoginUserInfo user);
+
+    void updateById(YwH5Banner model);
+
+    YwH5Banner findById(Integer id);
+
+    PageData<YwH5Banner> findPage(PageWrap<YwH5Banner> pageWrap);
+
+    List<YwH5Banner> listEnabledForCustomerWorkbench();
+}
diff --git a/server/visits/dmvisit_service/src/main/java/com/doumee/service/business/impl/MemberServiceImpl.java b/server/visits/dmvisit_service/src/main/java/com/doumee/service/business/impl/MemberServiceImpl.java
index d733728..563d197 100644
--- a/server/visits/dmvisit_service/src/main/java/com/doumee/service/business/impl/MemberServiceImpl.java
+++ b/server/visits/dmvisit_service/src/main/java/com/doumee/service/business/impl/MemberServiceImpl.java
@@ -135,7 +135,14 @@
     @Value("${debug_model}")
     private Boolean isDebug;
 
+    @Value("${h5.wechat.mock-enabled:false}")
+    private boolean h5WechatMockEnabled;
 
+    @Value("${h5.wechat.mock-openid:}")
+    private String h5WechatMockOpenid;
+
+    @Value("${h5.wechat.mock-code:DEV_MOCK}")
+    private String h5WechatMockCode;
 
     @Override
     @Transactional(rollbackFor = {BusinessException.class,Exception.class})
@@ -346,6 +353,7 @@
         if (StringUtils.isNotBlank(member.getIdcardNo()) && !IdcardUtil.isValidCard(member.getIdcardNo())){
             throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(),"韬唤璇佸彿鏍煎紡鏈夎");
         }
+        // 鍐呴儴鍛樺伐/鍙告満锛氫粎鍦� type=0,2 鑼冨洿鍐呭幓閲嶏紝涓嶄笌鍟嗘埛浜哄憳(type=3)浜掓枼
         if(StringUtils.isNotBlank(member.getIdcardNo())){
             if(memberMapper.selectCount(new QueryWrapper<Member>().lambda()
                     .in(Member::getType,new Integer[]{Constants.ZERO,Constants.TWO})
@@ -1785,6 +1793,9 @@
         if(StringUtils.isBlank(code)){
             throw new BusinessException(ResponseStatus.BAD_REQUEST);
         }
+        if (h5WechatMockEnabled && StringUtils.equals(code, h5WechatMockCode)) {
+            return buildYwWxAuthorizeVO(StringUtils.trimToEmpty(h5WechatMockOpenid), userType);
+        }
         String appId = systemDictDataBiz.queryByCode(Constants.WX_PLATFORM,Constants.WX_PLATFORM_APPID).getCode();
         String appSecret = systemDictDataBiz.queryByCode(Constants.WX_PLATFORM,Constants.WX_PLATFORM_SECRET).getCode();
         String getTokenUrl = WXConstant.GET_USER_INFO_URL.replace("CODE", code)
@@ -1793,14 +1804,18 @@
         JSONObject tokenJson = JSONObject.parseObject(HttpsUtil.get(getTokenUrl,true));
         log.error("=========================tokenJson=====================" + tokenJson);
         String openId = "";
-        WxAuthorizeVO wxAuthorizeVO = new WxAuthorizeVO();
         if(Objects.nonNull(tokenJson)&&!Objects.isNull(tokenJson.get("access_token"))){
             openId = tokenJson.getString("openid");
         }else{
             if(StringUtils.isBlank(openId)){
-                return wxAuthorizeVO;
+                return new WxAuthorizeVO();
             }
         }
+        return buildYwWxAuthorizeVO(openId, userType);
+    }
+
+    private WxAuthorizeVO buildYwWxAuthorizeVO(String openId, Integer userType) {
+        WxAuthorizeVO wxAuthorizeVO = new WxAuthorizeVO();
         wxAuthorizeVO.setOpenid(openId);
         if(Constants.equalsInteger(userType, LoginUserInfo.H5_USER_CUSTOMER)){
             String token = ywCustomerH5AuthService.loginByOpenId(openId);
@@ -1809,7 +1824,6 @@
             }
             return wxAuthorizeVO;
         }
-        //鏍规嵁openId 鏌ヨ杩愮淮鐢ㄦ埛淇℃伅
         SystemUser user = systemUserMapper.selectOne(new QueryWrapper<SystemUser>().lambda()
                 .eq(SystemUser::getOpenid,openId)
                 .eq(SystemUser::getDeleted,Boolean.FALSE)
@@ -2189,6 +2203,7 @@
         member.setIsdeleted(Constants.ZERO);
         member.setStatus(Constants.ZERO);
         this.checkYwMember(member);
+        applyYwMemberIdcard(member);
         memberMapper.insert(member);
         return member;
     }
@@ -2205,17 +2220,69 @@
         ){
             throw new BusinessException(ResponseStatus.BAD_REQUEST);
         }
+        Member existing = memberMapper.selectById(member.getId());
+        if (existing == null || Constants.equalsInteger(existing.getIsdeleted(), Constants.ONE)) {
+            throw new BusinessException(ResponseStatus.DATA_EMPTY.getCode(), "浜哄憳淇℃伅涓嶅瓨鍦紝璇峰埛鏂板悗閲嶈瘯");
+        }
         LoginUserInfo loginUserInfo = member.getLoginUserInfo();
         member.setEditor(loginUserInfo.getId());
         member.setCustomerId(null);
         member.setEditDate(new Date());
         member.setType(Constants.memberType.customer);
-        member.setStatus(Constants.ZERO);
-        member.setIsdeleted(Constants.ZERO);
-        member.setStatus(Constants.ZERO);
         this.checkYwMember(member);
+        if (StringUtils.isNotBlank(member.getIdcardNoNew())) {
+            applyYwMemberIdcardChange(member, existing);
+        } else {
+            member.setIdcardNo(existing.getIdcardNo());
+            member.setIdcardDecode(existing.getIdcardDecode());
+        }
+        member.setIdcardNoNew(null);
         memberMapper.updateById(member);
         return member;
+    }
+
+    /** 鏂板缓锛氭槑鏂囪瘉浠跺彿鍐欏叆 idcard_no(鍔犲瘑) + idcard_decode(鑴辨晱) */
+    private void applyYwMemberIdcard(Member member) {
+        if (StringUtils.isBlank(member.getIdcardNo())) {
+            return;
+        }
+        String plain = member.getIdcardNo().trim();
+        if (Constants.equalsInteger(member.getIdcardType(), Constants.ZERO)) {
+            member.setSex(Constants.getSexByCardNo(plain));
+            member.setBirthday(DateUtil.fromStringToDate("yyyyMMdd", IdcardUtil.getBirthByIdCard(plain)));
+        }
+        member.setIdcardDecode(Constants.getTuominStr(plain));
+        member.setIdcardNo(DESUtil.encrypt(Constants.EDS_PWD, plain));
+    }
+
+    /** 缂栬緫锛氶�氳繃 idcardNoNew 鍙樻洿璇佷欢鍙� */
+    private void applyYwMemberIdcardChange(Member member, Member existing) {
+        String plain = member.getIdcardNoNew().trim();
+        String encrypted = DESUtil.encrypt(Constants.EDS_PWD, plain);
+        if (StringUtils.equals(existing.getIdcardNo(), encrypted)) {
+            return;
+        }
+        if (Constants.equalsInteger(member.getIdcardType(), Constants.ZERO)) {
+            member.setSex(Constants.getSexByCardNo(plain));
+            member.setBirthday(DateUtil.fromStringToDate("yyyyMMdd", IdcardUtil.getBirthByIdCard(plain)));
+        }
+        member.setIdcardDecode(Constants.getTuominStr(plain));
+        member.setIdcardNo(encrypted);
+    }
+
+    /** 鍒楄〃灞曠ず锛氳ˉ鍏� idcard_decode */
+    private void fillMemberIdcardDecode(Member member) {
+        if (StringUtils.isNotBlank(member.getIdcardDecode()) || StringUtils.isBlank(member.getIdcardNo())) {
+            return;
+        }
+        try {
+            String plain = DESUtil.decrypt(Constants.EDS_PWD, member.getIdcardNo());
+            if (StringUtils.isNotBlank(plain)) {
+                member.setIdcardDecode(Constants.getTuominStr(plain));
+            }
+        } catch (Exception e) {
+            member.setIdcardDecode(Constants.getTuominStr(member.getIdcardNo()));
+        }
     }
 
 
@@ -2232,27 +2299,41 @@
     }
 
 
-    public void checkYwMember(Member member){
-        if (StringUtils.isBlank(member.getPhone())||!PhoneUtil.isPhone(member.getPhone())){
-            throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(),"鐢佃瘽鍙风爜鏍煎紡鏈夎");
+    public void checkYwMember(Member member) {
+        if (StringUtils.isBlank(member.getPhone()) || !PhoneUtil.isPhone(member.getPhone())) {
+            throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "鐢佃瘽鍙风爜鏍煎紡鏈夎");
         }
-        if (StringUtils.isNotBlank(member.getIdcardNo()) && Constants.equalsInteger(member.getIdcardType(),Constants.ZERO)  && !IdcardUtil.isValidCard(member.getIdcardNo())){
-            throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(),"韬唤璇佸彿鏍煎紡鏈夎");
+        if (memberMapper.selectCount(new QueryWrapper<Member>().lambda()
+                .ne(Objects.nonNull(member.getId()), Member::getId, member.getId())
+                .eq(Member::getPhone, member.getPhone())
+                .eq(Member::getType, Constants.memberType.customer)
+                .eq(Member::getIsdeleted, Constants.ZERO)) > Constants.ZERO) {
+            throw new BusinessException(ResponseStatus.DATA_EXISTS.getCode(), "鎵嬫満鍙枫��" + member.getPhone() + "銆戝凡琚娇鐢紝涓嶈兘閲嶅");
         }
-        if(StringUtils.isNotBlank(member.getIdcardNo() ) && Constants.equalsInteger(member.getIdcardType(),Constants.ZERO) ){
-            if(memberMapper.selectCount(new QueryWrapper<Member>().lambda()
-                            .ne(Objects.nonNull(member.getId()),Member::getId,member.getId())
-                    .eq(Member::getIdcardNo, DESUtil.encrypt(Constants.EDS_PWD, member.getIdcardNo()))
-                    .eq(Member::getIsdeleted,Constants.ZERO)) >0){
-                throw new BusinessException(ResponseStatus.DATA_EXISTS.getCode(), "韬唤璇佸彿銆�"+member.getIdcardNo()+"銆戝凡琚娇鐢紝涓嶈兘閲嶅");
+        String plainIdcard = resolveYwPlainIdcard(member);
+        if (StringUtils.isNotBlank(plainIdcard)) {
+            if (Constants.equalsInteger(member.getIdcardType(), Constants.ZERO) && !IdcardUtil.isValidCard(plainIdcard)) {
+                throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "韬唤璇佸彿鏍煎紡鏈夎");
+            }
+            String encrypted = DESUtil.encrypt(Constants.EDS_PWD, plainIdcard);
+            if (memberMapper.selectCount(new QueryWrapper<Member>().lambda()
+                    .ne(Objects.nonNull(member.getId()), Member::getId, member.getId())
+                    .eq(Member::getType, Constants.memberType.customer)
+                    .eq(Member::getIsdeleted, Constants.ZERO)
+                    .eq(Member::getIdcardNo, encrypted)) > Constants.ZERO) {
+                throw new BusinessException(ResponseStatus.DATA_EXISTS.getCode(), "璇佷欢鍙枫��" + plainIdcard + "銆戝凡琚娇鐢紝涓嶈兘閲嶅");
             }
         }
-//        if(memberMapper.selectCount(new QueryWrapper<Member>().lambda()
-//                .ne(Objects.nonNull(member.getId()),Member::getId,member.getId())
-//                .eq(Member::getPhone,  member.getPhone())
-//                .eq(Member::getIsdeleted,Constants.ZERO) ) >0){
-//            throw new BusinessException(ResponseStatus.DATA_EXISTS.getCode(), "鎵嬫満鍙枫��"+member.getPhone()+"銆戝凡琚娇鐢紝涓嶈兘閲嶅");
-//        }
+    }
+
+    private String resolveYwPlainIdcard(Member member) {
+        if (StringUtils.isNotBlank(member.getIdcardNoNew())) {
+            return member.getIdcardNoNew().trim();
+        }
+        if (member.getId() == null && StringUtils.isNotBlank(member.getIdcardNo())) {
+            return member.getIdcardNo().trim();
+        }
+        return null;
     }
 
 
@@ -2271,10 +2352,21 @@
                 .eq(Objects.nonNull(model)&&Objects.nonNull(model.getCustomerId()),Member::getCustomerId,model.getCustomerId())
                 .and(Objects.nonNull(model)&&StringUtils.isNotBlank(model.getName()),i->i.like(Member::getName,model.getName()).or().like(
                         Member::getPhone,model.getName()
-                ))
+                ).or().like(Member::getIdcardDecode, model.getName()))
+                .and(Objects.nonNull(model) && StringUtils.isNotBlank(model.getIdcardNo()), w -> {
+                    String idcard = model.getIdcardNo().trim();
+                    w.eq(Member::getIdcardNo, DESUtil.encrypt(Constants.EDS_PWD, idcard))
+                            .or().like(Member::getIdcardDecode, idcard);
+                })
                 .orderByDesc(Member::getCreateDate)
         );
-        return PageData.from(iPage);
+        PageData<Member> pageData = PageData.from(iPage);
+        if (pageData.getRecords() != null) {
+            for (Member item : pageData.getRecords()) {
+                fillMemberIdcardDecode(item);
+            }
+        }
+        return pageData;
     }
 
 }
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 a50a208..4129a20 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
@@ -8,6 +8,7 @@
 import com.doumee.biz.system.SystemDictDataBiz;
 import com.doumee.core.constants.ResponseStatus;
 import com.doumee.core.exception.BusinessException;
+import com.doumee.core.model.LoginUserInfo;
 import com.doumee.core.model.PageData;
 import com.doumee.core.model.PageWrap;
 import com.doumee.core.utils.*;
@@ -16,6 +17,7 @@
 import com.doumee.dao.business.dao.SmsEmailMapper;
 import com.doumee.dao.business.model.*;
 import com.doumee.dao.system.SystemUserMapper;
+import com.doumee.dao.system.model.SystemDictData;
 import com.doumee.dao.system.model.SystemUser;
 import com.doumee.service.business.SmsEmailService;
 import com.doumee.service.business.third.EmailService;
@@ -49,6 +51,10 @@
     private SmsConfigMapper smsConfigMapper;
     @Autowired
     private SystemUserMapper systemUserMapper;
+    @Autowired
+    private YwCustomerMapper ywCustomerMapper;
+    @Autowired
+    private MemberMapper memberMapper;
 
     @Value("${debug_model}")
     private boolean debugModel;
@@ -87,44 +93,117 @@
     }
 
     @Override
-    public Integer sendSms(SmsEmail smsEmail) {
-        if(StringUtils.isBlank(smsEmail.getPhone())){
-            throw  new BusinessException(ResponseStatus.BAD_REQUEST);
+    public Integer sendMerchantLoginSms(String phone) {
+        if (StringUtils.isBlank(phone)) {
+            throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "鎵嬫満鍙蜂笉鑳戒负绌�");
         }
-        //鏍规嵁鎵嬫満鍙锋煡璇㈢敤鎴�
-        if(systemUserMapper.selectCount(new QueryWrapper<SystemUser>().lambda().eq(SystemUser::getMobile,smsEmail.getPhone()))==Constants.ZERO){
-            throw  new BusinessException(ResponseStatus.SERVER_ERROR.getCode(),"瀵逛笉璧凤紝鎵嬫満鍙锋棤鏁堣妫�鏌ュ悗閲嶈瘯!");
-        };
-        String nowDate = DateUtil.getFomartDate(new Date(),"yyyy-MM-dd HH:mm:ss");
-        if(smsEmailMapper.selectCount(new QueryWrapper<SmsEmail>().lambda()
-                .eq(SmsEmail::getPhone,smsEmail.getPhone())
-                .eq(SmsEmail::getType,Constants.ZERO)
-                .between(SmsEmail::getCreateDate, DateUtil.getFomartDate(DateUtil.afterMinutesDate(-5),"yyyy-MM-dd HH:mm:ss"),nowDate)
-        )>=3){
-            throw  new BusinessException(ResponseStatus.SERVER_ERROR.getCode(),"瀵逛笉璧凤紝瓒呭嚭鍙戦�佹鏁帮紝璇风◢鍚庨噸璇曪紒");
+        return doSendH5LoginSms(phone.trim(), LoginUserInfo.H5_USER_CUSTOMER);
+    }
+
+    @Override
+    public Integer sendSms(SmsEmail smsEmail) {
+        if (StringUtils.isBlank(smsEmail.getPhone())) {
+            throw new BusinessException(ResponseStatus.BAD_REQUEST);
+        }
+        return doSendH5LoginSms(smsEmail.getPhone().trim(), LoginUserInfo.H5_USER_OPS);
+    }
+
+    private Integer doSendH5LoginSms(String phone, Integer h5UserType) {
+        assertH5LoginAccountExists(phone, h5UserType);
+        String nowDate = DateUtil.getFomartDate(new Date(), "yyyy-MM-dd HH:mm:ss");
+        if (smsEmailMapper.selectCount(new QueryWrapper<SmsEmail>().lambda()
+                .eq(SmsEmail::getPhone, phone)
+                .eq(SmsEmail::getType, Constants.ZERO)
+                .between(SmsEmail::getCreateDate, DateUtil.getFomartDate(DateUtil.afterMinutesDate(-5), "yyyy-MM-dd HH:mm:ss"), nowDate)
+        ) >= 3) {
+            throw new BusinessException(ResponseStatus.SERVER_ERROR.getCode(), "瀵逛笉璧凤紝瓒呭嚭鍙戦�佹鏁帮紝璇风◢鍚庨噸璇曪紒");
         }
         String code = Constants.getRandom6Num();
         SmsConfig smsConfig = smsConfigMapper.selectOne(new QueryWrapper<SmsConfig>().lambda().eq(SmsConfig::getObjType,
                 SmsConstants.inventCode).last(" limit 1 "));
-        String comName = systemDictDataBiz.queryByCode(Constants.SMS,Constants.SMS_COMNAME).getCode();
-        //寮�鍚煭淇¢�氱煡
-        if(Objects.nonNull(smsConfig) || Constants.equalsInteger(smsConfig.getStatus(),Constants.ZERO)){
-            if(StringUtils.isNotBlank(smsConfig.getContent())){
-                String content  = comName + smsConfig.getContent().replace("{楠岃瘉鐮亇",code);
-                emayService.sendSingleSms(smsEmail.getPhone(),content);
-                smsEmail.setRemark(code);
-                smsEmail.setIsdeleted(Constants.ZERO);
-                smsEmail.setCreateDate(new Date());
-                smsEmail.setStatus(Constants.ZERO);
-                smsEmail.setType(Constants.ZERO);
-                smsEmail.setTitle("鐭俊楠岃瘉鐮�");
-                smsEmail.setContent(content);
-                smsEmail.setObjType(Constants.ZERO+"");
-                smsEmailMapper.insert(smsEmail);
-                return smsEmail.getId();
-            }
+        SystemDictData comNameDict = systemDictDataBiz.queryByCode(Constants.SMS, Constants.SMS_COMNAME);
+        if (comNameDict == null || StringUtils.isBlank(comNameDict.getCode())) {
+            throw new BusinessException(ResponseStatus.SERVER_ERROR.getCode(), "鐭俊绛惧悕鏈厤缃紝璇疯仈绯荤鐞嗗憳");
         }
-        return null;
+        String comName = comNameDict.getCode();
+        if (smsConfig == null || StringUtils.isBlank(smsConfig.getContent())) {
+            throw new BusinessException(ResponseStatus.SERVER_ERROR.getCode(), "鐭俊鏈嶅姟鏈厤缃紝璇疯仈绯荤鐞嗗憳");
+        }
+        if (!Constants.equalsInteger(smsConfig.getStatus(), Constants.ZERO)) {
+            throw new BusinessException(ResponseStatus.SERVER_ERROR.getCode(), "鐭俊鏈嶅姟鏈紑鍚紝璇疯仈绯荤鐞嗗憳");
+        }
+        String content = comName + smsConfig.getContent().replace("{楠岃瘉鐮亇", code);
+        if (!emayService.sendSingleSms(phone, content)) {
+            throw new BusinessException(ResponseStatus.SERVER_ERROR.getCode(), "鐭俊鍙戦�佸け璐ワ紝璇风◢鍚庨噸璇�");
+        }
+        SmsEmail record = new SmsEmail();
+        record.setPhone(phone);
+        record.setRemark(code);
+        record.setIsdeleted(Constants.ZERO);
+        record.setCreateDate(new Date());
+        record.setStatus(Constants.ZERO);
+        record.setType(Constants.ZERO);
+        record.setTitle("鐭俊楠岃瘉鐮�");
+        record.setContent(content);
+        record.setObjType(Constants.ZERO + "");
+        smsEmailMapper.insert(record);
+        return record.getId();
+    }
+
+    /**
+     * H5 鐧诲綍鍙戠煭淇″墠鏍¢獙锛氬晢鎴锋煡 yw_customer锛岃繍缁存煡 system_user
+     */
+    private void assertH5LoginAccountExists(String phone, Integer h5UserType) {
+        if (Constants.equalsInteger(h5UserType, LoginUserInfo.H5_USER_CUSTOMER)) {
+            assertMerchantAccountExists(phone);
+            return;
+        }
+        if (systemUserMapper.selectCount(new QueryWrapper<SystemUser>().lambda()
+                .eq(SystemUser::getMobile, phone)
+                .eq(SystemUser::getDeleted, Boolean.FALSE)) == Constants.ZERO) {
+            throw new BusinessException(ResponseStatus.SERVER_ERROR.getCode(), "瀵逛笉璧凤紝鎵嬫満鍙锋棤鏁堣妫�鏌ュ悗閲嶈瘯!");
+        }
+    }
+
+    private void assertMerchantAccountExists(String phone) {
+        YwCustomer customer = findMerchantByPhone(phone);
+        if (customer == null) {
+            throw new BusinessException(ResponseStatus.ACCOUNT_INCORRECT.getCode(), "鍟嗘埛涓嶅瓨鍦ㄦ垨鏈敞鍐�");
+        }
+        if (customer.getStatus() != null && Constants.equalsInteger(customer.getStatus(), Constants.ONE)) {
+            throw new BusinessException(ResponseStatus.NO_ALLOW_LOGIN.getCode(), "鍟嗘埛璐﹀彿宸茬鐢�");
+        }
+    }
+
+    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)
+                .eq(Member::getPhone, phone)
+                .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;
+        }
+        return ywCustomerMapper.selectOne(new QueryWrapper<YwCustomer>().lambda()
+                .eq(YwCustomer::getIsdeleted, Constants.ZERO)
+                .eq(YwCustomer::getMemberId, member.getId())
+                .last(" limit 1 "));
     }
 
 
diff --git a/server/visits/dmvisit_service/src/main/java/com/doumee/service/business/impl/YwContractRevenueServiceImpl.java b/server/visits/dmvisit_service/src/main/java/com/doumee/service/business/impl/YwContractRevenueServiceImpl.java
index a462929..c1012e9 100644
--- a/server/visits/dmvisit_service/src/main/java/com/doumee/service/business/impl/YwContractRevenueServiceImpl.java
+++ b/server/visits/dmvisit_service/src/main/java/com/doumee/service/business/impl/YwContractRevenueServiceImpl.java
@@ -12,7 +12,7 @@
 import com.doumee.dao.business.*;
 import com.doumee.dao.business.dao.CompanyMapper;
 import com.doumee.dao.business.model.*;
-import com.doumee.dao.business.vo.EditRecordDataVO;
+import com.doumee.dao.business.dto.EditRecordDataVO;
 import com.doumee.dao.system.MultifileMapper;
 import com.doumee.dao.system.SystemUserMapper;
 import com.doumee.dao.system.model.Multifile;
@@ -24,7 +24,6 @@
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.github.yulichang.wrapper.MPJLambdaWrapper;
 import org.apache.commons.lang3.StringUtils;
-import org.checkerframework.checker.units.qual.C;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
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 383ca4c..20689e4 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
@@ -19,6 +19,7 @@
 import com.doumee.dao.system.model.Multifile;
 import com.doumee.dao.system.model.SystemUser;
 import com.doumee.service.business.YwContractService;
+import com.doumee.service.business.YwCustomerDeviceAutoBindService;
 import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
 import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
 import com.baomidou.mybatisplus.core.metadata.IPage;
@@ -73,6 +74,8 @@
 
     @Autowired
     private SystemDictDataBiz systemDictDataBiz;
+    @Autowired
+    private YwCustomerDeviceAutoBindService ywCustomerDeviceAutoBindService;
     @Override
     @Transactional(rollbackFor = {BusinessException.class,Exception.class})
     public Integer create(YwContract model) {
@@ -94,6 +97,9 @@
         dealDetailListBiz(model,false);//澶勭悊鏉℃淇℃伅
         dealMultifileBiz(model);//澶勭悊闄勪欢淇℃伅
         dealRoomsForContract(model);//澶勭悊鎴挎簮鍏宠仈琛�
+        if (Constants.equalsInteger(model.getStatus(), Constants.ONE) && model.getRenterId() != null) {
+            ywCustomerDeviceAutoBindService.syncByContractId(model.getId(), model.getLoginUserInfo());
+        }
         dealLogBiz(model,Constants.YwLogType.CONTRACT_CREATE,model.getLoginUserInfo().getRealname(),"銆�"+model.getRemark().replace("鍚堝悓鎽樿锛�","")+"銆�");//璁板綍鏂板缓鏃ュ織
         return model.getId();
     }
@@ -417,6 +423,14 @@
                         .in(YwRoom::getId,contractRoomList.stream().map(i->i.getRoomId()).collect(Collectors.toList()))
                 );
             }
+            LoginUserInfo timerUser = new LoginUserInfo();
+            timerUser.setId(1);
+            timerUser.setRealname("timer");
+            for (YwContract c : listA) {
+                if (c.getRenterId() != null) {
+                    ywCustomerDeviceAutoBindService.syncByContractId(c.getId(), timerUser);
+                }
+            }
         }
 
     }
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
new file mode 100644
index 0000000..07a3d71
--- /dev/null
+++ b/server/visits/dmvisit_service/src/main/java/com/doumee/service/business/impl/YwCustomerDeviceAutoBindServiceImpl.java
@@ -0,0 +1,218 @@
+package com.doumee.service.business.impl;
+
+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.dao.business.*;
+import com.doumee.dao.business.dto.YwCustomerGsConfigDTO;
+import com.doumee.dao.business.model.*;
+import com.doumee.service.business.YwCustomerDeviceAutoBindService;
+import com.doumee.service.business.YwCustomerRechargeBizService;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.util.CollectionUtils;
+
+import java.math.BigDecimal;
+import java.util.*;
+import java.util.stream.Collectors;
+
+@Service
+@Slf4j
+public class YwCustomerDeviceAutoBindServiceImpl implements YwCustomerDeviceAutoBindService {
+
+    private static final int BIND_SOURCE_CONTRACT = 1;
+
+    @Autowired
+    private YwContractMapper ywContractMapper;
+    @Autowired
+    private YwContractRoomMapper ywContractRoomMapper;
+    @Autowired
+    private YwElectricalRoomMapper ywElectricalRoomMapper;
+    @Autowired
+    private YwCustomerElectricalMapper ywCustomerElectricalMapper;
+    @Autowired
+    private YwCustomerConditionerMapper ywCustomerConditionerMapper;
+    @Autowired
+    private YwConditionerMapper ywConditionerMapper;
+    @Autowired
+    private YwCustomerRechargeBizService ywCustomerRechargeBizService;
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void syncByContractId(Integer contractId, LoginUserInfo user) {
+        if (contractId == null) {
+            return;
+        }
+        YwContract contract = ywContractMapper.selectById(contractId);
+        if (contract == null || Objects.equals(contract.getIsdeleted(), Constants.ONE)) {
+            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);
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void syncByCustomerId(Integer customerId, LoginUserInfo user) {
+        if (customerId == null) {
+            return;
+        }
+        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);
+            }
+        }
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void unbindByContractId(Integer contractId, LoginUserInfo user) {
+        if (contractId == null) {
+            return;
+        }
+        Date now = new Date();
+        Integer editor = user != null ? user.getId() : null;
+        ywCustomerElectricalMapper.update(null, new UpdateWrapper<YwCustomerElectrical>().lambda()
+                .set(YwCustomerElectrical::getIsdeleted, Constants.ONE)
+                .set(YwCustomerElectrical::getEditDate, now)
+                .set(YwCustomerElectrical::getEditor, editor)
+                .eq(YwCustomerElectrical::getContractId, contractId)
+                .eq(YwCustomerElectrical::getBindSource, BIND_SOURCE_CONTRACT)
+                .eq(YwCustomerElectrical::getIsdeleted, Constants.ZERO));
+    }
+
+    private boolean isActiveContract(YwContract contract) {
+        if (contract.getStartDate() == null || contract.getEndDate() == null) {
+            return false;
+        }
+        long now = System.currentTimeMillis();
+        return contract.getStartDate().getTime() <= now
+                && contract.getEndDate().getTime() >= now
+                && !Objects.equals(contract.getStatus(), Constants.FOUR);
+    }
+
+    private List<Integer> listContractRoomIds(Integer contractId) {
+        return ywContractRoomMapper.selectList(new QueryWrapper<YwContractRoom>().lambda()
+                        .eq(YwContractRoom::getContractId, contractId)
+                        .eq(YwContractRoom::getType, Constants.ZERO)
+                        .eq(YwContractRoom::getIsdeleted, Constants.ZERO))
+                .stream().map(YwContractRoom::getRoomId).filter(Objects::nonNull).distinct()
+                .collect(Collectors.toList());
+    }
+
+    private void bindElectricals(YwContract contract, List<Integer> roomIds, LoginUserInfo user) {
+        List<YwElectricalRoom> relRooms = ywElectricalRoomMapper.selectList(new QueryWrapper<YwElectricalRoom>().lambda()
+                .in(YwElectricalRoom::getRoomId, roomIds)
+                .eq(YwElectricalRoom::getType, Constants.ZERO)
+                .eq(YwElectricalRoom::getIsdeleted, Constants.ZERO));
+        Set<Integer> electricalIds = relRooms.stream().map(YwElectricalRoom::getObjId)
+                .filter(Objects::nonNull).collect(Collectors.toCollection(LinkedHashSet::new));
+        if (electricalIds.isEmpty()) {
+            return;
+        }
+        Set<Integer> boundOthers = listBoundElectricalIdsExcept(contract.getRenterId());
+        Date now = new Date();
+        for (Integer eid : electricalIds) {
+            if (boundOthers.contains(eid)) {
+                log.warn("skip electrical {} already bound to other customer", eid);
+                continue;
+            }
+            YwCustomerElectrical exist = ywCustomerElectricalMapper.selectOne(new QueryWrapper<YwCustomerElectrical>().lambda()
+                    .eq(YwCustomerElectrical::getCustomerId, contract.getRenterId())
+                    .eq(YwCustomerElectrical::getElectricalId, eid)
+                    .eq(YwCustomerElectrical::getIsdeleted, Constants.ZERO)
+                    .last("limit 1"));
+            if (exist != null) {
+                if (exist.getContractId() == null) {
+                    exist.setContractId(contract.getId());
+                    exist.setBindSource(BIND_SOURCE_CONTRACT);
+                    exist.setEditDate(now);
+                    exist.setEditor(user != null ? user.getId() : null);
+                    ywCustomerElectricalMapper.updateById(exist);
+                }
+                continue;
+            }
+            YwCustomerElectrical rel = new YwCustomerElectrical();
+            rel.setCreator(user != null ? user.getId() : null);
+            rel.setCreateDate(now);
+            rel.setEditor(user != null ? user.getId() : null);
+            rel.setEditDate(now);
+            rel.setIsdeleted(Constants.ZERO);
+            rel.setCustomerId(contract.getRenterId());
+            rel.setElectricalId(eid);
+            rel.setBindSource(BIND_SOURCE_CONTRACT);
+            rel.setContractId(contract.getId());
+            ywCustomerElectricalMapper.insert(rel);
+        }
+    }
+
+    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);
+        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()) {
+            return;
+        }
+        YwCustomerGsConfigDTO dto = new YwCustomerGsConfigDTO();
+        dto.setCustomerId(contract.getRenterId());
+        dto.setIsPwr(Constants.ONE);
+        dto.setIsRestStop(Constants.ZERO);
+        dto.setGsBz("鍚堝悓鑷姩鍏宠仈");
+        dto.setStopMoney(BigDecimal.ZERO);
+        List<YwCustomerGsConfigDTO.ConditionerItem> items = new ArrayList<>();
+        for (Integer cid : conditionerIds) {
+            YwCustomerGsConfigDTO.ConditionerItem item = new YwCustomerGsConfigDTO.ConditionerItem();
+            item.setConditionerId(cid);
+            item.setDevRatio(100);
+            items.add(item);
+        }
+        dto.setConditioners(items);
+        try {
+            ywCustomerRechargeBizService.saveCustomerGsConfig(dto, user != null ? user : systemUser());
+        } catch (BusinessException e) {
+            log.warn("auto bind conditioner GS failed contractId={}: {}", contract.getId(), e.getMessage());
+        }
+    }
+
+    private Set<Integer> listBoundElectricalIdsExcept(Integer customerId) {
+        List<YwCustomerElectrical> list = ywCustomerElectricalMapper.selectList(new QueryWrapper<YwCustomerElectrical>().lambda()
+                .eq(YwCustomerElectrical::getIsdeleted, Constants.ZERO)
+                .ne(customerId != null, YwCustomerElectrical::getCustomerId, customerId));
+        return list.stream().map(YwCustomerElectrical::getElectricalId).collect(Collectors.toSet());
+    }
+
+    private LoginUserInfo systemUser() {
+        LoginUserInfo u = new LoginUserInfo();
+        u.setId(1);
+        u.setRealname("system");
+        return u;
+    }
+}
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 37fcf42..59d02f7 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
@@ -7,7 +7,9 @@
 import com.doumee.core.exception.BusinessException;
 import com.doumee.core.model.LoginUserInfo;
 import com.doumee.core.utils.Constants;
+import com.doumee.dao.business.MemberMapper;
 import com.doumee.dao.business.YwCustomerMapper;
+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.YwCustomerH5AuthService;
@@ -24,6 +26,8 @@
 
     @Autowired
     private YwCustomerMapper ywCustomerMapper;
+    @Autowired
+    private MemberMapper memberMapper;
     @Autowired
     private JwtTokenUtil jwtTokenUtil;
 
@@ -67,19 +71,55 @@
         return toLoginUserInfo(requireActiveCustomer(customerId));
     }
 
+    @Override
+    public void assertActiveCustomerByPhone(String phone) {
+        findActiveByPhone(phone);
+    }
+
     private YwCustomer findActiveByPhone(String phone) {
         if (StringUtils.isBlank(phone)) {
             throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "鎵嬫満鍙蜂笉鑳戒负绌�");
         }
-        YwCustomer customer = ywCustomerMapper.selectOne(new QueryWrapper<YwCustomer>().lambda()
-                .eq(YwCustomer::getIsdeleted, Constants.ZERO)
-                .eq(YwCustomer::getPhone, phone.trim())
-                .last(" limit 1 "));
+        YwCustomer customer = findCustomerByPhone(phone.trim());
         if (customer == null) {
             throw new BusinessException(ResponseStatus.ACCOUNT_INCORRECT.getCode(), "鍟嗘埛涓嶅瓨鍦ㄦ垨鏈敞鍐�");
         }
         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)
+                .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)
+                .eq(Member::getPhone, phone)
+                .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;
+        }
+        return ywCustomerMapper.selectOne(new QueryWrapper<YwCustomer>().lambda()
+                .eq(YwCustomer::getIsdeleted, Constants.ZERO)
+                .eq(YwCustomer::getMemberId, member.getId())
+                .last(" limit 1 "));
     }
 
     private YwCustomer requireActiveCustomer(Integer customerId) {
@@ -125,11 +165,24 @@
         loginUserInfo.setId(customer.getId());
         loginUserInfo.setH5UserType(LoginUserInfo.H5_USER_CUSTOMER);
         loginUserInfo.setRealname(customer.getName());
-        loginUserInfo.setMobile(customer.getPhone());
+        loginUserInfo.setMobile(resolveLoginMobile(customer));
         loginUserInfo.setUsername("customer_" + customer.getId());
         loginUserInfo.setSource(LoginUserInfo.SOURCE_H5_CUSTOMER);
         loginUserInfo.setRoles(Collections.singletonList("h5_customer"));
         loginUserInfo.setPermissions(Collections.emptyList());
         return loginUserInfo;
     }
+
+    private String resolveLoginMobile(YwCustomer customer) {
+        if (StringUtils.isNotBlank(customer.getPhone())) {
+            return customer.getPhone();
+        }
+        if (customer.getMemberId() != null) {
+            Member member = memberMapper.selectById(customer.getMemberId());
+            if (member != null && StringUtils.isNotBlank(member.getPhone())) {
+                return member.getPhone();
+            }
+        }
+        return customer.getPhone();
+    }
 }
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
new file mode 100644
index 0000000..78e0714
--- /dev/null
+++ b/server/visits/dmvisit_service/src/main/java/com/doumee/service/business/impl/YwCustomerH5BizServiceImpl.java
@@ -0,0 +1,752 @@
+package com.doumee.service.business.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.doumee.core.constants.ResponseStatus;
+import com.doumee.core.exception.BusinessException;
+import com.doumee.core.model.LoginUserInfo;
+import com.doumee.core.model.PageData;
+import com.doumee.core.model.PageWrap;
+import com.doumee.biz.system.SystemDictDataBiz;
+import com.doumee.core.utils.Constants;
+import com.doumee.core.utils.Utils;
+import com.doumee.dao.business.YwContractBillMapper;
+import com.doumee.dao.business.YwContractMapper;
+import com.doumee.dao.business.YwContractRevenueMapper;
+import com.doumee.dao.business.YwContractRoomMapper;
+import com.doumee.dao.business.YwCustomerElectricalMapper;
+import com.doumee.dao.business.YwCustomerMapper;
+import com.doumee.dao.business.YwElectricalMapper;
+import com.doumee.dao.business.YwRoomMapper;
+import com.doumee.dao.business.dto.YwCustomerRechargeElectricalDTO;
+import com.doumee.dao.business.dto.YwCustomerRechargeDetailVO;
+import com.doumee.dao.business.dto.YwCustomerRechargeRecordQueryDTO;
+import com.doumee.dao.business.dto.YwCustomerRechargeRecordVO;
+import com.doumee.dao.business.dto.h5.*;
+import com.doumee.dao.business.model.*;
+import com.doumee.dao.system.MultifileMapper;
+import com.doumee.dao.system.model.Multifile;
+import com.doumee.service.business.YwCustomerH5BizService;
+import com.doumee.service.business.YwCustomerRechargeBizService;
+import com.doumee.service.business.YwElectricalBizService;
+import com.doumee.service.business.YwH5BannerService;
+import com.github.yulichang.wrapper.MPJLambdaWrapper;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.util.CollectionUtils;
+
+import java.math.BigDecimal;
+import java.text.SimpleDateFormat;
+import java.util.*;
+import java.util.stream.Collectors;
+
+@Service
+public class YwCustomerH5BizServiceImpl implements YwCustomerH5BizService {
+
+    private static final int BILL_DUE_SOON_DAYS = 7;
+
+    @Autowired
+    private YwH5BannerService ywH5BannerService;
+    @Autowired
+    private YwCustomerMapper ywCustomerMapper;
+    @Autowired
+    private YwCustomerRechargeBizService ywCustomerRechargeBizService;
+    @Autowired
+    private YwCustomerElectricalMapper ywCustomerElectricalMapper;
+    @Autowired
+    private YwElectricalMapper ywElectricalMapper;
+    @Autowired
+    private YwElectricalBizService ywElectricalBizService;
+    @Autowired
+    private YwContractMapper ywContractMapper;
+    @Autowired
+    private YwContractBillMapper ywContractBillMapper;
+    @Autowired
+    private YwContractRevenueMapper ywContractRevenueMapper;
+    @Autowired
+    private YwContractRoomMapper ywContractRoomMapper;
+    @Autowired
+    private YwRoomMapper ywRoomMapper;
+    @Autowired
+    private MultifileMapper multifileMapper;
+    @Autowired
+    private SystemDictDataBiz systemDictDataBiz;
+
+    @Override
+    public List<YwH5Banner> listBanners() {
+        return ywH5BannerService.listEnabledForCustomerWorkbench();
+    }
+
+    @Override
+    public Map<String, Object> home(Integer customerId) {
+        YwCustomer customer = requireCustomer(customerId);
+        YwCustomerRechargeDetailVO detail = ywCustomerRechargeBizService.getDetail(customerId);
+        Map<String, Object> map = new LinkedHashMap<>();
+        map.put("customerName", customer.getName());
+        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());
+        map.put("electricalList", detail.getElectricalList());
+        return map;
+    }
+
+    @Override
+    public PageData<CustomerDeviceH5VO> devicePage(PageWrap<CustomerDeviceQueryDTO> pageWrap, Integer customerId) {
+        requireCustomer(customerId);
+        CustomerDeviceQueryDTO q = pageWrap.getModel() != null ? pageWrap.getModel() : new CustomerDeviceQueryDTO();
+        List<CustomerDeviceH5VO> all = new ArrayList<>();
+        if (q.getDeviceType() == null || q.getDeviceType() == 0) {
+            all.addAll(buildElectricalDevices(customerId));
+        }
+        if (q.getDeviceType() == null || q.getDeviceType() == 1) {
+            all.addAll(buildConditionerDevices(customerId));
+        }
+        if (q.getStatusFilter() != null) {
+            all = all.stream().filter(d -> matchDeviceStatus(d, q.getStatusFilter())).collect(Collectors.toList());
+        }
+        int p = (int) Math.max(pageWrap.getPage(), 1);
+        int size = (int) Math.max(pageWrap.getCapacity(), 10);
+        int from = (p - 1) * size;
+        int to = Math.min(from + size, all.size());
+        PageData<CustomerDeviceH5VO> data = new PageData<>();
+        data.setTotal(all.size());
+        data.setPage(p);
+        data.setCapacity(size);
+        data.setRecords(from >= all.size() ? Collections.emptyList() : all.subList(from, to));
+        return data;
+    }
+
+    @Override
+    public CustomerDeviceH5VO deviceDetail(Integer deviceType, Integer deviceId, Integer customerId) {
+        PageWrap<CustomerDeviceQueryDTO> pw = new PageWrap<>();
+        pw.setPage(1);
+        pw.setCapacity(1000);
+        CustomerDeviceQueryDTO q = new CustomerDeviceQueryDTO();
+        q.setDeviceType(deviceType);
+        pw.setModel(q);
+        return devicePage(pw, customerId).getRecords().stream()
+                .filter(d -> Objects.equals(d.getDeviceType(), deviceType) && Objects.equals(d.getDeviceId(), deviceId))
+                .findFirst()
+                .orElseThrow(() -> new BusinessException(ResponseStatus.DATA_EMPTY.getCode(), "璁惧涓嶅瓨鍦�"));
+    }
+
+    @Override
+    public PageData<YwCustomerRechargeRecordVO> rechargeRecordPage(PageWrap<CustomerRechargeRecordH5QueryDTO> pageWrap, Integer customerId) {
+        PageWrap<YwCustomerRechargeRecordQueryDTO> wrap = new PageWrap<>();
+        wrap.setPage(pageWrap.getPage());
+        wrap.setCapacity(pageWrap.getCapacity());
+        YwCustomerRechargeRecordQueryDTO model = new YwCustomerRechargeRecordQueryDTO();
+        model.setCustomerId(customerId);
+        CustomerRechargeRecordH5QueryDTO q = pageWrap.getModel();
+        if (q != null) {
+            model.setStatus(q.getStatus());
+            model.setCreateTimeBegin(q.getCreateTimeBegin());
+            model.setCreateTimeEnd(q.getCreateTimeEnd());
+            if (StringUtils.isNotBlank(q.getMonth())) {
+                try {
+                    SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM");
+                    Date start = sdf.parse(q.getMonth());
+                    Calendar cal = Calendar.getInstance();
+                    cal.setTime(start);
+                    cal.add(Calendar.MONTH, 1);
+                    model.setCreateTimeBegin(start);
+                    model.setCreateTimeEnd(cal.getTime());
+                } catch (Exception ignored) {
+                }
+            }
+        }
+        wrap.setModel(model);
+        return ywCustomerRechargeBizService.findRechargeRecordPage(wrap);
+    }
+
+    @Override
+    public PageData<YwContract> contractPage(PageWrap<CustomerContractQueryDTO> pageWrap, Integer customerId) {
+        requireCustomer(customerId);
+        IPage<YwContract> page = new Page<>(pageWrap.getPage(), pageWrap.getCapacity());
+        CustomerContractQueryDTO q = pageWrap.getModel();
+        QueryWrapper<YwContract> qw = new QueryWrapper<>();
+        qw.lambda()
+                .eq(YwContract::getRenterId, customerId)
+                .eq(YwContract::getIsdeleted, Constants.ZERO)
+                .eq(q != null && q.getStatus() != null, YwContract::getStatus, q != null ? q.getStatus() : null)
+                .orderByDesc(YwContract::getCreateDate);
+        IPage<YwContract> result = ywContractMapper.selectPage(page, qw);
+        enrichContractListForH5(result.getRecords());
+        return PageData.from(result);
+    }
+
+    @Override
+    public Map<String, Object> contractDetail(Integer contractId, Integer customerId, Integer billType) {
+        YwContract contract = requireCustomerContract(contractId, customerId);
+        enrichContractForH5(contract, true);
+        int type = billType != null ? billType : Constants.ZERO;
+        Map<String, Object> map = new LinkedHashMap<>();
+        map.put("contract", contract);
+        map.put("bills", listContractBillsForH5(contractId, type));
+        map.put("billType", type);
+        return map;
+    }
+
+    @Override
+    public PageData<YwContractBill> billPage(PageWrap<CustomerBillQueryDTO> pageWrap, Integer customerId) {
+        List<Integer> contractIds = listCustomerContractIds(customerId);
+        if (contractIds.isEmpty()) {
+            return emptyBillPage(pageWrap);
+        }
+        IPage<YwContractBill> page = new Page<>(pageWrap.getPage(), pageWrap.getCapacity());
+        CustomerBillQueryDTO q = pageWrap.getModel();
+        MPJLambdaWrapper<YwContractBill> wrapper = new MPJLambdaWrapper<YwContractBill>()
+                .selectAll(YwContractBill.class)
+                .select(" ( select ifnull( sum( CASE WHEN t.bill_type = 0 and yw.REVENUE_TYPE = 0 THEN yw.ACT_RECEIVABLE_FEE when  t.bill_type = 0 and yw.REVENUE_TYPE = 1 then -yw.ACT_RECEIVABLE_FEE  when t.bill_type = 1 and yw.REVENUE_TYPE = 0 then -yw.ACT_RECEIVABLE_FEE else  yw.ACT_RECEIVABLE_FEE END),0) from  yw_contract_revenue yw where yw.bill_id = t.id and yw.status = 0 and yw.isdeleted = 0 ) as  actReceivableFee  ")
+                .in(YwContractBill::getContractId, contractIds)
+                .eq(YwContractBill::getIsdeleted, Constants.ZERO)
+                .eq(YwContractBill::getBillType, Constants.ZERO);
+        if (q != null && q.getPayTab() != null) {
+            if (q.getPayTab() == 0) {
+                wrapper.in(YwContractBill::getPayStatus, Arrays.asList(Constants.ZERO, Constants.TWO, Constants.THREE));
+            } else if (q.getPayTab() == 1) {
+                wrapper.eq(YwContractBill::getPayStatus, Constants.ONE);
+            }
+        }
+        wrapper.orderByDesc(YwContractBill::getPlanPayDate);
+        IPage<YwContractBill> result = ywContractBillMapper.selectJoinPage(page, YwContractBill.class, wrapper);
+        enrichBillsWithContract(result.getRecords());
+        return PageData.from(result);
+    }
+
+    @Override
+    public Map<String, Object> billDetail(Integer billId, Integer customerId) {
+        YwContractBill bill = ywContractBillMapper.selectById(billId);
+        if (bill == null || Objects.equals(bill.getIsdeleted(), Constants.ONE)) {
+            throw new BusinessException(ResponseStatus.DATA_EMPTY.getCode(), "璐﹀崟涓嶅瓨鍦�");
+        }
+        requireCustomerContract(bill.getContractId(), customerId);
+        enrichBillWithContract(bill);
+        enrichBillWithRooms(bill);
+        List<YwContractRevenue> revenues = listBillRevenues(billId, bill);
+        BigDecimal paid = sumPaid(revenues, bill.getBillType());
+        BigDecimal needPay = bill.getReceivableFee() != null ? bill.getReceivableFee().subtract(paid) : BigDecimal.ZERO;
+        bill.setActReceivableFee(paid);
+        bill.setNeedReceivableFee(needPay);
+        bill.setYwContractRevenueList(revenues);
+        Map<String, Object> map = new LinkedHashMap<>();
+        map.put("bill", bill);
+        map.put("revenues", revenues);
+        map.put("paidAmount", paid);
+        map.put("needPayAmount", needPay);
+        return map;
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void applyFirstRechargeIfNeeded(Integer customerId, LoginUserInfo user) {
+        YwCustomer customer = requireCustomer(customerId);
+        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());
+        for (Integer eid : electricalIds) {
+            YwCustomerRechargeElectricalDTO dto = new YwCustomerRechargeElectricalDTO();
+            dto.setCustomerId(customerId);
+            dto.setElectricalId(eid);
+            dto.setResetAction("resetPrepay");
+            try {
+                ywCustomerRechargeBizService.resetElectricalAccount(dto, user);
+            } catch (Exception e) {
+                // 宸插紑鎴峰垯璺宠繃
+            }
+        }
+        try {
+            ywCustomerRechargeBizService.cleanConditionerAccount(customerId, user);
+        } catch (Exception ignored) {
+        }
+        ywCustomerMapper.update(null, new UpdateWrapper<YwCustomer>().lambda()
+                .set(YwCustomer::getFirstRechargeDone, Constants.ONE)
+                .set(YwCustomer::getEditDate, new Date())
+                .eq(YwCustomer::getId, customerId));
+    }
+
+    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());
+        if (ids.isEmpty()) {
+            return Collections.emptyList();
+        }
+        List<YwElectrical> list = ywElectricalMapper.selectBatchIds(ids);
+        ywElectricalBizService.enrichList(list);
+        List<CustomerDeviceH5VO> vos = new ArrayList<>();
+        for (YwElectrical e : list) {
+            if (e == null || Objects.equals(e.getIsdeleted(), Constants.ONE)) continue;
+            CustomerDeviceH5VO vo = new CustomerDeviceH5VO();
+            vo.setDeviceType(0);
+            vo.setDeviceId(e.getId());
+            vo.setDeviceName(e.getName());
+            vo.setMeterAccountNo(e.getParamId());
+            vo.setRoomInfo(e.getRoomNames());
+            vo.setBalance(e.getBalance());
+            vo.setBalanceLow(e.getBalance() != null && e.getBalance().compareTo(new BigDecimal("50")) < 0);
+            vo.setUpdateTime(e.getEditDate());
+            boolean online = Objects.equals(e.getOnline(), Constants.ONE);
+            vo.setStatusCode(online ? 1 : 2);
+            vo.setStatusText(online ? "姝e父" : "鏂數");
+            List<String> alarms = new ArrayList<>();
+            if (!online) alarms.add("鏂數鎶ヨ");
+            if (Boolean.TRUE.equals(vo.getBalanceLow())) alarms.add("浣欓涓嶈冻浜岀骇鎶ヨ");
+            vo.setAlarmTags(alarms);
+            vos.add(vo);
+        }
+        return vos;
+    }
+
+    private List<CustomerDeviceH5VO> buildConditionerDevices(Integer customerId) {
+        List<YwConditioner> list = ywCustomerRechargeBizService.listCustomerConditioner(new PageWrap<>(), customerId).getRecords();
+        if (CollectionUtils.isEmpty(list)) {
+            return Collections.emptyList();
+        }
+        YwCustomerGs gs = ywCustomerRechargeBizService.getCustomerGsConfig(customerId);
+        List<CustomerDeviceH5VO> vos = new ArrayList<>();
+        for (YwConditioner c : list) {
+            CustomerDeviceH5VO vo = new CustomerDeviceH5VO();
+            vo.setDeviceType(1);
+            vo.setDeviceId(c.getId());
+            vo.setDeviceName(StringUtils.defaultIfBlank(c.getName(), c.getCode()));
+            vo.setRoomInfo(c.getRoomName());
+            if (StringUtils.isNotBlank(c.getRoomName())) {
+                vo.setRoomList(Collections.singletonList(c.getRoomName()));
+            }
+            vo.setBalance(gs != null ? gs.getLeftMoney() : null);
+            boolean online = "鍦ㄧ嚎".equals(c.getOnline());
+            vo.setStatusCode(online ? 1 : 2);
+            vo.setStatusText(online ? "姝e父" : "绂荤嚎");
+            vo.setUpdateTime(c.getLastSyncDate());
+            vos.add(vo);
+        }
+        return vos;
+    }
+
+    private boolean matchDeviceStatus(CustomerDeviceH5VO d, Integer filter) {
+        if (filter == 1) return Objects.equals(d.getStatusCode(), 1);
+        if (filter == 2) return Objects.equals(d.getStatusCode(), 2);
+        return true;
+    }
+
+    private YwCustomer requireCustomer(Integer customerId) {
+        YwCustomer c = ywCustomerMapper.selectById(customerId);
+        if (c == null || Objects.equals(c.getIsdeleted(), Constants.ONE)) {
+            throw new BusinessException(ResponseStatus.DATA_EMPTY.getCode(), "鍟嗘埛涓嶅瓨鍦�");
+        }
+        return c;
+    }
+
+    private YwContract requireCustomerContract(Integer contractId, Integer customerId) {
+        YwContract c = ywContractMapper.selectById(contractId);
+        if (c == null || !Objects.equals(c.getRenterId(), customerId)) {
+            throw new BusinessException(ResponseStatus.NOT_ALLOWED);
+        }
+        return c;
+    }
+
+    private List<Integer> listCustomerContractIds(Integer customerId) {
+        return ywContractMapper.selectList(new QueryWrapper<YwContract>().lambda()
+                        .eq(YwContract::getRenterId, customerId)
+                        .eq(YwContract::getIsdeleted, Constants.ZERO))
+                .stream().map(YwContract::getId).collect(Collectors.toList());
+    }
+
+    private void enrichContractListForH5(List<YwContract> contracts) {
+        if (CollectionUtils.isEmpty(contracts)) {
+            return;
+        }
+        List<Integer> contractIds = contracts.stream().map(YwContract::getId).collect(Collectors.toList());
+        Map<Integer, List<YwRoom>> roomMap = loadContractRoomMap(contractIds);
+        Map<Integer, List<YwContractBill>> billMap = loadContractBillMap(contractIds);
+        for (YwContract contract : contracts) {
+            List<YwRoom> rooms = roomMap.getOrDefault(contract.getId(), Collections.emptyList());
+            applyRoomSummary(contract, rooms);
+            contract.setPayTypeText(resolvePayTypeText(contract));
+            fillBillStatusTip(contract, billMap.getOrDefault(contract.getId(), Collections.emptyList()));
+        }
+    }
+
+    private void enrichContractForH5(YwContract contract, boolean withFiles) {
+        if (contract == null) {
+            return;
+        }
+        Map<Integer, List<YwRoom>> roomMap = loadContractRoomMap(Collections.singletonList(contract.getId()));
+        applyRoomSummary(contract, roomMap.getOrDefault(contract.getId(), Collections.emptyList()));
+        contract.setPayTypeText(resolvePayTypeText(contract));
+        contract.setFreeRentPeriod(resolveFreeRentPeriod(contract));
+        fillBillStatusTip(contract, loadContractBillMap(Collections.singletonList(contract.getId()))
+                .getOrDefault(contract.getId(), Collections.emptyList()));
+        if (withFiles) {
+            initContractFiles(contract);
+        }
+    }
+
+    private Map<Integer, List<YwRoom>> loadContractRoomMap(List<Integer> contractIds) {
+        if (CollectionUtils.isEmpty(contractIds)) {
+            return Collections.emptyMap();
+        }
+        List<YwContractRoom> contractRooms = ywContractRoomMapper.selectList(new QueryWrapper<YwContractRoom>().lambda()
+                .in(YwContractRoom::getContractId, contractIds)
+                .eq(YwContractRoom::getType, Constants.ZERO)
+                .eq(YwContractRoom::getIsdeleted, Constants.ZERO));
+        if (contractRooms.isEmpty()) {
+            return Collections.emptyMap();
+        }
+        List<Integer> roomIds = contractRooms.stream().map(YwContractRoom::getRoomId).distinct().collect(Collectors.toList());
+        Map<Integer, YwRoom> roomById = loadRoomsWithNames(roomIds).stream()
+                .collect(Collectors.toMap(YwRoom::getId, r -> r, (a, b) -> a));
+        Map<Integer, List<YwRoom>> result = new HashMap<>();
+        for (YwContractRoom cr : contractRooms) {
+            YwRoom room = roomById.get(cr.getRoomId());
+            if (room != null) {
+                result.computeIfAbsent(cr.getContractId(), k -> new ArrayList<>()).add(room);
+            }
+        }
+        return result;
+    }
+
+    private List<YwRoom> loadRoomsWithNames(List<Integer> roomIds) {
+        if (CollectionUtils.isEmpty(roomIds)) {
+            return Collections.emptyList();
+        }
+        MPJLambdaWrapper<YwRoom> wrapper = new MPJLambdaWrapper<YwRoom>()
+                .selectAll(YwRoom.class)
+                .selectAs(YwProject::getName, YwRoom::getProjectName)
+                .selectAs(YwFloor::getName, YwRoom::getFloorName)
+                .selectAs(YwBuilding::getName, YwRoom::getBuildingName)
+                .leftJoin(YwProject.class, YwProject::getId, YwRoom::getProjectId)
+                .leftJoin(YwBuilding.class, YwBuilding::getId, YwRoom::getBuildingId)
+                .leftJoin(YwFloor.class, YwFloor::getId, YwRoom::getFloor)
+                .in(YwRoom::getId, roomIds)
+                .eq(YwRoom::getIsdeleted, Constants.ZERO);
+        return ywRoomMapper.selectJoinList(YwRoom.class, wrapper);
+    }
+
+    private void applyRoomSummary(YwContract contract, List<YwRoom> rooms) {
+        contract.setRoomList(rooms);
+        contract.setRoomInfo(buildRoomInfo(rooms));
+        BigDecimal totalArea = BigDecimal.ZERO;
+        if (!CollectionUtils.isEmpty(rooms)) {
+            for (YwRoom room : rooms) {
+                totalArea = totalArea.add(Constants.formatBigdecimal(room.getRentArea()));
+            }
+        }
+        contract.setTotalArea(totalArea);
+    }
+
+    private String buildRoomInfo(List<YwRoom> rooms) {
+        if (CollectionUtils.isEmpty(rooms)) {
+            return "";
+        }
+        return rooms.stream().map(this::formatRoomLine).filter(StringUtils::isNotBlank).collect(Collectors.joining("銆�"));
+    }
+
+    private String formatRoomLine(YwRoom room) {
+        if (room == null) {
+            return "";
+        }
+        StringBuilder sb = new StringBuilder();
+        if (StringUtils.isNotBlank(room.getProjectName())) {
+            sb.append(room.getProjectName());
+        }
+        if (StringUtils.isNotBlank(room.getBuildingName())) {
+            if (sb.length() > 0) {
+                sb.append("/");
+            }
+            sb.append(room.getBuildingName());
+        }
+        if (StringUtils.isNotBlank(room.getFloorName()) || StringUtils.isNotBlank(room.getRoomNum())) {
+            if (sb.length() > 0) {
+                sb.append("/");
+            }
+            sb.append(StringUtils.defaultString(room.getFloorName())).append("/").append(StringUtils.defaultString(room.getRoomNum()));
+        }
+        return sb.toString();
+    }
+
+    private String resolvePayTypeText(YwContract contract) {
+        Integer type = contract.getType() == null ? Constants.ZERO : contract.getType();
+        if (Objects.equals(type, Constants.ONE)) {
+            return formatPayType(contract.getWyPayType());
+        }
+        if (Objects.equals(type, Constants.TWO)) {
+            return formatPayType(contract.getZlPayType());
+        }
+        String zl = formatPayType(contract.getZlPayType());
+        String wy = formatPayType(contract.getWyPayType());
+        if (StringUtils.isBlank(wy) || StringUtils.equals(zl, wy)) {
+            return zl;
+        }
+        return "绉熻祦" + zl + "锛涚墿涓�" + wy;
+    }
+
+    private String formatPayType(Integer payType) {
+        if (payType == null) {
+            return "-";
+        }
+        if (Objects.equals(payType, Constants.ONE)) {
+            return "姣忎笁涓湀涓�浠�";
+        }
+        if (Objects.equals(payType, Constants.TWO)) {
+            return "鍏釜鏈堜竴浠�";
+        }
+        if (Objects.equals(payType, Constants.THREE)) {
+            return "涓�骞翠竴浠�";
+        }
+        return "涓�娆℃�т粯娆�";
+    }
+
+    private String resolveFreeRentPeriod(YwContract contract) {
+        Date start;
+        Date end;
+        if (Objects.equals(contract.getType(), Constants.ONE)) {
+            start = contract.getWyFreeStartDate();
+            end = contract.getWyFreeEndDate();
+        } else {
+            start = contract.getZlFreeStartDate();
+            end = contract.getZlFreeEndDate();
+        }
+        if (start == null && end == null) {
+            return "-";
+        }
+        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
+        String startText = start != null ? sdf.format(start) : "-";
+        String endText = end != null ? sdf.format(end) : "-";
+        return startText + " ~ " + endText;
+    }
+
+    private Map<Integer, List<YwContractBill>> loadContractBillMap(List<Integer> contractIds) {
+        if (CollectionUtils.isEmpty(contractIds)) {
+            return Collections.emptyMap();
+        }
+        List<YwContractBill> bills = ywContractBillMapper.selectList(new QueryWrapper<YwContractBill>().lambda()
+                .in(YwContractBill::getContractId, contractIds)
+                .eq(YwContractBill::getIsdeleted, Constants.ZERO)
+                .orderByDesc(YwContractBill::getPlanPayDate));
+        return bills.stream().collect(Collectors.groupingBy(YwContractBill::getContractId));
+    }
+
+    private void fillBillStatusTip(YwContract contract, List<YwContractBill> bills) {
+        int overdue = 0;
+        int dueSoon = 0;
+        long now = System.currentTimeMillis();
+        Calendar cal = Calendar.getInstance();
+        cal.add(Calendar.DAY_OF_MONTH, BILL_DUE_SOON_DAYS);
+        long dueSoonLimit = Utils.Date.getDayEnd(cal.getTime()).getTime();
+        for (YwContractBill bill : bills) {
+            if (!isBillCountable(bill) || !isBillUnpaid(bill) || bill.getPlanPayDate() == null) {
+                continue;
+            }
+            long planEnd = Utils.Date.getEnd(bill.getPlanPayDate()).getTime();
+            if (planEnd < now) {
+                overdue++;
+            } else if (planEnd <= dueSoonLimit) {
+                dueSoon++;
+            }
+        }
+        if (overdue > 0) {
+            contract.setBillStatusTip(overdue + "涓处鍗曞凡閫炬湡锛岃灏藉揩鏀粯");
+            contract.setBillStatusType("danger");
+        } else if (dueSoon > 0) {
+            contract.setBillStatusTip(dueSoon + "涓处鍗曞嵆灏嗗埌鏈燂紝璇峰強鏃舵敮浠�");
+            contract.setBillStatusType("warn");
+        } else {
+            contract.setBillStatusTip("璐﹀崟閮芥寜鏃剁即璐逛簡锛屽緢妫掞紒");
+            contract.setBillStatusType("ok");
+        }
+    }
+
+    private boolean isBillCountable(YwContractBill bill) {
+        if (bill == null || Objects.equals(bill.getIsdeleted(), Constants.ONE)) {
+            return false;
+        }
+        if (!Constants.equalsInteger(bill.getStatus(), Constants.ZERO)) {
+            return false;
+        }
+        return bill.getReceivableFee() != null && bill.getReceivableFee().compareTo(BigDecimal.ZERO) > 0;
+    }
+
+    private boolean isBillUnpaid(YwContractBill bill) {
+        Integer payStatus = bill.getPayStatus();
+        if (Constants.equalsInteger(payStatus, Constants.ONE) || Constants.equalsInteger(payStatus, Constants.FIVE)) {
+            return false;
+        }
+        return Constants.equalsInteger(payStatus, Constants.ZERO)
+                || Constants.equalsInteger(payStatus, Constants.TWO)
+                || Constants.equalsInteger(payStatus, Constants.THREE)
+                || Constants.equalsInteger(payStatus, Constants.FOUR);
+    }
+
+    private void initContractFiles(YwContract contract) {
+        List<Multifile> multifiles = multifileMapper.selectJoinList(Multifile.class, new MPJLambdaWrapper<Multifile>()
+                .selectAll(Multifile.class)
+                .eq(Multifile::getObjId, contract.getId())
+                .eq(Multifile::getObjType, Constants.MultiFile.YW_CONTRACT_FILE.getKey())
+                .eq(Multifile::getIsdeleted, Constants.ZERO)
+                .orderByAsc(Multifile::getSortnum));
+        if (CollectionUtils.isEmpty(multifiles)) {
+            contract.setFileList(Collections.emptyList());
+            return;
+        }
+        String path = systemDictDataBiz.queryByCode(Constants.FTP, Constants.FTP_RESOURCE_PATH).getCode()
+                + systemDictDataBiz.queryByCode(Constants.FTP, Constants.YW_CONTRACT_FILE).getCode();
+        List<Multifile> fileList = new ArrayList<>();
+        for (Multifile file : multifiles) {
+            if (StringUtils.isBlank(file.getFileurl())) {
+                continue;
+            }
+            file.setFileurlFull(path + file.getFileurl());
+            fileList.add(file);
+        }
+        contract.setFileList(fileList);
+    }
+
+    private void enrichBillsWithContract(List<YwContractBill> bills) {
+        if (CollectionUtils.isEmpty(bills)) {
+            return;
+        }
+        List<Integer> contractIds = bills.stream()
+                .map(YwContractBill::getContractId)
+                .filter(Objects::nonNull)
+                .distinct()
+                .collect(Collectors.toList());
+        if (contractIds.isEmpty()) {
+            return;
+        }
+        Map<Integer, YwContract> contractMap = ywContractMapper.selectBatchIds(contractIds).stream()
+                .filter(Objects::nonNull)
+                .collect(Collectors.toMap(YwContract::getId, c -> c, (a, b) -> a));
+        for (YwContractBill bill : bills) {
+            applyContractToBill(bill, contractMap.get(bill.getContractId()));
+        }
+    }
+
+    private void enrichBillWithContract(YwContractBill bill) {
+        if (bill == null || bill.getContractId() == null) {
+            return;
+        }
+        YwContract contract = ywContractMapper.selectById(bill.getContractId());
+        applyContractToBill(bill, contract);
+        if (contract != null && contract.getRenterId() != null) {
+            YwCustomer customer = ywCustomerMapper.selectById(contract.getRenterId());
+            if (customer != null) {
+                bill.setCustomerName(customer.getName());
+            }
+        }
+    }
+
+    private void enrichBillWithRooms(YwContractBill bill) {
+        if (bill == null || bill.getContractId() == null) {
+            return;
+        }
+        List<YwRoom> rooms = loadContractRoomMap(Collections.singletonList(bill.getContractId()))
+                .getOrDefault(bill.getContractId(), Collections.emptyList());
+        bill.setRoomList(rooms);
+        bill.setRoomInfo(buildRoomInfo(rooms));
+    }
+
+    private void applyContractToBill(YwContractBill bill, YwContract contract) {
+        if (bill == null || contract == null) {
+            return;
+        }
+        bill.setContractCode(contract.getCode());
+        bill.setContractStartDate(contract.getStartDate());
+        bill.setContractEndDate(contract.getEndDate());
+    }
+
+    private List<YwContractBill> listContractBillsForH5(Integer contractId, Integer billType) {
+        List<YwContractBill> bills = ywContractBillMapper.selectJoinList(YwContractBill.class,
+                new MPJLambdaWrapper<YwContractBill>()
+                        .selectAll(YwContractBill.class)
+                        .select(" ( select ifnull( sum( CASE WHEN t.bill_type = 0 and yw.REVENUE_TYPE = 0 THEN yw.ACT_RECEIVABLE_FEE when  t.bill_type = 0 and yw.REVENUE_TYPE = 1 then -yw.ACT_RECEIVABLE_FEE  when t.bill_type = 1 and yw.REVENUE_TYPE = 0 then -yw.ACT_RECEIVABLE_FEE else  yw.ACT_RECEIVABLE_FEE END),0) from  yw_contract_revenue yw where yw.bill_id = t.id and yw.status = 0 and yw.isdeleted = 0 ) as  actReceivableFee  ")
+                        .eq(YwContractBill::getIsdeleted, Constants.ZERO)
+                        .eq(YwContractBill::getStatus, Constants.ZERO)
+                        .eq(YwContractBill::getContractId, contractId)
+                        .eq(YwContractBill::getBillType, billType != null ? billType : Constants.ZERO)
+                        .orderByDesc(YwContractBill::getId));
+        enrichContractBillsForH5(bills);
+        return bills;
+    }
+
+    private void enrichContractBillsForH5(List<YwContractBill> bills) {
+        if (CollectionUtils.isEmpty(bills)) {
+            return;
+        }
+        for (YwContractBill bill : bills) {
+            if (bill.getReceivableFee() != null && bill.getActReceivableFee() != null) {
+                bill.setNeedReceivableFee(bill.getReceivableFee().subtract(bill.getActReceivableFee()));
+            }
+            if (Constants.equalsInteger(bill.getStatus(), Constants.ZERO)
+                    && (Constants.equalsInteger(bill.getPayStatus(), Constants.ZERO)
+                    || Constants.equalsInteger(bill.getPayStatus(), Constants.TWO)
+                    || Constants.equalsInteger(bill.getPayStatus(), Constants.THREE)
+                    || Constants.equalsInteger(bill.getPayStatus(), Constants.FOUR))
+                    && bill.getPlanPayDate() != null
+                    && Utils.Date.getEnd(bill.getPlanPayDate()).getTime() < System.currentTimeMillis()) {
+                bill.setIsOverdue(Constants.ONE);
+            } else {
+                bill.setIsOverdue(Constants.ZERO);
+            }
+        }
+    }
+
+    /**
+     * 涓� PC 绔敹鏀祦姘翠竴鑷达細缁忚处鍗曞叧鑱斿悎鍚屾壙绉熶汉锛屽~鍏呭鏂瑰崟浣嶅悕绉�
+     */
+    private List<YwContractRevenue> listBillRevenues(Integer billId, YwContractBill bill) {
+        List<YwContractRevenue> revenues = ywContractRevenueMapper.selectJoinList(YwContractRevenue.class, new MPJLambdaWrapper<YwContractRevenue>()
+                .selectAll(YwContractRevenue.class)
+                .selectAs(YwCustomer::getName, YwContractRevenue::getCustomerName)
+                .leftJoin(YwContractBill.class, YwContractBill::getId, YwContractRevenue::getBillId)
+                .leftJoin(YwContract.class, YwContract::getId, YwContractBill::getContractId)
+                .leftJoin(YwCustomer.class, YwCustomer::getId, YwContract::getRenterId)
+                .eq(YwContractRevenue::getStatus, Constants.ZERO)
+                .eq(YwContractRevenue::getBillId, billId)
+                .orderByDesc(YwContractRevenue::getId));
+        fillRevenueCustomerNames(revenues, bill);
+        return revenues;
+    }
+
+    private void fillRevenueCustomerNames(List<YwContractRevenue> revenues, YwContractBill bill) {
+        if (CollectionUtils.isEmpty(revenues) || bill == null || StringUtils.isBlank(bill.getCustomerName())) {
+            return;
+        }
+        for (YwContractRevenue revenue : revenues) {
+            if (StringUtils.isBlank(revenue.getCustomerName())) {
+                revenue.setCustomerName(bill.getCustomerName());
+            }
+        }
+    }
+
+    private BigDecimal sumPaid(List<YwContractRevenue> revenues, Integer billType) {
+        if (CollectionUtils.isEmpty(revenues)) return BigDecimal.ZERO;
+        BigDecimal total = BigDecimal.ZERO;
+        for (YwContractRevenue r : revenues) {
+            if (r.getActReceivableFee() == null) continue;
+            int sign = Constants.equalsInteger(r.getRevenueType(), Constants.ZERO) ? 1 : -1;
+            if (Constants.equalsInteger(billType, Constants.ONE)) sign = -sign;
+            total = total.add(r.getActReceivableFee().multiply(BigDecimal.valueOf(sign)));
+        }
+        return total;
+    }
+
+    private PageData<YwContractBill> emptyBillPage(PageWrap<?> pageWrap) {
+        PageData<YwContractBill> data = new PageData<>();
+        data.setRecords(Collections.emptyList());
+        data.setTotal(0);
+        data.setPage(pageWrap.getPage());
+        data.setCapacity(pageWrap.getCapacity());
+        return data;
+    }
+}
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 66dc3c4..c1136e4 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
@@ -751,6 +751,7 @@
                 .eq(YwElectricalCharge::getIsdeleted, Constants.ZERO)
                 .eq(query.getType() != null, YwElectricalCharge::getType, query.getType())
                 .eq(query.getStatus() != null, YwElectricalCharge::getStatus, query.getStatus())
+                .eq(query.getCustomerId() != null, YwElectricalCharge::getCustomerId, query.getCustomerId())
                 .like(StringUtils.isNotBlank(query.getCustomerName()), YwCustomer::getName, query.getCustomerName())
                 .ge(query.getCreateTimeBegin() != null, YwElectricalCharge::getCreateDate, query.getCreateTimeBegin())
                 .le(query.getCreateTimeEnd() != null, YwElectricalCharge::getCreateDate, query.getCreateTimeEnd())
diff --git a/server/visits/dmvisit_service/src/main/java/com/doumee/service/business/impl/YwCustomerServiceImpl.java b/server/visits/dmvisit_service/src/main/java/com/doumee/service/business/impl/YwCustomerServiceImpl.java
index f36d94a..e9cdf1a 100644
--- a/server/visits/dmvisit_service/src/main/java/com/doumee/service/business/impl/YwCustomerServiceImpl.java
+++ b/server/visits/dmvisit_service/src/main/java/com/doumee/service/business/impl/YwCustomerServiceImpl.java
@@ -78,6 +78,7 @@
         memberMapper.insert(member);
 
         ywCustomer.setMemberId(member.getId());
+        ywCustomer.setPhone(member.getPhone());
         ywCustomerMapper.updateById(ywCustomer);
 
         return ywCustomer.getId();
@@ -94,12 +95,14 @@
         if(StringUtils.isNotBlank(member.getIdcardNo() ) && Constants.equalsInteger(member.getIdcardType(),Constants.ZERO) ){
             if(memberMapper.selectCount(new QueryWrapper<Member>().lambda()
                     .eq(Member::getIdcardNo, DESUtil.encrypt(Constants.EDS_PWD, member.getIdcardNo()))
+                    .eq(Member::getType, Constants.memberType.customer)
                     .eq(Member::getIsdeleted,Constants.ZERO)) >0){
                 throw new BusinessException(ResponseStatus.DATA_EXISTS.getCode(), "韬唤璇佸彿銆�"+member.getIdcardNo()+"銆戝凡琚娇鐢紝涓嶈兘閲嶅");
             }
         }
         if(memberMapper.selectCount(new QueryWrapper<Member>().lambda()
                 .eq(Member::getPhone,  member.getPhone())
+                .eq(Member::getType, Constants.memberType.customer)
                 .eq(Member::getIsdeleted,Constants.ZERO) ) >0){
             throw new BusinessException(ResponseStatus.DATA_EXISTS.getCode(), "鎵嬫満鍙枫��"+member.getPhone()+"銆戝凡琚娇鐢紝涓嶈兘閲嶅");
         }
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
new file mode 100644
index 0000000..35e1b09
--- /dev/null
+++ b/server/visits/dmvisit_service/src/main/java/com/doumee/service/business/impl/YwCustomerWxPayServiceImpl.java
@@ -0,0 +1,257 @@
+package com.doumee.service.business.impl;
+
+import com.alibaba.fastjson.JSON;
+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.wx.WxJsapiPayUtil;
+import com.doumee.dao.business.*;
+import com.doumee.dao.business.dto.YwCustomerRechargeConditionerDTO;
+import com.doumee.dao.business.dto.YwCustomerRechargeElectricalDTO;
+import com.doumee.dao.business.dto.h5.CustomerPayCreateDTO;
+import com.doumee.dao.business.model.*;
+import com.doumee.service.business.YwContractRevenueService;
+import com.doumee.service.business.YwCustomerH5BizService;
+import com.doumee.service.business.YwCustomerRechargeBizService;
+import com.doumee.service.business.YwCustomerWxPayService;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.math.BigDecimal;
+import java.util.*;
+
+@Service
+@Slf4j
+public class YwCustomerWxPayServiceImpl implements YwCustomerWxPayService {
+
+    @Autowired
+    private YwWxPayOrderMapper ywWxPayOrderMapper;
+    @Autowired
+    private YwContractBillMapper ywContractBillMapper;
+    @Autowired
+    private YwContractMapper ywContractMapper;
+    @Autowired
+    private YwAccountMapper ywAccountMapper;
+    @Autowired
+    private YwElectricalChargeMapper ywElectricalChargeMapper;
+    @Autowired
+    private WxJsapiPayUtil wxJsapiPayUtil;
+    @Autowired
+    private YwCustomerRechargeBizService ywCustomerRechargeBizService;
+    @Autowired
+    private YwContractRevenueService ywContractRevenueService;
+    @Autowired
+    private YwCustomerH5BizService ywCustomerH5BizService;
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public Map<String, String> createOrder(CustomerPayCreateDTO dto, LoginUserInfo user, String clientIp) {
+        if (user == null || user.getCustomerId() == null) {
+            throw new BusinessException(ResponseStatus.NO_ALLOW_LOGIN);
+        }
+        if (dto.getAmount() == null || dto.getAmount().compareTo(BigDecimal.ZERO) <= 0) {
+            throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "閲戦椤诲ぇ浜�0");
+        }
+        if (StringUtils.isBlank(dto.getOpenid())) {
+            throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "openid涓嶈兘涓虹┖");
+        }
+        Integer orderType = dto.getOrderType();
+        Integer bizRefId = resolveBizRefId(dto, user.getCustomerId());
+        validateBiz(orderType, bizRefId, user.getCustomerId(), dto.getAmount());
+
+        String orderNo = "WX" + System.currentTimeMillis() + user.getCustomerId() + new Random().nextInt(1000);
+        YwWxPayOrder order = new YwWxPayOrder();
+        order.setCreator(user.getId());
+        order.setCreateDate(new Date());
+        order.setEditor(user.getId());
+        order.setEditDate(new Date());
+        order.setIsdeleted(Constants.ZERO);
+        order.setOrderNo(orderNo);
+        order.setCustomerId(user.getCustomerId());
+        order.setOrderType(orderType);
+        order.setBizRefId(bizRefId);
+        order.setAmount(dto.getAmount());
+        order.setStatus(YwWxPayOrder.STATUS_WAIT);
+        order.setOpenid(dto.getOpenid());
+        order.setRemark(dto.getRemark());
+        order.setRequestSnapshot(JSON.toJSONString(dto));
+        ywWxPayOrderMapper.insert(order);
+
+        String body = orderType == YwWxPayOrder.TYPE_BILL ? "璐﹀崟缂磋垂" :
+                (orderType == YwWxPayOrder.TYPE_CONDITIONER ? "绌鸿皟鍏呭��" : "鐢佃垂鍏呭��");
+        Map<String, String> payParams = wxJsapiPayUtil.createJsapiOrder(orderNo, dto.getOpenid(), body, dto.getAmount(), clientIp);
+        payParams.put("orderNo", orderNo);
+        return payParams;
+    }
+
+    @Override
+    public YwWxPayOrder queryOrder(String orderNo, Integer customerId) {
+        return ywWxPayOrderMapper.selectOne(new QueryWrapper<YwWxPayOrder>().lambda()
+                .eq(YwWxPayOrder::getOrderNo, orderNo)
+                .eq(YwWxPayOrder::getCustomerId, customerId)
+                .eq(YwWxPayOrder::getIsdeleted, Constants.ZERO)
+                .last("limit 1"));
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public String handleNotify(String xmlBody) {
+        Map<String, String> data = wxJsapiPayUtil.parseNotifyXml(xmlBody);
+        if (!wxJsapiPayUtil.verifyNotifySign(data)) {
+            return failXml("绛惧悕澶辫触");
+        }
+        if (!"SUCCESS".equals(data.get("return_code")) || !"SUCCESS".equals(data.get("result_code"))) {
+            return successXml();
+        }
+        String orderNo = data.get("out_trade_no");
+        YwWxPayOrder order = ywWxPayOrderMapper.selectOne(new QueryWrapper<YwWxPayOrder>().lambda()
+                .eq(YwWxPayOrder::getOrderNo, orderNo)
+                .eq(YwWxPayOrder::getIsdeleted, Constants.ZERO)
+                .last("limit 1"));
+        if (order == null) {
+            return successXml();
+        }
+        if (Objects.equals(order.getStatus(), YwWxPayOrder.STATUS_SUCCESS)) {
+            return successXml();
+        }
+        LoginUserInfo user = buildCustomerUser(order.getCustomerId());
+        try {
+            Integer bizRecordId = fulfillBiz(order, user);
+            order.setStatus(YwWxPayOrder.STATUS_SUCCESS);
+            order.setWxTransactionId(data.get("transaction_id"));
+            order.setPayTime(new Date());
+            order.setBizRecordId(bizRecordId);
+            order.setEditDate(new Date());
+            ywWxPayOrderMapper.updateById(order);
+        } catch (Exception e) {
+            log.error("wx pay fulfill failed orderNo={}", orderNo, e);
+            order.setStatus(YwWxPayOrder.STATUS_FAIL);
+            order.setStatusInfo(e.getMessage());
+            order.setEditDate(new Date());
+            ywWxPayOrderMapper.updateById(order);
+        }
+        return successXml();
+    }
+
+    private Integer fulfillBiz(YwWxPayOrder order, LoginUserInfo user) {
+        CustomerPayCreateDTO dto = JSON.parseObject(order.getRequestSnapshot(), CustomerPayCreateDTO.class);
+        if (order.getOrderType() == YwWxPayOrder.TYPE_BILL) {
+            return createBillRevenue(order, user);
+        }
+        ywCustomerH5BizService.applyFirstRechargeIfNeeded(order.getCustomerId(), user);
+        if (order.getOrderType() == YwWxPayOrder.TYPE_ELECTRICAL) {
+            YwCustomerRechargeElectricalDTO req = new YwCustomerRechargeElectricalDTO();
+            req.setCustomerId(order.getCustomerId());
+            req.setElectricalId(order.getBizRefId());
+            req.setMoney(order.getAmount());
+            req.setRemark(StringUtils.defaultIfBlank(dto != null ? dto.getRemark() : null, "寰俊H5鍏呭��"));
+            ywCustomerRechargeBizService.rechargeElectrical(req, user);
+            return findLatestChargeId(order.getCustomerId(), Constants.ZERO, order.getBizRefId(), order.getOrderNo());
+        }
+        YwCustomerRechargeConditionerDTO req = new YwCustomerRechargeConditionerDTO();
+        req.setCustomerId(order.getCustomerId());
+        req.setMoney(order.getAmount());
+        req.setRemark(StringUtils.defaultIfBlank(dto != null ? dto.getRemark() : null, "寰俊H5鍏呭��"));
+        ywCustomerRechargeBizService.rechargeConditioner(req, user);
+        return findLatestChargeId(order.getCustomerId(), Constants.ONE, order.getCustomerId(), order.getOrderNo());
+    }
+
+    private Integer createBillRevenue(YwWxPayOrder order, LoginUserInfo user) {
+        YwContractBill bill = ywContractBillMapper.selectById(order.getBizRefId());
+        if (bill == null) {
+            throw new BusinessException(ResponseStatus.DATA_EMPTY.getCode(), "璐﹀崟涓嶅瓨鍦�");
+        }
+        YwAccount account = ywAccountMapper.selectOne(new QueryWrapper<YwAccount>().lambda()
+                .eq(YwAccount::getCompanyId, bill.getCompanyId())
+                .eq(YwAccount::getIsdeleted, Constants.ZERO)
+                .eq(YwAccount::getStatus, Constants.ZERO)
+                .orderByAsc(YwAccount::getId)
+                .last("limit 1"));
+        if (account == null) {
+            throw new BusinessException(ResponseStatus.DATA_EMPTY.getCode(), "鏈厤缃敹鏀处鎴�");
+        }
+        YwContractRevenue revenue = new YwContractRevenue();
+        revenue.setBillId(bill.getId());
+        revenue.setCompanyId(bill.getCompanyId());
+        revenue.setAccountId(account.getId());
+        revenue.setActReceivableFee(order.getAmount());
+        revenue.setActPayDate(new Date());
+        revenue.setPayType(4);
+        revenue.setWxOrderNo(order.getOrderNo());
+        revenue.setRemark("寰俊H5鏀粯 orderNo=" + order.getOrderNo());
+        revenue.setLoginUserInfo(user);
+        return ywContractRevenueService.create(revenue);
+    }
+
+    private Integer findLatestChargeId(Integer customerId, int type, Integer objId, String orderNo) {
+        YwElectricalCharge charge = ywElectricalChargeMapper.selectOne(new QueryWrapper<YwElectricalCharge>().lambda()
+                .eq(YwElectricalCharge::getCustomerId, customerId)
+                .eq(YwElectricalCharge::getType, type)
+                .eq(type == Constants.ZERO, YwElectricalCharge::getObjId, objId)
+                .eq(YwElectricalCharge::getIsdeleted, Constants.ZERO)
+                .orderByDesc(YwElectricalCharge::getCreateDate)
+                .last("limit 1"));
+        if (charge != null && StringUtils.isBlank(charge.getWxOrderNo())) {
+            ywElectricalChargeMapper.update(null, new UpdateWrapper<YwElectricalCharge>().lambda()
+                    .set(YwElectricalCharge::getWxOrderNo, orderNo)
+                    .eq(YwElectricalCharge::getId, charge.getId()));
+            return charge.getId();
+        }
+        return charge != null ? charge.getId() : null;
+    }
+
+    private void validateBiz(Integer orderType, Integer bizRefId, Integer customerId, BigDecimal amount) {
+        if (orderType == YwWxPayOrder.TYPE_BILL) {
+            YwContractBill bill = ywContractBillMapper.selectById(bizRefId);
+            if (bill == null || Objects.equals(bill.getIsdeleted(), Constants.ONE)) {
+                throw new BusinessException(ResponseStatus.DATA_EMPTY.getCode(), "璐﹀崟涓嶅瓨鍦�");
+            }
+            YwContract contract = ywContractMapper.selectById(bill.getContractId());
+            if (contract == null || !Objects.equals(contract.getRenterId(), customerId)) {
+                throw new BusinessException(ResponseStatus.NOT_ALLOWED);
+            }
+        }
+    }
+
+    private Integer resolveBizRefId(CustomerPayCreateDTO dto, Integer customerId) {
+        if (dto.getOrderType() == YwWxPayOrder.TYPE_ELECTRICAL) {
+            if (dto.getElectricalId() == null) {
+                throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "鐢佃〃ID涓嶈兘涓虹┖");
+            }
+            return dto.getElectricalId();
+        }
+        if (dto.getOrderType() == YwWxPayOrder.TYPE_CONDITIONER) {
+            return customerId;
+        }
+        if (dto.getOrderType() == YwWxPayOrder.TYPE_BILL) {
+            if (dto.getBillId() == null) {
+                throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "璐﹀崟ID涓嶈兘涓虹┖");
+            }
+            return dto.getBillId();
+        }
+        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 String successXml() {
+        return "<xml><return_code><![CDATA[SUCCESS]]></return_code><return_msg><![CDATA[OK]]></return_msg></xml>";
+    }
+
+    private String failXml(String msg) {
+        return "<xml><return_code><![CDATA[FAIL]]></return_code><return_msg><![CDATA[" + msg + "]]></return_msg></xml>";
+    }
+}
diff --git a/server/visits/dmvisit_service/src/main/java/com/doumee/service/business/impl/YwH5BannerServiceImpl.java b/server/visits/dmvisit_service/src/main/java/com/doumee/service/business/impl/YwH5BannerServiceImpl.java
new file mode 100644
index 0000000..9f17e6c
--- /dev/null
+++ b/server/visits/dmvisit_service/src/main/java/com/doumee/service/business/impl/YwH5BannerServiceImpl.java
@@ -0,0 +1,169 @@
+package com.doumee.service.business.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.doumee.core.constants.ResponseStatus;
+import com.doumee.core.exception.BusinessException;
+import com.doumee.core.model.LoginUserInfo;
+import com.doumee.core.model.PageData;
+import com.doumee.core.model.PageWrap;
+import com.doumee.core.utils.Constants;
+import com.doumee.core.utils.Utils;
+import com.doumee.dao.business.YwH5BannerMapper;
+import com.doumee.dao.business.model.YwH5Banner;
+import com.doumee.service.business.YwH5BannerService;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.util.CollectionUtils;
+
+import java.util.Date;
+import java.util.List;
+import java.util.Objects;
+
+@Service
+public class YwH5BannerServiceImpl implements YwH5BannerService {
+
+    @Autowired
+    private YwH5BannerMapper ywH5BannerMapper;
+
+    @Override
+    public Integer create(YwH5Banner model) {
+        validateBanner(model, true);
+        LoginUserInfo user = resolveUser(model);
+        Date now = new Date();
+        YwH5Banner insert = new YwH5Banner();
+        insert.setCreator(user.getId());
+        insert.setCreateDate(now);
+        insert.setEditor(user.getId());
+        insert.setEditDate(now);
+        insert.setIsdeleted(Constants.ZERO);
+        insert.setTitle(StringUtils.trimToNull(model.getTitle()));
+        insert.setImageUrl(StringUtils.trim(model.getImageUrl()));
+        insert.setLinkUrl(StringUtils.trimToNull(model.getLinkUrl()));
+        insert.setSortnum(model.getSortnum() == null ? Constants.ZERO : model.getSortnum());
+        insert.setStatus(model.getStatus() == null ? Constants.ZERO : model.getStatus());
+        insert.setScope(Constants.ONE);
+        insert.setRemark(StringUtils.trimToNull(model.getRemark()));
+        ywH5BannerMapper.insert(insert);
+        return insert.getId();
+    }
+
+    @Override
+    public void deleteById(Integer id, LoginUserInfo user) {
+        YwH5Banner row = ywH5BannerMapper.selectById(id);
+        if (row == null || Constants.equalsInteger(row.getIsdeleted(), Constants.ONE)) {
+            throw new BusinessException(ResponseStatus.DATA_EMPTY);
+        }
+        ywH5BannerMapper.update(null, new UpdateWrapper<YwH5Banner>().lambda()
+                .set(YwH5Banner::getIsdeleted, Constants.ONE)
+                .set(YwH5Banner::getEditor, user.getId())
+                .set(YwH5Banner::getEditDate, new Date())
+                .eq(YwH5Banner::getId, id));
+    }
+
+    @Override
+    public void deleteByIdInBatch(List<Integer> ids, LoginUserInfo user) {
+        if (CollectionUtils.isEmpty(ids)) {
+            return;
+        }
+        for (Integer id : ids) {
+            deleteById(id, user);
+        }
+    }
+
+    @Override
+    public void updateById(YwH5Banner model) {
+        if (model.getId() == null) {
+            throw new BusinessException(ResponseStatus.BAD_REQUEST);
+        }
+        YwH5Banner existing = ywH5BannerMapper.selectById(model.getId());
+        if (existing == null || Constants.equalsInteger(existing.getIsdeleted(), Constants.ONE)) {
+            throw new BusinessException(ResponseStatus.DATA_EMPTY);
+        }
+        if (StringUtils.isNotBlank(model.getImageUrl())) {
+            validateBanner(model, false);
+        }
+        LoginUserInfo user = resolveUser(model);
+        YwH5Banner update = new YwH5Banner();
+        update.setId(model.getId());
+        update.setEditor(user.getId());
+        update.setEditDate(new Date());
+        if (model.getTitle() != null) {
+            update.setTitle(StringUtils.trimToNull(model.getTitle()));
+        }
+        if (StringUtils.isNotBlank(model.getImageUrl())) {
+            update.setImageUrl(StringUtils.trim(model.getImageUrl()));
+        }
+        if (model.getLinkUrl() != null) {
+            update.setLinkUrl(StringUtils.trimToNull(model.getLinkUrl()));
+        }
+        if (model.getSortnum() != null) {
+            update.setSortnum(model.getSortnum());
+        }
+        if (model.getStatus() != null) {
+            update.setStatus(model.getStatus());
+        }
+        if (model.getRemark() != null) {
+            update.setRemark(StringUtils.trimToNull(model.getRemark()));
+        }
+        ywH5BannerMapper.updateById(update);
+    }
+
+    @Override
+    public YwH5Banner findById(Integer id) {
+        YwH5Banner row = ywH5BannerMapper.selectById(id);
+        if (row == null || Constants.equalsInteger(row.getIsdeleted(), Constants.ONE)) {
+            return null;
+        }
+        return row;
+    }
+
+    @Override
+    public PageData<YwH5Banner> findPage(PageWrap<YwH5Banner> pageWrap) {
+        IPage<YwH5Banner> page = new Page<>(pageWrap.getPage(), pageWrap.getCapacity());
+        QueryWrapper<YwH5Banner> qw = new QueryWrapper<>();
+        YwH5Banner model = pageWrap.getModel();
+        if (model != null) {
+            Utils.MP.blankToNull(model);
+            if (StringUtils.isNotBlank(model.getTitle())) {
+                qw.lambda().like(YwH5Banner::getTitle, model.getTitle());
+            }
+            if (model.getStatus() != null) {
+                qw.lambda().eq(YwH5Banner::getStatus, model.getStatus());
+            }
+        }
+        qw.lambda()
+                .eq(YwH5Banner::getIsdeleted, Constants.ZERO)
+                .eq(YwH5Banner::getScope, Constants.ONE)
+                .orderByAsc(YwH5Banner::getSortnum)
+                .orderByDesc(YwH5Banner::getCreateDate);
+        return PageData.from(ywH5BannerMapper.selectPage(page, qw));
+    }
+
+    @Override
+    public List<YwH5Banner> listEnabledForCustomerWorkbench() {
+        return ywH5BannerMapper.selectList(new QueryWrapper<YwH5Banner>().lambda()
+                .eq(YwH5Banner::getIsdeleted, Constants.ZERO)
+                .eq(YwH5Banner::getStatus, Constants.ZERO)
+                .eq(YwH5Banner::getScope, Constants.ONE)
+                .orderByAsc(YwH5Banner::getSortnum)
+                .orderByDesc(YwH5Banner::getCreateDate));
+    }
+
+    private void validateBanner(YwH5Banner model, boolean requireImage) {
+        if (requireImage && StringUtils.isBlank(model.getImageUrl())) {
+            throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "璇蜂笂浼犺疆鎾浘鐗�");
+        }
+    }
+
+    private LoginUserInfo resolveUser(YwH5Banner model) {
+        LoginUserInfo user = model.getLoginUserInfo();
+        if (user == null) {
+            throw new BusinessException(ResponseStatus.NO_LOGIN);
+        }
+        return user;
+    }
+}
diff --git a/server/visits/dmvisit_service/src/main/java/com/doumee/service/business/impl/YwMaterialServiceImpl.java b/server/visits/dmvisit_service/src/main/java/com/doumee/service/business/impl/YwMaterialServiceImpl.java
index ea498da..29d83f8 100644
--- a/server/visits/dmvisit_service/src/main/java/com/doumee/service/business/impl/YwMaterialServiceImpl.java
+++ b/server/visits/dmvisit_service/src/main/java/com/doumee/service/business/impl/YwMaterialServiceImpl.java
@@ -27,7 +27,6 @@
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.github.yulichang.base.MPJBaseMapper;
 import com.github.yulichang.wrapper.MPJLambdaWrapper;
-import javafx.scene.paint.Material;
 import org.apache.commons.lang.StringUtils;
 import org.apache.shiro.SecurityUtils;
 import org.springframework.beans.BeanUtils;

--
Gitblit v1.9.3