doum
9 天以前 0201c32312f6478b2bde706607c8c6338e9e1d06
新增智能电表、空调管理
已添加26个文件
已修改27个文件
2501 ■■■■■ 文件已修改
admin/src/api/business/ywelectricalactions.js 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
admin/src/api/business/ywelectricalcharge.js 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
admin/src/views/business/ywelectricalactions.vue 24 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
admin/src/views/business/ywelectricalcharge.vue 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/db/ELECTRICAL_INTEGRATION.md 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/db/business.yw_customer_gs.stop_money.sql 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/db/business.yw_customer_recharge.admin_role_grant.sql 34 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/db/business.yw_customer_recharge.menu.sql 34 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/db/business.yw_customer_recharge.permissions.sql 33 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/db/business.yw_customer_recharge.sql 76 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/db/business.yw_electrical.menu.sql 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/db/business.yw_electrical_actions.menu.sql 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/db/business.yw_electrical_actions.permissions.sql 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/db/business.yw_electrical_actions.queryResult.grant.sql 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/db/business.yw_electrical_charge.permissions.sql 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/db/quartz_job.yw_timer.sql 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/system_timer/src/main/java/com/doumee/jobs/fegin/VisitServiceFegin.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/visits/admin_timer/src/main/java/com/doumee/api/YwTimerController.java 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/visits/dmvisit_admin/src/main/java/com/doumee/cloud/admin/YwCustomerRechargeCloudController.java 212 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/visits/dmvisit_admin/src/main/java/com/doumee/cloud/admin/YwElectricalActionsCloudController.java 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/visits/dmvisit_admin/src/main/java/com/doumee/cloud/admin/YwElectricalChargeCloudController.java 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/visits/dmvisit_service/src/main/java/com/doumee/core/conditoner/ConditionerUtil.java 80 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/visits/dmvisit_service/src/main/java/com/doumee/core/conditoner/model/ConditionerConstant.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/visits/dmvisit_service/src/main/java/com/doumee/core/conditoner/model/request/CompanyGsManageRequest.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/visits/dmvisit_service/src/main/java/com/doumee/core/conditoner/model/request/ConditionerSessionRequest.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/visits/dmvisit_service/src/main/java/com/doumee/core/device/ElectronicToolUtil.java 69 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/visits/dmvisit_service/src/main/java/com/doumee/core/device/model/request/RequestStatusRequest.java 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/visits/dmvisit_service/src/main/java/com/doumee/dao/business/YwCustomerConditionerMapper.java 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/visits/dmvisit_service/src/main/java/com/doumee/dao/business/YwCustomerElectricalMapper.java 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/visits/dmvisit_service/src/main/java/com/doumee/dao/business/YwCustomerGsMapper.java 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/visits/dmvisit_service/src/main/java/com/doumee/dao/business/dto/YwCustomerElectricalBalanceItem.java 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/visits/dmvisit_service/src/main/java/com/doumee/dao/business/dto/YwCustomerElectricalSaveDTO.java 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/visits/dmvisit_service/src/main/java/com/doumee/dao/business/dto/YwCustomerGsConfigDTO.java 24 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/visits/dmvisit_service/src/main/java/com/doumee/dao/business/dto/YwCustomerRechargeConditionerDTO.java 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/visits/dmvisit_service/src/main/java/com/doumee/dao/business/dto/YwCustomerRechargeDetailVO.java 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/visits/dmvisit_service/src/main/java/com/doumee/dao/business/dto/YwCustomerRechargeElectricalDTO.java 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/visits/dmvisit_service/src/main/java/com/doumee/dao/business/dto/YwCustomerRechargeMerchantVO.java 48 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/visits/dmvisit_service/src/main/java/com/doumee/dao/business/dto/YwCustomerRechargeQueryDTO.java 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/visits/dmvisit_service/src/main/java/com/doumee/dao/business/dto/YwCustomerRechargeRecordQueryDTO.java 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/visits/dmvisit_service/src/main/java/com/doumee/dao/business/dto/YwCustomerRechargeRecordVO.java 61 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/visits/dmvisit_service/src/main/java/com/doumee/dao/business/dto/YwElectricalOperateDTO.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/visits/dmvisit_service/src/main/java/com/doumee/dao/business/model/YwConditioner.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/visits/dmvisit_service/src/main/java/com/doumee/dao/business/model/YwCustomerConditioner.java 35 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/visits/dmvisit_service/src/main/java/com/doumee/dao/business/model/YwCustomerElectrical.java 32 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/visits/dmvisit_service/src/main/java/com/doumee/dao/business/model/YwCustomerGs.java 51 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/visits/dmvisit_service/src/main/java/com/doumee/dao/business/model/YwElectricalCharge.java 8 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/visits/dmvisit_service/src/main/java/com/doumee/service/business/YwCustomerRechargeBizService.java 56 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/visits/dmvisit_service/src/main/java/com/doumee/service/business/YwElectricalActionsService.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/visits/dmvisit_service/src/main/java/com/doumee/service/business/YwElectricalBizService.java 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/visits/dmvisit_service/src/main/java/com/doumee/service/business/impl/YwCustomerRechargeBizServiceImpl.java 943 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/visits/dmvisit_service/src/main/java/com/doumee/service/business/impl/YwElectricalActionsServiceImpl.java 26 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/visits/dmvisit_service/src/main/java/com/doumee/service/business/impl/YwElectricalBizServiceImpl.java 373 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/visits/dmvisit_service/src/main/java/com/doumee/service/business/impl/YwElectricalServiceImpl.java 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
admin/src/api/business/ywelectricalactions.js
@@ -5,3 +5,7 @@
export function fetchList (data) {
  return request.post(base + '/page', data, { trim: true })
}
export function queryResult (id) {
  return request.get(base + '/queryResult/' + id)
}
admin/src/api/business/ywelectricalcharge.js
@@ -9,3 +9,7 @@
export function exportExcel (data) {
  return request.post(base + '/exportExcel', data, { trim: true, download: true })
}
export function syncStatus (id) {
  return request.get(base + '/syncStatus/' + id)
}
admin/src/views/business/ywelectricalactions.vue
@@ -59,6 +59,17 @@
            <el-button type="text" :disabled="!row.responseBody" @click="openJson('响应报文', row.responseBody)">查看</el-button>
          </template>
        </el-table-column>
        <el-table-column label="操作" min-width="120" align="center" fixed="right">
          <template slot-scope="{ row }">
            <el-button
              v-if="row.status === 0 && row.oprId"
              type="text"
              :loading="queryLoadingId === row.id"
              v-permissions="['business:ywelectricalactions:queryResult']"
              @click="handleQueryResult(row)"
            >手动查询结果</el-button>
          </template>
        </el-table-column>
      </el-table>
      <pagination
        @size-change="handleSizeChange"
@@ -101,7 +112,8 @@
      actionTypeOptions: Object.keys(ACTION_TYPE_MAP).map(key => ({
        value: Number(key),
        label: ACTION_TYPE_MAP[key]
      }))
      })),
      queryLoadingId: null
    }
  },
  created () {
@@ -164,6 +176,16 @@
    openJson (title, content) {
      if (!content) return
      this.$refs.jsonWindow.open(title, { content })
    },
    handleQueryResult (row) {
      this.queryLoadingId = row.id
      actionsApi.queryResult(row.id)
        .then(msg => {
          this.$tip.success(msg || '查询完成')
          this.handlePageChange(this.tableData.pagination.pageIndex)
        })
        .catch(e => this.$tip.apiFailed(e))
        .finally(() => { this.queryLoadingId = null })
    }
  }
}
admin/src/views/business/ywelectricalcharge.vue
@@ -31,6 +31,7 @@
        <el-table-column prop="roomNames" label="绑定房间" min-width="150" align="center" show-overflow-tooltip/>
        <el-table-column prop="money" label="充值金额(元)" min-width="110" align="center"/>
        <el-table-column prop="banlance" label="充值前余额" min-width="110" align="center"/>
        <el-table-column prop="balanceAfter" label="充值后余额" min-width="110" align="center"/>
        <el-table-column label="状态" min-width="100" align="center">
          <template slot-scope="{ row }">
            <span v-if="row.status === 0">充值中</span>
@@ -44,6 +45,11 @@
        <el-table-column prop="statusInfo" label="状态说明" min-width="140" align="center" show-overflow-tooltip/>
        <el-table-column prop="createDate" label="提交时间" min-width="160" align="center"/>
        <el-table-column prop="statusTime" label="状态更新时间" min-width="160" align="center"/>
        <el-table-column label="操作" min-width="100" align="center" fixed="right">
          <template slot-scope="{ row }">
            <el-button v-if="row.status === 0" type="text" v-permissions="['business:ywelectricalcharge:syncStatus']" @click="handleSync(row)">手动同步</el-button>
          </template>
        </el-table-column>
      </el-table>
      <pagination @size-change="handleSizeChange" @current-change="handlePageChange" :pagination="tableData.pagination"/>
    </template>
@@ -103,6 +109,14 @@
    reset () {
      this.searchForm = { meterKeyword: '', status: '', oprId: '' }
      this.search()
    },
    handleSync (row) {
      chargeApi.syncStatus(row.id)
        .then(msg => {
          this.$tip.success(msg || '同步完成')
          this.handlePageChange(this.tableData.pagination.pageIndex)
        })
        .catch(e => this.$tip.apiFailed(e))
    }
  }
}
server/db/ELECTRICAL_INTEGRATION.md
@@ -174,5 +174,6 @@
| é‡‡é›†å™¨çŠ¶æ€ | `0 */5 * * * ?` | `getElectricalStatus` | GET `/timer/yw/getElectricalStatus` |
| æ‰¹é‡æŠ„表 | `30 0 * * * ?` | `syncElectricalMeterData` | GET `/timer/yw/syncElectricalMeterData` |
| æ—¥å¿—清理 | `0 30 2 * * ?` | `cleanElectricalLog` | GET `/timer/yw/cleanElectricalLog` |
| å¼‚步任务状态补偿 | `0 5 * * * ?` | `syncElectricalAsyncStatus` | GET `/timer/yw/syncElectricalAsyncStatus` |
| ç©ºè°ƒç½‘关状态 | `0 */5 * * * ?` | `syncConditionerGatewayStatus` | GET `/timer/yw/syncConditionerGatewayStatus` |
| ç©ºè°ƒå†…机状态 | `0 */10 * * * ?` | `syncConditionerIndoorUnits` | GET `/timer/yw/syncConditionerIndoorUnits` |
server/db/business.yw_customer_gs.stop_money.sql
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,12 @@
-- å•†æˆ·ç©ºè°ƒ GS é…ç½®ï¼šæ¬ è´¹é¢åº¦ï¼ˆå…ƒï¼‰
SET @db := DATABASE();
SET @sql := IF(
  (SELECT COUNT(*) FROM information_schema.COLUMNS
   WHERE TABLE_SCHEMA = @db AND TABLE_NAME = 'yw_customer_gs' AND COLUMN_NAME = 'stop_money') = 0,
  'ALTER TABLE `yw_customer_gs` ADD COLUMN `stop_money` decimal(12,2) DEFAULT 0 COMMENT ''欠费额度(元)'' AFTER `gs_bz`',
  'SELECT 1'
);
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
server/db/business.yw_customer_recharge.admin_role_grant.sql
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,34 @@
-- å•†æˆ·å……值:为超级管理员角色补全权限(可重复执行)
-- æ‰§è¡ŒåŽè¯·é‡æ–°ç™»å½•以刷新 Redis ä¸­çš„æƒé™ç¼“å­˜
INSERT INTO `SYSTEM_ROLE_PERMISSION` (`ROLE_ID`, `PERMISSION_ID`, `CREATE_TIME`, `UPDATE_TIME`, `CREATE_USER`, `UPDATE_USER`, `DELETED`)
SELECT r.`ID`, p.`ID`, CURRENT_TIMESTAMP, NULL, 1, NULL, 0
FROM `SYSTEM_ROLE` r
INNER JOIN `SYSTEM_PERMISSION` p ON p.`CODE` IN (
    'business:ywcustomerrecharge:query',
    'business:ywcustomerrecharge:exportExcel',
    'business:ywcustomerrecharge:bindDevice',
    'business:ywcustomerrecharge:recharge',
    'business:ywcustomerrechargerecord:query',
    'business:ywcustomerrechargerecord:exportExcel',
    'business:ywcustomerrechargerecord:retry',
    'business:ywcustomerrechargerecord:syncStatus'
) 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
  );
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.`DELETED` = 0 AND (
    (menu.`NAME` = '商户充值' AND (menu.`PATH` IS NULL OR menu.`PATH` = ''))
    OR menu.`PATH` IN ('/business/ywcustomerrecharge', '/business/ywcustomerrechargerecord')
)
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
  );
server/db/business.yw_customer_recharge.menu.sql
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,34 @@
-- å•†æˆ·å……值:一级菜单 + 2 å­èœå• + è¶…级管理员菜单授权
INSERT INTO `SYSTEM_MENU` (`PARENT_ID`, `NAME`, `PATH`, `REMARK`, `ICON`, `DISABLED`, `SORT`, `FIXED`, `CREATE_TIME`, `UPDATE_TIME`, `CREATE_USER`, `UPDATE_USER`, `DELETED`, `PARAMS`)
SELECT 0, '商户充值', '', '商户电表/空调充值管理', NULL, 0,
       IFNULL((SELECT MAX(sm.`SORT`) FROM `SYSTEM_MENU` sm WHERE sm.`PARENT_ID` = 0 AND sm.`DELETED` = 0), 0) + 1,
       0, CURRENT_TIMESTAMP, NULL, 1, NULL, 0, NULL
WHERE NOT EXISTS (
    SELECT 1 FROM `SYSTEM_MENU` x WHERE x.`DELETED` = 0 AND x.`NAME` = '商户充值' AND (x.`PATH` IS NULL OR x.`PATH` = '')
);
INSERT INTO `SYSTEM_MENU` (`PARENT_ID`, `NAME`, `PATH`, `REMARK`, `ICON`, `DISABLED`, `SORT`, `FIXED`, `CREATE_TIME`, `UPDATE_TIME`, `CREATE_USER`, `UPDATE_USER`, `DELETED`, `PARAMS`)
SELECT p.`ID`, '商户充值', '/business/ywcustomerrecharge', '商户列表、关联设备、充值', NULL, 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/ywcustomerrecharge')
LIMIT 1;
INSERT INTO `SYSTEM_MENU` (`PARENT_ID`, `NAME`, `PATH`, `REMARK`, `ICON`, `DISABLED`, `SORT`, `FIXED`, `CREATE_TIME`, `UPDATE_TIME`, `CREATE_USER`, `UPDATE_USER`, `DELETED`, `PARAMS`)
SELECT p.`ID`, '充值记录', '/business/ywcustomerrechargerecord', '电表/空调统一充值记录', NULL, 0, 2, 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/ywcustomerrechargerecord')
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.`DELETED` = 0 AND (
    (menu.`NAME` = '商户充值' AND (menu.`PATH` IS NULL OR menu.`PATH` = ''))
    OR menu.`PATH` IN ('/business/ywcustomerrecharge', '/business/ywcustomerrechargerecord')
)
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
  );
server/db/business.yw_customer_recharge.permissions.sql
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,33 @@
-- å•†æˆ·å……值模块权限(防重复 INSERT)
INSERT INTO `SYSTEM_PERMISSION`(`CODE`, `NAME`, `REMARK`, `FIXED`, `CREATE_USER`, `CREATE_TIME`, `UPDATE_USER`, `UPDATE_TIME`, `DELETED`)
SELECT 'business:ywcustomerrecharge:query', '查询商户充值', '', 0, 1, CURRENT_TIMESTAMP, NULL, NULL, 0
WHERE NOT EXISTS (SELECT 1 FROM `SYSTEM_PERMISSION` WHERE `CODE` = 'business:ywcustomerrecharge:query' AND `DELETED` = 0);
INSERT INTO `SYSTEM_PERMISSION`(`CODE`, `NAME`, `REMARK`, `FIXED`, `CREATE_USER`, `CREATE_TIME`, `UPDATE_USER`, `UPDATE_TIME`, `DELETED`)
SELECT 'business:ywcustomerrecharge:exportExcel', '导出商户充值', '', 0, 1, CURRENT_TIMESTAMP, NULL, NULL, 0
WHERE NOT EXISTS (SELECT 1 FROM `SYSTEM_PERMISSION` WHERE `CODE` = 'business:ywcustomerrecharge:exportExcel' AND `DELETED` = 0);
INSERT INTO `SYSTEM_PERMISSION`(`CODE`, `NAME`, `REMARK`, `FIXED`, `CREATE_USER`, `CREATE_TIME`, `UPDATE_USER`, `UPDATE_TIME`, `DELETED`)
SELECT 'business:ywcustomerrecharge:bindDevice', '关联设备', '', 0, 1, CURRENT_TIMESTAMP, NULL, NULL, 0
WHERE NOT EXISTS (SELECT 1 FROM `SYSTEM_PERMISSION` WHERE `CODE` = 'business:ywcustomerrecharge:bindDevice' AND `DELETED` = 0);
INSERT INTO `SYSTEM_PERMISSION`(`CODE`, `NAME`, `REMARK`, `FIXED`, `CREATE_USER`, `CREATE_TIME`, `UPDATE_USER`, `UPDATE_TIME`, `DELETED`)
SELECT 'business:ywcustomerrecharge:recharge', '商户充值/清零', '', 0, 1, CURRENT_TIMESTAMP, NULL, NULL, 0
WHERE NOT EXISTS (SELECT 1 FROM `SYSTEM_PERMISSION` WHERE `CODE` = 'business:ywcustomerrecharge:recharge' AND `DELETED` = 0);
INSERT INTO `SYSTEM_PERMISSION`(`CODE`, `NAME`, `REMARK`, `FIXED`, `CREATE_USER`, `CREATE_TIME`, `UPDATE_USER`, `UPDATE_TIME`, `DELETED`)
SELECT 'business:ywcustomerrechargerecord:query', '查询充值记录', '', 0, 1, CURRENT_TIMESTAMP, NULL, NULL, 0
WHERE NOT EXISTS (SELECT 1 FROM `SYSTEM_PERMISSION` WHERE `CODE` = 'business:ywcustomerrechargerecord:query' AND `DELETED` = 0);
INSERT INTO `SYSTEM_PERMISSION`(`CODE`, `NAME`, `REMARK`, `FIXED`, `CREATE_USER`, `CREATE_TIME`, `UPDATE_USER`, `UPDATE_TIME`, `DELETED`)
SELECT 'business:ywcustomerrechargerecord:exportExcel', '导出充值记录', '', 0, 1, CURRENT_TIMESTAMP, NULL, NULL, 0
WHERE NOT EXISTS (SELECT 1 FROM `SYSTEM_PERMISSION` WHERE `CODE` = 'business:ywcustomerrechargerecord:exportExcel' AND `DELETED` = 0);
INSERT INTO `SYSTEM_PERMISSION`(`CODE`, `NAME`, `REMARK`, `FIXED`, `CREATE_USER`, `CREATE_TIME`, `UPDATE_USER`, `UPDATE_TIME`, `DELETED`)
SELECT 'business:ywcustomerrechargerecord:retry', '再次提交充值', '', 0, 1, CURRENT_TIMESTAMP, NULL, NULL, 0
WHERE NOT EXISTS (SELECT 1 FROM `SYSTEM_PERMISSION` WHERE `CODE` = 'business:ywcustomerrechargerecord:retry' AND `DELETED` = 0);
INSERT INTO `SYSTEM_PERMISSION`(`CODE`, `NAME`, `REMARK`, `FIXED`, `CREATE_USER`, `CREATE_TIME`, `UPDATE_USER`, `UPDATE_TIME`, `DELETED`)
SELECT 'business:ywcustomerrechargerecord:syncStatus', '手动同步充值状态', '', 0, 1, CURRENT_TIMESTAMP, NULL, NULL, 0
WHERE NOT EXISTS (SELECT 1 FROM `SYSTEM_PERMISSION` WHERE `CODE` = 'business:ywcustomerrechargerecord:syncStatus' AND `DELETED` = 0);
server/db/business.yw_customer_recharge.sql
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,76 @@
-- å•†æˆ·å……值模块:客户 GS é…ç½®ã€ç”µè¡¨/空调关联、充值记录扩展
CREATE TABLE IF NOT EXISTS `yw_customer_gs` (
  `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` int DEFAULT 0,
  `remark` varchar(500) DEFAULT NULL,
  `customer_id` int NOT NULL COMMENT '关联 yw_customer.id',
  `platform_gs_id` int DEFAULT NULL COMMENT '智精灵 addGs è¿”回的公司 ID',
  `is_pwr` tinyint DEFAULT 1 COMMENT '计费开关 0关 1开',
  `is_rest_stop` tinyint DEFAULT 0 COMMENT '18:00-09:00 ä¸åœæœº 0否 1是',
  `gs_bz` varchar(500) DEFAULT NULL COMMENT '备注',
  `stop_money` decimal(12,2) DEFAULT 0 COMMENT '欠费额度(元)',
  `left_money` decimal(12,4) DEFAULT NULL COMMENT '剩余金额,同步自 getGs',
  `sync_date` datetime DEFAULT NULL COMMENT '余额同步时间',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_customer_id` (`customer_id`),
  KEY `idx_platform_gs_id` (`platform_gs_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商户空调 GS é…ç½®';
CREATE TABLE IF NOT EXISTS `yw_customer_electrical` (
  `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` int DEFAULT 0,
  `remark` varchar(500) DEFAULT NULL,
  `customer_id` int NOT NULL COMMENT '关联 yw_customer.id',
  `electrical_id` int NOT NULL COMMENT '关联 yw_electrical.id',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_customer_electrical` (`customer_id`, `electrical_id`),
  KEY `idx_electrical_id` (`electrical_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商户关联电表';
CREATE TABLE IF NOT EXISTS `yw_customer_conditioner` (
  `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` int DEFAULT 0,
  `remark` varchar(500) DEFAULT NULL,
  `customer_id` int NOT NULL COMMENT '关联 yw_customer.id',
  `conditioner_id` int NOT NULL COMMENT '关联 yw_conditioner.id',
  `dev_ratio` int DEFAULT 100 COMMENT '电费占比%',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_customer_conditioner` (`customer_id`, `conditioner_id`),
  KEY `idx_conditioner_id` (`conditioner_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商户关联空调内机';
-- yw_electrical_charge æ‰©å±•列(若已存在则跳过)
SET @db := DATABASE();
SET @sql := IF(
  (SELECT COUNT(*) FROM information_schema.COLUMNS
   WHERE TABLE_SCHEMA = @db AND TABLE_NAME = 'yw_electrical_charge' AND COLUMN_NAME = 'balance_after') = 0,
  'ALTER TABLE `yw_electrical_charge` ADD COLUMN `balance_after` decimal(12,4) DEFAULT NULL COMMENT ''充值后余额(元)'' AFTER `banlance`',
  '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 = 'device_info') = 0,
  'ALTER TABLE `yw_electrical_charge` ADD COLUMN `device_info` varchar(500) DEFAULT NULL COMMENT ''设备展示信息'' AFTER `balance_after`',
  'SELECT 1'
);
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
server/db/business.yw_electrical.menu.sql
@@ -63,6 +63,7 @@
    'business:ywelectrical:exportExcel',
    'business:ywelectricalcharge:query',
    'business:ywelectricalcharge:exportExcel',
    'business:ywelectricalcharge:syncStatus',
    'business:ywelectricalparam:query',
    'business:ywelectricalparam:create',
    'business:ywelectricalparam:update',
server/db/business.yw_electrical_actions.menu.sql
@@ -22,7 +22,7 @@
INSERT INTO `SYSTEM_ROLE_PERMISSION` (`ROLE_ID`, `PERMISSION_ID`, `CREATE_TIME`, `UPDATE_TIME`, `CREATE_USER`, `UPDATE_USER`, `DELETED`)
SELECT r.`ID`, p.`ID`, CURRENT_TIMESTAMP, NULL, 1, NULL, 0
FROM `SYSTEM_ROLE` r
INNER JOIN `SYSTEM_PERMISSION` p ON p.`CODE` = 'business:ywelectricalactions:query' AND p.`DELETED` = 0
INNER JOIN `SYSTEM_PERMISSION` p ON p.`CODE` IN ('business:ywelectricalactions:query', 'business:ywelectricalactions:queryResult') AND p.`DELETED` = 0
WHERE r.`DELETED` = 0 AND (r.`CODE` = 'admin' OR r.`NAME` = '管理员')
  AND NOT EXISTS (
    SELECT 1 FROM `SYSTEM_ROLE_PERMISSION` rp
server/db/business.yw_electrical_actions.permissions.sql
@@ -1 +1,2 @@
INSERT INTO `SYSTEM_PERMISSION`(`CODE`, `NAME`, `REMARK`, `FIXED`, `CREATE_USER`, `CREATE_TIME`, `UPDATE_USER`, `UPDATE_TIME`, `DELETED`) VALUES ('business:ywelectricalactions:query', '查询电表远程操作记录', '', 0, 1, CURRENT_TIMESTAMP, NULL, NULL, 0);
INSERT INTO `SYSTEM_PERMISSION`(`CODE`, `NAME`, `REMARK`, `FIXED`, `CREATE_USER`, `CREATE_TIME`, `UPDATE_USER`, `UPDATE_TIME`, `DELETED`) VALUES ('business:ywelectricalactions:queryResult', '手动查询异步操作结果', '', 0, 1, CURRENT_TIMESTAMP, NULL, NULL, 0);
server/db/business.yw_electrical_actions.queryResult.grant.sql
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,14 @@
-- æ“ä½œè®°å½•「手动查询结果」权限(已部署环境补执行)
INSERT INTO `SYSTEM_PERMISSION`(`CODE`, `NAME`, `REMARK`, `FIXED`, `CREATE_USER`, `CREATE_TIME`, `UPDATE_USER`, `UPDATE_TIME`, `DELETED`)
SELECT 'business:ywelectricalactions:queryResult', '手动查询异步操作结果', '', 0, 1, CURRENT_TIMESTAMP, NULL, NULL, 0
WHERE NOT EXISTS (SELECT 1 FROM `SYSTEM_PERMISSION` WHERE `CODE` = 'business:ywelectricalactions:queryResult' AND `DELETED` = 0);
INSERT INTO `SYSTEM_ROLE_PERMISSION` (`ROLE_ID`, `PERMISSION_ID`, `CREATE_TIME`, `UPDATE_TIME`, `CREATE_USER`, `UPDATE_USER`, `DELETED`)
SELECT r.`ID`, p.`ID`, CURRENT_TIMESTAMP, NULL, 1, NULL, 0
FROM `SYSTEM_ROLE` r
INNER JOIN `SYSTEM_PERMISSION` p ON p.`CODE` = 'business:ywelectricalactions:queryResult' AND p.`DELETED` = 0
WHERE r.`DELETED` = 0 AND (r.`CODE` = 'admin' OR r.`NAME` = '管理员')
  AND NOT EXISTS (
    SELECT 1 FROM `SYSTEM_ROLE_PERMISSION` rp
    WHERE rp.`ROLE_ID` = r.`ID` AND rp.`PERMISSION_ID` = p.`ID` AND rp.`DELETED` = 0
  );
server/db/business.yw_electrical_charge.permissions.sql
@@ -1,2 +1,3 @@
INSERT INTO `SYSTEM_PERMISSION`(`CODE`, `NAME`, `REMARK`, `FIXED`, `CREATE_USER`, `CREATE_TIME`, `UPDATE_USER`, `UPDATE_TIME`, `DELETED`) VALUES ('business:ywelectricalcharge:query', '查询电表充值记录', '', 0, 1, CURRENT_TIMESTAMP, NULL, NULL, 0);
INSERT INTO `SYSTEM_PERMISSION`(`CODE`, `NAME`, `REMARK`, `FIXED`, `CREATE_USER`, `CREATE_TIME`, `UPDATE_USER`, `UPDATE_TIME`, `DELETED`) VALUES ('business:ywelectricalcharge:exportExcel', '导出电表充值记录(Excel)', '', 0, 1, CURRENT_TIMESTAMP, NULL, NULL, 0);
INSERT INTO `SYSTEM_PERMISSION`(`CODE`, `NAME`, `REMARK`, `FIXED`, `CREATE_USER`, `CREATE_TIME`, `UPDATE_USER`, `UPDATE_TIME`, `DELETED`) VALUES ('business:ywelectricalcharge:syncStatus', '手动同步电表充值状态', '', 0, 1, CURRENT_TIMESTAMP, NULL, NULL, 0);
server/db/quartz_job.yw_timer.sql
@@ -22,6 +22,12 @@
);
INSERT INTO `quartz_job` (`bean_name`, `params`, `module`, `cron_expres`, `state`, `remark`, `create_time`)
SELECT 'visitServiceJob', '', 'syncElectricalAsyncStatus', '0 5 * * * ?', 1, '智能电表-异步任务状态补偿查询', CURRENT_TIMESTAMP
WHERE NOT EXISTS (
    SELECT 1 FROM `quartz_job` WHERE `module` = 'syncElectricalAsyncStatus' AND `bean_name` = 'visitServiceJob'
);
INSERT INTO `quartz_job` (`bean_name`, `params`, `module`, `cron_expres`, `state`, `remark`, `create_time`)
SELECT 'visitServiceJob', '', 'syncConditionerGatewayStatus', '0 */5 * * * ?', 1, '空调多联机-网关在线状态同步', CURRENT_TIMESTAMP
WHERE NOT EXISTS (
    SELECT 1 FROM `quartz_job` WHERE `module` = 'syncConditionerGatewayStatus' AND `bean_name` = 'visitServiceJob'
server/system_timer/src/main/java/com/doumee/jobs/fegin/VisitServiceFegin.java
@@ -100,6 +100,10 @@
    @GetMapping("/timer/yw/cleanElectricalLog")
    ApiResponse cleanElectricalLog();
    @ApiOperation("【阜宁运维】定时查询处理中的电表异步任务状态")
    @GetMapping("/timer/yw/syncElectricalAsyncStatus")
    ApiResponse syncElectricalAsyncStatus();
    @ApiOperation("【阜宁运维】定时同步智精灵网关在线状态")
    @GetMapping("/timer/yw/syncConditionerGatewayStatus")
    ApiResponse syncConditionerGatewayStatus();
server/visits/admin_timer/src/main/java/com/doumee/api/YwTimerController.java
@@ -109,6 +109,13 @@
        return ApiResponse.success("电表接口日志清理成功");
    }
    @ApiOperation("定时查询处理中的电表异步任务状态")
    @GetMapping("/syncElectricalAsyncStatus")
    public ApiResponse syncElectricalAsyncStatus() {
        ywElectricalBizService.syncPendingAsyncActionsScheduled();
        return ApiResponse.success("电表异步任务状态同步成功");
    }
    @Autowired
    private com.doumee.service.business.ConditionerBizService conditionerBizService;
server/visits/dmvisit_admin/src/main/java/com/doumee/cloud/admin/YwCustomerRechargeCloudController.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,212 @@
package com.doumee.cloud.admin;
import com.doumee.api.BaseController;
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.config.annotation.CloudRequiredPermission;
import com.doumee.dao.business.dto.*;
import com.doumee.dao.business.model.YwConditioner;
import com.doumee.dao.business.model.YwCustomerGs;
import com.doumee.dao.business.model.YwElectrical;
import com.doumee.service.business.YwCustomerRechargeBizService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletResponse;
import java.util.Map;
@Api(tags = "商户充值")
@RestController
@RequestMapping(Constants.CLOUD_SERVICE_URL_INDEX + "/business/ywCustomerRecharge")
public class YwCustomerRechargeCloudController extends BaseController {
    @Autowired
    private YwCustomerRechargeBizService ywCustomerRechargeBizService;
    @ApiOperation("商户分页列表")
    @PostMapping("/merchantPage")
    @CloudRequiredPermission("business:ywcustomerrecharge:query")
    public ApiResponse<PageData<YwCustomerRechargeMerchantVO>> merchantPage(
            @RequestBody PageWrap<YwCustomerRechargeQueryDTO> pageWrap,
            @RequestHeader(Constants.HEADER_USER_TOKEN) String token) {
        return ApiResponse.success(ywCustomerRechargeBizService.findMerchantPage(pageWrap));
    }
    @ApiOperation("商户详情")
    @GetMapping("/{customerId}/detail")
    @CloudRequiredPermission("business:ywcustomerrecharge:query")
    public ApiResponse<YwCustomerRechargeDetailVO> detail(@PathVariable Integer customerId,
                                                          @RequestHeader(Constants.HEADER_USER_TOKEN) String token) {
        return ApiResponse.success(ywCustomerRechargeBizService.getDetail(customerId));
    }
    @ApiOperation("关联电表列表")
    @PostMapping("/electrical/page")
    @CloudRequiredPermission("business:ywcustomerrecharge:query")
    public ApiResponse<PageData<YwElectrical>> electricalPage(
            @RequestBody PageWrap<YwElectrical> pageWrap,
            @RequestParam Integer customerId,
            @RequestHeader(Constants.HEADER_USER_TOKEN) String token) {
        return ApiResponse.success(ywCustomerRechargeBizService.listCustomerElectrical(pageWrap, customerId));
    }
    @ApiOperation("可选电表列表")
    @PostMapping("/electrical/selectablePage")
    @CloudRequiredPermission("business:ywcustomerrecharge:bindDevice")
    public ApiResponse<PageData<YwElectrical>> selectableElectricalPage(
            @RequestBody PageWrap<YwElectrical> pageWrap,
            @RequestParam Integer customerId,
            @RequestHeader(Constants.HEADER_USER_TOKEN) String token) {
        return ApiResponse.success(ywCustomerRechargeBizService.pageSelectableElectrical(pageWrap, customerId));
    }
    @PreventRepeat
    @ApiOperation("保存关联电表")
    @PostMapping("/electrical/save")
    @CloudRequiredPermission("business:ywcustomerrecharge:bindDevice")
    public ApiResponse saveElectrical(@RequestBody YwCustomerElectricalSaveDTO dto,
                                      @RequestHeader(Constants.HEADER_USER_TOKEN) String token) {
        ywCustomerRechargeBizService.saveCustomerElectrical(dto, getLoginUser(token));
        return ApiResponse.success(null);
    }
    @ApiOperation("删除关联电表")
    @GetMapping("/electrical/delete")
    @CloudRequiredPermission("business:ywcustomerrecharge:bindDevice")
    public ApiResponse deleteElectrical(@RequestParam Integer customerId,
                                        @RequestParam Integer electricalId,
                                        @RequestHeader(Constants.HEADER_USER_TOKEN) String token) {
        ywCustomerRechargeBizService.removeCustomerElectrical(customerId, electricalId, getLoginUser(token));
        return ApiResponse.success(null);
    }
    @ApiOperation("关联空调列表")
    @PostMapping("/conditioner/page")
    @CloudRequiredPermission("business:ywcustomerrecharge:query")
    public ApiResponse<PageData<YwConditioner>> conditionerPage(
            @RequestBody PageWrap<YwConditioner> pageWrap,
            @RequestParam Integer customerId,
            @RequestHeader(Constants.HEADER_USER_TOKEN) String token) {
        return ApiResponse.success(ywCustomerRechargeBizService.listCustomerConditioner(pageWrap, customerId));
    }
    @ApiOperation("空调 GS é…ç½®")
    @GetMapping("/conditioner/gsConfig")
    @CloudRequiredPermission("business:ywcustomerrecharge:query")
    public ApiResponse<YwCustomerGs> gsConfig(@RequestParam Integer customerId,
                                              @RequestHeader(Constants.HEADER_USER_TOKEN) String token) {
        return ApiResponse.success(ywCustomerRechargeBizService.getCustomerGsConfig(customerId));
    }
    @PreventRepeat
    @ApiOperation("保存空调 GS é…ç½®")
    @PostMapping("/conditioner/saveGsConfig")
    @CloudRequiredPermission("business:ywcustomerrecharge:bindDevice")
    public ApiResponse saveGsConfig(@RequestBody YwCustomerGsConfigDTO dto,
                                    @RequestHeader(Constants.HEADER_USER_TOKEN) String token) {
        ywCustomerRechargeBizService.saveCustomerGsConfig(dto, getLoginUser(token));
        return ApiResponse.success(null);
    }
    @PreventRepeat
    @ApiOperation("电表充值")
    @PostMapping("/recharge/electrical")
    @CloudRequiredPermission("business:ywcustomerrecharge:recharge")
    public ApiResponse<String> rechargeElectrical(@RequestBody YwCustomerRechargeElectricalDTO dto,
                                                  @RequestHeader(Constants.HEADER_USER_TOKEN) String token) {
        return ApiResponse.success(ywCustomerRechargeBizService.rechargeElectrical(dto, getLoginUser(token)));
    }
    @PreventRepeat
    @ApiOperation("电表清零")
    @PostMapping("/reset/electrical")
    @CloudRequiredPermission("business:ywcustomerrecharge:recharge")
    public ApiResponse<String> resetElectrical(@RequestBody YwCustomerRechargeElectricalDTO dto,
                                               @RequestHeader(Constants.HEADER_USER_TOKEN) String token) {
        return ApiResponse.success(ywCustomerRechargeBizService.resetElectricalAccount(dto, getLoginUser(token)));
    }
    @ApiOperation("电表抄表刷新")
    @GetMapping("/readMeter")
    @CloudRequiredPermission("business:ywcustomerrecharge:recharge")
    public ApiResponse<Map<String, Object>> readMeter(@RequestParam Integer customerId,
                                                      @RequestParam Integer electricalId,
                                                      @RequestHeader(Constants.HEADER_USER_TOKEN) String token) {
        return ApiResponse.success(ywCustomerRechargeBizService.readMeterAndRefresh(customerId, electricalId, getLoginUser(token)));
    }
    @ApiOperation("电表远程信息")
    @GetMapping("/electrical/remoteInfo")
    @CloudRequiredPermission("business:ywcustomerrecharge:recharge")
    public ApiResponse<Map<String, Object>> electricalRemoteInfo(@RequestParam Integer electricalId,
                                                                  @RequestHeader(Constants.HEADER_USER_TOKEN) String token) {
        return ApiResponse.success(ywCustomerRechargeBizService.getElectricalRemoteInfo(electricalId));
    }
    @ApiOperation("空调充值前信息")
    @GetMapping("/recharge/conditioner/info")
    @CloudRequiredPermission("business:ywcustomerrecharge:recharge")
    public ApiResponse<Map<String, Object>> conditionerRechargeInfo(@RequestParam Integer customerId,
                                                                      @RequestHeader(Constants.HEADER_USER_TOKEN) String token) {
        return ApiResponse.success(ywCustomerRechargeBizService.getConditionerRechargeInfo(customerId));
    }
    @PreventRepeat
    @ApiOperation("空调充值")
    @PostMapping("/recharge/conditioner")
    @CloudRequiredPermission("business:ywcustomerrecharge:recharge")
    public ApiResponse<String> rechargeConditioner(@RequestBody YwCustomerRechargeConditionerDTO dto,
                                                   @RequestHeader(Constants.HEADER_USER_TOKEN) String token) {
        return ApiResponse.success(ywCustomerRechargeBizService.rechargeConditioner(dto, getLoginUser(token)));
    }
    @PreventRepeat
    @ApiOperation("空调清零")
    @PostMapping("/clean/conditioner")
    @CloudRequiredPermission("business:ywcustomerrecharge:recharge")
    public ApiResponse<String> cleanConditioner(@RequestParam Integer customerId,
                                                @RequestHeader(Constants.HEADER_USER_TOKEN) String token) {
        return ApiResponse.success(ywCustomerRechargeBizService.cleanConditionerAccount(customerId, getLoginUser(token)));
    }
    @ApiOperation("充值记录分页")
    @PostMapping("/rechargeRecord/page")
    @CloudRequiredPermission("business:ywcustomerrechargerecord:query")
    public ApiResponse<PageData<YwCustomerRechargeRecordVO>> rechargeRecordPage(
            @RequestBody PageWrap<YwCustomerRechargeRecordQueryDTO> pageWrap,
            @RequestHeader(Constants.HEADER_USER_TOKEN) String token) {
        return ApiResponse.success(ywCustomerRechargeBizService.findRechargeRecordPage(pageWrap));
    }
    @PreventRepeat
    @ApiOperation("再次提交充值")
    @PostMapping("/rechargeRecord/retry/{id}")
    @CloudRequiredPermission("business:ywcustomerrechargerecord:retry")
    public ApiResponse<String> retryRecharge(@PathVariable Integer id,
                                             @RequestHeader(Constants.HEADER_USER_TOKEN) String token) {
        return ApiResponse.success(ywCustomerRechargeBizService.retryRecharge(id, getLoginUser(token)));
    }
    @PreventRepeat
    @ApiOperation("手动同步充值状态")
    @PostMapping("/rechargeRecord/sync/{id}")
    @CloudRequiredPermission("business:ywcustomerrechargerecord:syncStatus")
    public ApiResponse<String> syncRechargeStatus(@PathVariable Integer id,
                                                  @RequestHeader(Constants.HEADER_USER_TOKEN) String token) {
        return ApiResponse.success(ywCustomerRechargeBizService.syncRechargeStatus(id, getLoginUser(token)));
    }
    @ApiOperation("导出充值记录")
    @PostMapping("/rechargeRecord/exportExcel")
    @CloudRequiredPermission("business:ywcustomerrechargerecord:exportExcel")
    public void exportRechargeRecord(@RequestBody PageWrap<YwCustomerRechargeRecordQueryDTO> pageWrap,
                                     HttpServletResponse response,
                                     @RequestHeader(Constants.HEADER_USER_TOKEN) String token) {
        ywCustomerRechargeBizService.exportRechargeRecord(pageWrap, response);
    }
}
server/visits/dmvisit_admin/src/main/java/com/doumee/cloud/admin/YwElectricalActionsCloudController.java
@@ -1,5 +1,6 @@
package com.doumee.cloud.admin;
import com.doumee.core.annotation.pr.PreventRepeat;
import com.doumee.api.BaseController;
import com.doumee.config.annotation.CloudRequiredPermission;
import com.doumee.core.model.ApiResponse;
@@ -34,4 +35,12 @@
        }
        return ApiResponse.success(ywElectricalActionsService.findPage(pageWrap));
    }
    @PreventRepeat
    @ApiOperation("手动查询异步操作结果")
    @GetMapping("/queryResult/{id}")
    @CloudRequiredPermission("business:ywelectricalactions:queryResult")
    public ApiResponse<String> queryResult(@PathVariable Integer id) {
        return ApiResponse.success(ywElectricalActionsService.queryAsyncResult(id));
    }
}
server/visits/dmvisit_admin/src/main/java/com/doumee/cloud/admin/YwElectricalChargeCloudController.java
@@ -32,6 +32,9 @@
    @Autowired
    private YwElectricalChargeService ywElectricalChargeService;
    @Autowired
    private com.doumee.service.business.YwElectricalBizService ywElectricalBizService;
    @PreventRepeat
    @ApiOperation("新建")
    @PostMapping("/create")
@@ -88,4 +91,12 @@
    public ApiResponse findById(@PathVariable Integer id, @RequestHeader(Constants.HEADER_USER_TOKEN) String token) {
        return ApiResponse.success(ywElectricalChargeService.findById(id));
    }
    @PreventRepeat
    @ApiOperation("手动同步充值状态")
    @GetMapping("/syncStatus/{id}")
    @CloudRequiredPermission("business:ywelectricalcharge:syncStatus")
    public ApiResponse<String> syncStatus(@PathVariable Integer id, @RequestHeader(Constants.HEADER_USER_TOKEN) String token) {
        return ApiResponse.success(ywElectricalBizService.syncChargeStatusById(id));
    }
}
server/visits/dmvisit_service/src/main/java/com/doumee/core/conditoner/ConditionerUtil.java
@@ -283,11 +283,51 @@
    }
    public static ConditionerBaseResponse<Object> addGs(CompanyGsManageRequest req) {
        return postJson("/addGs", req, Object.class);
        if (req == null) {
            return null;
        }
        req.fillSessionDefaults();
        JSONObject body = new JSONObject(true);
        putSessionFields(body, req);
        body.put("is_pwr", req.getIs_pwr() != null ? req.getIs_pwr() : 1);
        if (req.getLi_dev() != null && !req.getLi_dev().isEmpty()) {
            body.put("li_dev", req.getLi_dev());
        }
        if (req.getD_dev() != null && !req.getD_dev().isEmpty()) {
            body.put("d_dev", req.getD_dev());
        }
        body.put("gs_name", req.getGs_name());
        body.put("is_rest_stop", req.getIs_rest_stop() != null ? req.getIs_rest_stop() : 0);
        body.put("gs_bz", StringUtils.defaultString(req.getGs_bz()));
        putStopMoney(body, req.getStop_money());
        return postJsonBody("/addGs", body, Object.class);
    }
    public static ConditionerBaseResponse<Object> changeGs(CompanyGsManageRequest req) {
        return postJson("/changeGs", req, Object.class);
        if (req == null || req.getId() == null) {
            return null;
        }
        req.fillSessionDefaults();
        JSONObject body = new JSONObject(true);
        body.put("id", req.getId());
        body.put("is_pwr", req.getIs_pwr() != null ? req.getIs_pwr() : 1);
        body.put("is_rest_stop", req.getIs_rest_stop() != null ? req.getIs_rest_stop() : 0);
        body.put("gs_name", req.getGs_name());
        if (req.getLeft_money() != null) {
            body.put("left_money", req.getLeft_money());
        }
        body.put("is_stop", req.getIs_stop() != null ? req.getIs_stop() : 0);
        body.put("li_dev", req.getLi_dev());
        body.put("d_dev", req.getD_dev());
        body.put("gs_bz", StringUtils.defaultString(req.getGs_bz()));
        putStopMoney(body, req.getStop_money());
        return postJsonBody("/changeGs", body, Object.class);
    }
    private static void putStopMoney(JSONObject body, Object stopMoney) {
        if (stopMoney != null) {
            body.put("stop_money", stopMoney);
        }
    }
    public static ConditionerBaseResponse<Object> delGs(CompanyGsManageRequest req) {
@@ -299,11 +339,26 @@
    }
    public static ConditionerBaseResponse<Object> addMoney(AddMoneyRequest req) {
        return postJson("/addMoney", req, Object.class);
        if (req == null) {
            return null;
        }
        req.fillSessionDefaults();
        JSONObject body = new JSONObject(true);
        putSessionFields(body, req);
        body.put("id", req.getId());
        body.put("cz_money", req.getCz_money());
        return postJsonBody("/addMoney", body, Object.class);
    }
    public static ConditionerBaseResponse<Object> cleanMoney(CompanyGsManageRequest req) {
        return postJson("/cleanMoney", req, Object.class);
        if (req == null || req.getId() == null) {
            return null;
        }
        req.fillSessionDefaults();
        JSONObject body = new JSONObject(true);
        putSessionFields(body, req);
        body.put("id", req.getId());
        return postJsonBody("/cleanMoney", body, Object.class);
    }
    public static ConditionerBaseResponse<List<Object>> getCzLog(LogQueryRequest req) {
@@ -381,6 +436,23 @@
        }
    }
    private static <T> ConditionerBaseResponse<T> postJsonBody(String path, JSONObject body, Class<T> dataClass) {
        try {
            String raw = doPostRaw(path, body.toJSONString());
            return parseObjectResponse(raw, dataClass);
        } catch (Exception e) {
            log.error("conditioner POST {} failed", path, e);
            return null;
        }
    }
    private static void putSessionFields(JSONObject body, ConditionerSessionRequest req) {
        body.put("kt_token", req.getKt_token());
        body.put("kt_dwid", req.getKt_dwid());
        body.put("kt_unit", StringUtils.defaultIfBlank(req.getKt_unit(), ConditionerConstant.DEFAULT_KT_UNIT));
        body.put("kt_sonid", req.getKt_sonid());
    }
    private static String resolveUrl(String path) {
        String base = StringUtils.defaultIfBlank(ConditionerConstant.base_url, ConditionerConstant.DEFAULT_BASE_URL);
        if (base.endsWith("/")) {
server/visits/dmvisit_service/src/main/java/com/doumee/core/conditoner/model/ConditionerConstant.java
@@ -11,6 +11,8 @@
    public static final String DEFAULT_USERNAME = "admin";
    public static final String DEFAULT_PASSWORD = "12345678";
    public static final String DEFAULT_KT_SONID = "0";
    /** å•å…ƒ ID,addGs/addMoney ç­‰ POST æŽ¥å£æ–‡æ¡£ç¤ºä¾‹å‡ä¸º "1" */
    public static final String DEFAULT_KT_UNIT = "1";
    /** API æ ¹åœ°å€ï¼Œå¦‚ http://119.45.163.5:1125/zjl/API */
    public static String base_url = DEFAULT_BASE_URL;
@@ -21,6 +23,7 @@
    public static String kt_token;
    public static String kt_dwid;
    public static String kt_sonid = DEFAULT_KT_SONID;
    public static String kt_unit = DEFAULT_KT_UNIT;
    private ConditionerConstant() {
    }
server/visits/dmvisit_service/src/main/java/com/doumee/core/conditoner/model/request/CompanyGsManageRequest.java
@@ -37,4 +37,7 @@
    @ApiModelProperty("备注")
    private String gs_bz;
    @ApiModelProperty("欠费额度(元)")
    private Object stop_money;
}
server/visits/dmvisit_service/src/main/java/com/doumee/core/conditoner/model/request/ConditionerSessionRequest.java
@@ -35,5 +35,8 @@
        if (StringUtils.isBlank(kt_sonid)) {
            kt_sonid = ConditionerConstant.kt_sonid;
        }
        if (StringUtils.isBlank(kt_unit)) {
            kt_unit = ConditionerConstant.kt_unit;
        }
    }
}
server/visits/dmvisit_service/src/main/java/com/doumee/core/device/ElectronicToolUtil.java
@@ -203,7 +203,7 @@
        return null;
    }
    /**
     * ç”µè¡¨å¼€æˆ·_电表充值
     * ç”µè¡¨_电表充值
     */
    public static ElectronicBaseResponse   recharger(List<OpenAccountRequest> param) {
        if(param ==null || param.size()==0){
@@ -216,7 +216,7 @@
            String r = requestAsync(url, request_content);
            return parseAsyncMeterResponse(r);
        }catch (Exception e){
            log.error("电表==============开户",e);
            log.error("电表==============充值",e);
        }
        return null;
    }
@@ -227,7 +227,7 @@
        if(param ==null || param.size()==0){
            return null;
        }
        String url = ElectronicConstant.api2_url+"/Api_v2/ele_security/ele_control";
        String url = ElectronicConstant.api2_url+"/Api_v2/ele_control";
        try {
            String request_content = JSON.toJSONString(param);
@@ -239,6 +239,69 @@
        return null;
    }
    /**
     * æŸ¥è¯¢å¼‚步操作任务状态(/Api_v2/request/status)。
     * request_content ä¸º JSON æ•°ç»„,可合并传入多个 opr_id ä¸€æ¬¡æŸ¥è¯¢ã€‚
     * è°ƒç”¨æˆåŠŸåŽå»ºè®® 30s é¦–次查询,后续间隔递增(1h、2h、4h…),禁止频繁调用。
     * å“åº” SUCCESS、FAIL、TIMEOUT、CANCELED、RESPONSE_FAIL åŽæ— éœ€å†æŸ¥è¯¢ã€‚
     */
    public static ElectronicBaseResponse requestStatus(List<RequestStatusRequest> param) {
        if (param == null || param.isEmpty()) {
            return null;
        }
        String url = ElectronicConstant.api2_url + "/Api_v2/request/status";
        try {
            String request_content = JSON.toJSONString(param);
            String r = request(url, request_content);
            return parseAsyncMeterResponse(r);
        } catch (Exception e) {
            log.error("电表==============查询异步任务状态", e);
        }
        return null;
    }
    /**
     * æŒ‰ opr_id åˆ—表合并查询异步任务状态。
     */
    public static ElectronicBaseResponse requestStatusByOprIds(List<String> oprIds) {
        if (oprIds == null || oprIds.isEmpty()) {
            return null;
        }
        List<RequestStatusRequest> param = new ArrayList<>();
        for (String oprId : oprIds) {
            if (StringUtils.isNotBlank(oprId)) {
                RequestStatusRequest req = new RequestStatusRequest();
                req.setOpr_id(oprId.trim());
                param.add(req);
            }
        }
        return param.isEmpty() ? null : requestStatus(param);
    }
    /**
     * æŸ¥è¯¢å•个异步操作任务状态。
     */
    public static ElectronicBaseResponse requestStatus(String oprId) {
        if (StringUtils.isBlank(oprId)) {
            return null;
        }
        RequestStatusRequest req = new RequestStatusRequest();
        req.setOpr_id(oprId.trim());
        return requestStatus(Collections.singletonList(req));
    }
    /**
     * å¼‚步任务是否已到达终态,平台后续不再处理,无需再轮询。
     */
    public static boolean isAsyncStatusFinal(String status) {
        if (StringUtils.isBlank(status)) {
            return false;
        }
        String s = status.trim().toUpperCase(Locale.ROOT);
        return "SUCCESS".equals(s) || "FAIL".equals(s) || "TIMEOUT".equals(s)
                || "CANCELED".equals(s) || "RESPONSE_FAIL".equals(s);
    }
    /**
     * ç”µè¡¨_立即抄表
     */
    public static ElectronicBaseResponse   eleRead(List<EleReadRequest> param) {
server/visits/dmvisit_service/src/main/java/com/doumee/core/device/model/request/RequestStatusRequest.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,19 @@
package com.doumee.core.device.model.request;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.io.Serializable;
/**
 * å¼‚步操作任务状态查询请求项
 */
@ApiModel("异步操作任务状态查询")
@Data
public class RequestStatusRequest implements Serializable {
    @ApiModelProperty(value = "操作ID,长度16-32,与提交异步任务时一致")
    private String opr_id;
}
server/visits/dmvisit_service/src/main/java/com/doumee/dao/business/YwCustomerConditionerMapper.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,7 @@
package com.doumee.dao.business;
import com.doumee.dao.business.model.YwCustomerConditioner;
import com.github.yulichang.base.MPJBaseMapper;
public interface YwCustomerConditionerMapper extends MPJBaseMapper<YwCustomerConditioner> {
}
server/visits/dmvisit_service/src/main/java/com/doumee/dao/business/YwCustomerElectricalMapper.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,7 @@
package com.doumee.dao.business;
import com.doumee.dao.business.model.YwCustomerElectrical;
import com.github.yulichang.base.MPJBaseMapper;
public interface YwCustomerElectricalMapper extends MPJBaseMapper<YwCustomerElectrical> {
}
server/visits/dmvisit_service/src/main/java/com/doumee/dao/business/YwCustomerGsMapper.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,7 @@
package com.doumee.dao.business;
import com.doumee.dao.business.model.YwCustomerGs;
import com.github.yulichang.base.MPJBaseMapper;
public interface YwCustomerGsMapper extends MPJBaseMapper<YwCustomerGs> {
}
server/visits/dmvisit_service/src/main/java/com/doumee/dao/business/dto/YwCustomerElectricalBalanceItem.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,13 @@
package com.doumee.dao.business.dto;
import lombok.Data;
import java.math.BigDecimal;
@Data
public class YwCustomerElectricalBalanceItem {
    private String name;
    private String address;
    private BigDecimal balance;
}
server/visits/dmvisit_service/src/main/java/com/doumee/dao/business/dto/YwCustomerElectricalSaveDTO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,12 @@
package com.doumee.dao.business.dto;
import lombok.Data;
import java.util.List;
@Data
public class YwCustomerElectricalSaveDTO {
    private Integer customerId;
    private List<Integer> electricalIds;
}
server/visits/dmvisit_service/src/main/java/com/doumee/dao/business/dto/YwCustomerGsConfigDTO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,24 @@
package com.doumee.dao.business.dto;
import lombok.Data;
import java.math.BigDecimal;
import java.util.List;
@Data
public class YwCustomerGsConfigDTO {
    private Integer customerId;
    private Integer isPwr;
    private Integer isRestStop;
    private String gsBz;
    /** æ¬ è´¹é¢åº¦ï¼ˆå…ƒï¼‰ */
    private BigDecimal stopMoney;
    private List<ConditionerItem> conditioners;
    @Data
    public static class ConditionerItem {
        private Integer conditionerId;
        private Integer devRatio;
    }
}
server/visits/dmvisit_service/src/main/java/com/doumee/dao/business/dto/YwCustomerRechargeConditionerDTO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,13 @@
package com.doumee.dao.business.dto;
import lombok.Data;
import java.math.BigDecimal;
@Data
public class YwCustomerRechargeConditionerDTO {
    private Integer customerId;
    private BigDecimal money;
    private String remark;
}
server/visits/dmvisit_service/src/main/java/com/doumee/dao/business/dto/YwCustomerRechargeDetailVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,19 @@
package com.doumee.dao.business.dto;
import com.doumee.dao.business.model.YwConditioner;
import com.doumee.dao.business.model.YwCustomerGs;
import com.doumee.dao.business.model.YwElectrical;
import lombok.Data;
import java.util.List;
@Data
public class YwCustomerRechargeDetailVO {
    private Integer customerId;
    private String customerName;
    private String phone;
    private YwCustomerGs gsConfig;
    private List<YwElectrical> electricalList;
    private List<YwConditioner> conditionerList;
}
server/visits/dmvisit_service/src/main/java/com/doumee/dao/business/dto/YwCustomerRechargeElectricalDTO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,16 @@
package com.doumee.dao.business.dto;
import lombok.Data;
import java.math.BigDecimal;
@Data
public class YwCustomerRechargeElectricalDTO {
    private Integer customerId;
    private Integer electricalId;
    private BigDecimal money;
    private String remark;
    /** resetPrepay / resetPostpay */
    private String resetAction;
}
server/visits/dmvisit_service/src/main/java/com/doumee/dao/business/dto/YwCustomerRechargeMerchantVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,48 @@
package com.doumee.dao.business.dto;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.annotation.JsonIgnore;
import lombok.Data;
import java.math.BigDecimal;
import java.util.Date;
import java.util.List;
@Data
public class YwCustomerRechargeMerchantVO {
    private Integer id;
    /** å®¢æˆ·ç±»åž‹ 0个人 1企业 */
    private Integer type;
    private String name;
    private String phone;
    /** é»˜è®¤è”系人(同 clientList) */
    private String memberName;
    /** è”系人电话(同 clientList) */
    private String memberPhone;
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private Date createDate;
    private Integer electricalCount;
    /** å…³è”电表余额明细 */
    private List<YwCustomerElectricalBalanceItem> electricalBalances;
    private Integer conditionerCount;
    private BigDecimal acBalance;
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private Date acBalanceSyncDate;
    /** ç­›é€‰è¾…助:电表是否全在线 */
    @JsonIgnore
    private Boolean electricalOnlineAll;
    /** ç­›é€‰è¾…助:电表是否存在离线 */
    @JsonIgnore
    private Boolean electricalHasOffline;
    /** ç­›é€‰è¾…助:空调是否全在线 */
    @JsonIgnore
    private Boolean conditionerOnlineAll;
    /** ç­›é€‰è¾…助:空调是否存在离线 */
    @JsonIgnore
    private Boolean conditionerHasOffline;
}
server/visits/dmvisit_service/src/main/java/com/doumee/dao/business/dto/YwCustomerRechargeQueryDTO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,14 @@
package com.doumee.dao.business.dto;
import lombok.Data;
@Data
public class YwCustomerRechargeQueryDTO {
    /** å®¢æˆ·åç§°å…³é”®å­— */
    private String nameKeyword;
    /** ç”µè¡¨çŠ¶æ€ç­›é€‰ï¼š1全在线 2存在离线 3无设备 */
    private Integer electricalStatusFilter;
    /** ç©ºè°ƒçŠ¶æ€ç­›é€‰ï¼š1全在线 2存在离线 3无设备 */
    private Integer conditionerStatusFilter;
}
server/visits/dmvisit_service/src/main/java/com/doumee/dao/business/dto/YwCustomerRechargeRecordQueryDTO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,21 @@
package com.doumee.dao.business.dto;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import java.math.BigDecimal;
import java.util.Date;
@Data
public class YwCustomerRechargeRecordQueryDTO {
    private String customerName;
    private Integer type;
    private Integer status;
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private Date createTimeBegin;
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private Date createTimeEnd;
}
server/visits/dmvisit_service/src/main/java/com/doumee/dao/business/dto/YwCustomerRechargeRecordVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,61 @@
package com.doumee.dao.business.dto;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.doumee.core.annotation.excel.ExcelColumn;
import lombok.Data;
import java.math.BigDecimal;
import java.util.Date;
@Data
public class YwCustomerRechargeRecordVO {
    private Integer id;
    @ExcelColumn(name = "客户名称", index = 1, width = 20)
    private String customerName;
    @ExcelColumn(name = "业务类型", index = 2, width = 10)
    private String typeText;
    private Integer type;
    @ExcelColumn(name = "设备信息", index = 3, width = 30)
    private String deviceInfo;
    @ExcelColumn(name = "充值金额(元)", index = 4, width = 12)
    private BigDecimal money;
    @ExcelColumn(name = "充值前余额", index = 5, width = 12)
    private BigDecimal banlance;
    @ExcelColumn(name = "充值后余额", index = 6, width = 12)
    private BigDecimal balanceAfter;
    @ExcelColumn(name = "状态", index = 7, width = 10)
    private String statusText;
    private Integer status;
    @ExcelColumn(name = "任务ID", index = 8, width = 24)
    private String oprId;
    @ExcelColumn(name = "备注", index = 9, width = 20)
    private String remark;
    @ExcelColumn(name = "状态说明", index = 10, width = 20)
    private String statusInfo;
    @ExcelColumn(name = "提交时间", index = 11, width = 20)
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private Date createDate;
    @ExcelColumn(name = "状态更新时间", index = 12, width = 20)
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private Date statusTime;
    private Integer customerId;
    private Integer objId;
    private String address;
    private String name;
}
server/visits/dmvisit_service/src/main/java/com/doumee/dao/business/dto/YwElectricalOperateDTO.java
@@ -12,4 +12,6 @@
    private String action;
    private BigDecimal money;
    private String remark;
    /** å•†æˆ·å……值入口传入的客户 ID */
    private Integer customerId;
}
server/visits/dmvisit_service/src/main/java/com/doumee/dao/business/model/YwConditioner.java
@@ -135,4 +135,8 @@
    @TableField(exist = false)
    @ApiModelProperty("卡片筛选-网关MAC")
    private String wgMacFilter;
    @TableField(exist = false)
    @ApiModelProperty("商户关联-电费占比%")
    private Integer devRatio;
}
server/visits/dmvisit_service/src/main/java/com/doumee/dao/business/model/YwCustomerConditioner.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,35 @@
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.util.Date;
@Data
@TableName("yw_customer_conditioner")
@ApiModel("商户关联空调内机")
public class YwCustomerConditioner 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("客户 ID")
    private Integer customerId;
    @ApiModelProperty("空调内机 ID")
    private Integer conditionerId;
    @ApiModelProperty("电费占比%")
    private Integer devRatio;
}
server/visits/dmvisit_service/src/main/java/com/doumee/dao/business/model/YwCustomerElectrical.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,32 @@
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.util.Date;
@Data
@TableName("yw_customer_electrical")
@ApiModel("商户关联电表")
public class YwCustomerElectrical 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("客户 ID")
    private Integer customerId;
    @ApiModelProperty("电表 ID")
    private Integer electricalId;
}
server/visits/dmvisit_service/src/main/java/com/doumee/dao/business/model/YwCustomerGs.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,51 @@
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
@TableName("yw_customer_gs")
@ApiModel("商户空调 GS é…ç½®")
public class YwCustomerGs 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("客户 ID")
    private Integer customerId;
    @ApiModelProperty("智精灵 GS å…¬å¸ ID")
    private Integer platformGsId;
    @ApiModelProperty("计费开关 0关 1开")
    private Integer isPwr;
    @ApiModelProperty("18:00-09:00 ä¸åœæœº 0否 1是")
    private Integer isRestStop;
    @ApiModelProperty("备注")
    private String gsBz;
    @ApiModelProperty("欠费额度(元)")
    private BigDecimal stopMoney;
    @ApiModelProperty("剩余金额")
    private BigDecimal leftMoney;
    @ApiModelProperty("余额同步时间")
    private Date syncDate;
}
server/visits/dmvisit_service/src/main/java/com/doumee/dao/business/model/YwElectricalCharge.java
@@ -98,8 +98,14 @@
    @ApiModelProperty("充值前账号余额(元)")
    @ExcelColumn(name="充值前账号余额(元)",index=25 ,width=10)
    private BigDecimal banlance;
    @ApiModelProperty("充值后余额(元)")
    @ExcelColumn(name="充值后余额(元)",index=26 ,width=10)
    private BigDecimal balanceAfter;
    @ApiModelProperty("设备展示信息")
    @ExcelColumn(name="设备展示信息",index=27 ,width=10)
    private String deviceInfo;
    @ApiModelProperty("选择电表参数直接(yw_electrical_param)")
    @ExcelColumn(name="选择电表参数直接(yw_electrical_param)",index=26 ,width=10)
    @ExcelColumn(name="选择电表参数直接(yw_electrical_param)",index=28 ,width=10)
    private Integer paramId;
    @TableField(exist = false)
server/visits/dmvisit_service/src/main/java/com/doumee/service/business/YwCustomerRechargeBizService.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,56 @@
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.*;
import com.doumee.dao.business.model.YwConditioner;
import com.doumee.dao.business.model.YwCustomerGs;
import com.doumee.dao.business.model.YwElectrical;
import javax.servlet.http.HttpServletResponse;
import java.util.List;
import java.util.Map;
public interface YwCustomerRechargeBizService {
    PageData<YwCustomerRechargeMerchantVO> findMerchantPage(PageWrap<YwCustomerRechargeQueryDTO> pageWrap);
    YwCustomerRechargeDetailVO getDetail(Integer customerId);
    PageData<YwElectrical> listCustomerElectrical(PageWrap<YwElectrical> pageWrap, Integer customerId);
    PageData<YwElectrical> pageSelectableElectrical(PageWrap<YwElectrical> pageWrap, Integer customerId);
    void saveCustomerElectrical(YwCustomerElectricalSaveDTO dto, LoginUserInfo user);
    void removeCustomerElectrical(Integer customerId, Integer electricalId, LoginUserInfo user);
    PageData<YwConditioner> listCustomerConditioner(PageWrap<YwConditioner> pageWrap, Integer customerId);
    YwCustomerGs getCustomerGsConfig(Integer customerId);
    void saveCustomerGsConfig(YwCustomerGsConfigDTO dto, LoginUserInfo user);
    String rechargeElectrical(YwCustomerRechargeElectricalDTO dto, LoginUserInfo user);
    String resetElectricalAccount(YwCustomerRechargeElectricalDTO dto, LoginUserInfo user);
    Map<String, Object> readMeterAndRefresh(Integer customerId, Integer electricalId, LoginUserInfo user);
    Map<String, Object> getElectricalRemoteInfo(Integer electricalId);
    Map<String, Object> getConditionerRechargeInfo(Integer customerId);
    String rechargeConditioner(YwCustomerRechargeConditionerDTO dto, LoginUserInfo user);
    String cleanConditionerAccount(Integer customerId, LoginUserInfo user);
    PageData<YwCustomerRechargeRecordVO> findRechargeRecordPage(PageWrap<YwCustomerRechargeRecordQueryDTO> pageWrap);
    String retryRecharge(Integer id, LoginUserInfo user);
    String syncRechargeStatus(Integer id, LoginUserInfo user);
    void exportRechargeRecord(PageWrap<YwCustomerRechargeRecordQueryDTO> pageWrap, HttpServletResponse response);
}
server/visits/dmvisit_service/src/main/java/com/doumee/service/business/YwElectricalActionsService.java
@@ -10,4 +10,7 @@
public interface YwElectricalActionsService {
    PageData<YwElectricalActions> findPage(PageWrap<YwElectricalActions> pageWrap);
    /** ä¸»åŠ¨æŸ¥è¯¢å¼‚æ­¥ä»»åŠ¡çŠ¶æ€å¹¶åŒæ­¥æœ¬åœ°æ“ä½œ/充值结果 */
    String queryAsyncResult(Integer id);
}
server/visits/dmvisit_service/src/main/java/com/doumee/service/business/YwElectricalBizService.java
@@ -21,6 +21,15 @@
    boolean handleElectricalNotify(String responseContent, String timestamp, String sign);
    /** ä¸»åŠ¨æŸ¥è¯¢å•ä¸ªå¼‚æ­¥ä»»åŠ¡çŠ¶æ€å¹¶åŒæ­¥æœ¬åœ°è®°å½• */
    String syncAsyncActionStatus(String oprId);
    /** æŒ‰å……值记录 ID æ‰‹åŠ¨åŒæ­¥ï¼ˆä»…å……å€¼ä¸­ç”µè¡¨è®°å½•ï¼‰ */
    String syncChargeStatusById(Integer chargeId);
    /** å®šæ—¶æ‰¹é‡æŸ¥è¯¢å¤„理中的异步任务状态(回调丢失时的补偿) */
    void syncPendingAsyncActionsScheduled();
    void syncMeterDataScheduled();
    /** æ‰‹åŠ¨ä»Žç¬¬ä¸‰æ–¹å¹³å°æ‹‰å–æŠ„è¡¨æ•°æ®å…¥åº“ */
server/visits/dmvisit_service/src/main/java/com/doumee/service/business/impl/YwCustomerRechargeBizServiceImpl.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,943 @@
package com.doumee.service.business.impl;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
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.annotation.excel.ExcelExporter;
import com.doumee.core.conditoner.ConditionerUtil;
import com.doumee.core.conditoner.model.request.AddMoneyRequest;
import com.doumee.core.conditoner.model.request.CompanyGsManageRequest;
import com.doumee.core.conditoner.model.request.LogQueryRequest;
import com.doumee.core.conditoner.model.response.CompanyGsInfoResponse;
import com.doumee.core.conditoner.model.response.ConditionerBaseResponse;
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.*;
import com.doumee.dao.business.dto.*;
import com.doumee.dao.business.model.*;
import com.doumee.service.business.ConditionerBizService;
import com.doumee.service.business.YwCustomerRechargeBizService;
import com.doumee.service.business.YwElectricalBizService;
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 javax.servlet.http.HttpServletResponse;
import java.math.BigDecimal;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.stream.Collectors;
@Service
public class YwCustomerRechargeBizServiceImpl implements YwCustomerRechargeBizService {
    private static final String ONLINE_TEXT = "在线";
    @Autowired
    private YwCustomerMapper ywCustomerMapper;
    @Autowired
    private MemberMapper memberMapper;
    @Autowired
    private YwCustomerGsMapper ywCustomerGsMapper;
    @Autowired
    private YwCustomerElectricalMapper ywCustomerElectricalMapper;
    @Autowired
    private YwCustomerConditionerMapper ywCustomerConditionerMapper;
    @Autowired
    private YwElectricalMapper ywElectricalMapper;
    @Autowired
    private YwConditionerMapper ywConditionerMapper;
    @Autowired
    private YwElectricalChargeMapper ywElectricalChargeMapper;
    @Autowired
    private YwElectricalActionsMapper ywElectricalActionsMapper;
    @Autowired
    private YwElectricalBizService ywElectricalBizService;
    @Autowired
    private ConditionerBizService conditionerBizService;
    @Override
    public PageData<YwCustomerRechargeMerchantVO> findMerchantPage(PageWrap<YwCustomerRechargeQueryDTO> pageWrap) {
        YwCustomerRechargeQueryDTO query = pageWrap.getModel() != null ? pageWrap.getModel() : new YwCustomerRechargeQueryDTO();
        boolean hasDeviceFilter = query.getElectricalStatusFilter() != null || query.getConditionerStatusFilter() != null;
        if (hasDeviceFilter) {
            List<YwCustomer> all = ywCustomerMapper.selectList(new QueryWrapper<YwCustomer>().lambda()
                    .eq(YwCustomer::getIsdeleted, Constants.ZERO)
                    .like(StringUtils.isNotBlank(query.getNameKeyword()), YwCustomer::getName, query.getNameKeyword())
                    .orderByDesc(YwCustomer::getCreateDate));
            List<YwCustomerRechargeMerchantVO> enriched = enrichMerchantList(all);
            List<YwCustomerRechargeMerchantVO> filtered = enriched.stream()
                    .filter(vo -> matchDeviceFilter(vo, query))
                    .collect(Collectors.toList());
            return manualPage(filtered, pageWrap.getPage(), pageWrap.getCapacity());
        }
        IPage<YwCustomer> page = new Page<>(pageWrap.getPage(), pageWrap.getCapacity());
        IPage<YwCustomer> result = ywCustomerMapper.selectPage(page, new QueryWrapper<YwCustomer>().lambda()
                .eq(YwCustomer::getIsdeleted, Constants.ZERO)
                .like(StringUtils.isNotBlank(query.getNameKeyword()), YwCustomer::getName, query.getNameKeyword())
                .orderByDesc(YwCustomer::getCreateDate));
        List<YwCustomerRechargeMerchantVO> list = enrichMerchantList(result.getRecords());
        PageData<YwCustomerRechargeMerchantVO> data = new PageData<>();
        data.setRecords(list);
        data.setTotal(result.getTotal());
        data.setPage(result.getCurrent());
        data.setCapacity(result.getSize());
        return data;
    }
    private PageData<YwCustomerRechargeMerchantVO> manualPage(List<YwCustomerRechargeMerchantVO> list, long page, long capacity) {
        int p = (int) Math.max(page, 1);
        int size = (int) Math.max(capacity, 1);
        int from = (p - 1) * size;
        int to = Math.min(from + size, list.size());
        PageData<YwCustomerRechargeMerchantVO> data = new PageData<>();
        data.setTotal(list.size());
        data.setPage(p);
        data.setCapacity(size);
        data.setRecords(from >= list.size() ? Collections.emptyList() : list.subList(from, to));
        return data;
    }
    private boolean matchDeviceFilter(YwCustomerRechargeMerchantVO vo, YwCustomerRechargeQueryDTO query) {
        if (query.getElectricalStatusFilter() != null && !matchStatusFilter(
                query.getElectricalStatusFilter(), vo.getElectricalCount(), vo.getElectricalOnlineAll(), vo.getElectricalHasOffline())) {
            return false;
        }
        if (query.getConditionerStatusFilter() != null && !matchStatusFilter(
                query.getConditionerStatusFilter(), vo.getConditionerCount(), vo.getConditionerOnlineAll(), vo.getConditionerHasOffline())) {
            return false;
        }
        return true;
    }
    private boolean matchStatusFilter(Integer filter, Integer count, Boolean allOnline, Boolean hasOffline) {
        int c = count != null ? count : 0;
        if (filter == 3) {
            return c == 0;
        }
        if (c == 0) {
            return false;
        }
        if (filter == 1) {
            return Boolean.TRUE.equals(allOnline);
        }
        if (filter == 2) {
            return Boolean.TRUE.equals(hasOffline);
        }
        return true;
    }
    private List<YwCustomerRechargeMerchantVO> enrichMerchantList(List<YwCustomer> customers) {
        if (CollectionUtils.isEmpty(customers)) {
            return Collections.emptyList();
        }
        List<Integer> customerIds = customers.stream().map(YwCustomer::getId).collect(Collectors.toList());
        Map<Integer, YwCustomerGs> gsMap = ywCustomerGsMapper.selectList(new QueryWrapper<YwCustomerGs>().lambda()
                        .eq(YwCustomerGs::getIsdeleted, Constants.ZERO)
                        .in(YwCustomerGs::getCustomerId, customerIds))
                .stream().collect(Collectors.toMap(YwCustomerGs::getCustomerId, g -> g, (a, b) -> a));
        List<YwCustomerElectrical> relE = ywCustomerElectricalMapper.selectList(new QueryWrapper<YwCustomerElectrical>().lambda()
                .eq(YwCustomerElectrical::getIsdeleted, Constants.ZERO)
                .in(YwCustomerElectrical::getCustomerId, customerIds));
        Map<Integer, List<Integer>> customerElectricalIds = relE.stream()
                .collect(Collectors.groupingBy(YwCustomerElectrical::getCustomerId,
                        Collectors.mapping(YwCustomerElectrical::getElectricalId, Collectors.toList())));
        List<YwCustomerConditioner> relC = ywCustomerConditionerMapper.selectList(new QueryWrapper<YwCustomerConditioner>().lambda()
                .eq(YwCustomerConditioner::getIsdeleted, Constants.ZERO)
                .in(YwCustomerConditioner::getCustomerId, customerIds));
        Map<Integer, List<Integer>> customerConditionerIds = relC.stream()
                .collect(Collectors.groupingBy(YwCustomerConditioner::getCustomerId,
                        Collectors.mapping(YwCustomerConditioner::getConditionerId, Collectors.toList())));
        Set<Integer> allElectricalIds = relE.stream().map(YwCustomerElectrical::getElectricalId).collect(Collectors.toSet());
        Map<Integer, YwElectrical> electricalMap = allElectricalIds.isEmpty() ? Collections.emptyMap()
                : ywElectricalMapper.selectBatchIds(allElectricalIds).stream()
                .filter(e -> !Objects.equals(e.getIsdeleted(), Constants.ONE))
                .collect(Collectors.toMap(YwElectrical::getId, e -> e, (a, b) -> a));
        Set<Integer> allConditionerIds = relC.stream().map(YwCustomerConditioner::getConditionerId).collect(Collectors.toSet());
        Map<Integer, YwConditioner> conditionerMap = allConditionerIds.isEmpty() ? Collections.emptyMap()
                : ywConditionerMapper.selectBatchIds(allConditionerIds).stream()
                .filter(c -> !Objects.equals(c.getIsdeleted(), Constants.ONE))
                .collect(Collectors.toMap(YwConditioner::getId, c -> c, (a, b) -> a));
        Set<Integer> memberIds = customers.stream()
                .map(YwCustomer::getMemberId)
                .filter(Objects::nonNull)
                .collect(Collectors.toSet());
        Map<Integer, Member> memberMap = memberIds.isEmpty() ? Collections.emptyMap()
                : memberMapper.selectBatchIds(memberIds).stream()
                .filter(m -> !Objects.equals(m.getIsdeleted(), Constants.ONE))
                .collect(Collectors.toMap(Member::getId, m -> m, (a, b) -> a));
        List<YwCustomerRechargeMerchantVO> list = new ArrayList<>();
        for (YwCustomer c : customers) {
            YwCustomerRechargeMerchantVO vo = new YwCustomerRechargeMerchantVO();
            vo.setId(c.getId());
            vo.setType(c.getType());
            vo.setName(c.getName());
            vo.setPhone(c.getPhone());
            vo.setCreateDate(c.getCreateDate());
            Member member = c.getMemberId() != null ? memberMap.get(c.getMemberId()) : null;
            if (member != null) {
                vo.setMemberName(member.getName());
                vo.setMemberPhone(member.getPhone());
            }
            List<Integer> eIds = customerElectricalIds.getOrDefault(c.getId(), Collections.emptyList());
            vo.setElectricalCount(eIds.size());
            if (eIds.isEmpty()) {
                vo.setElectricalBalances(Collections.emptyList());
                vo.setElectricalOnlineAll(false);
                vo.setElectricalHasOffline(false);
            } else {
                List<YwCustomerElectricalBalanceItem> balanceItems = new ArrayList<>();
                boolean allOnline = true;
                boolean hasOffline = false;
                int index = 0;
                for (Integer eid : eIds) {
                    YwElectrical e = electricalMap.get(eid);
                    if (e == null) continue;
                    YwCustomerElectricalBalanceItem item = new YwCustomerElectricalBalanceItem();
                    index++;
                    item.setName(StringUtils.isNotBlank(e.getName()) ? e.getName() : ("电表" + index));
                    item.setAddress(StringUtils.isNotBlank(e.getAddress()) ? e.getAddress() : "-");
                    item.setBalance(e.getBalance() != null ? e.getBalance() : BigDecimal.ZERO);
                    balanceItems.add(item);
                    if (!Objects.equals(e.getOnline(), Constants.ONE)) {
                        allOnline = false;
                        hasOffline = true;
                    }
                }
                vo.setElectricalBalances(balanceItems);
                vo.setElectricalOnlineAll(allOnline && !balanceItems.isEmpty());
                vo.setElectricalHasOffline(hasOffline);
            }
            List<Integer> cIds = customerConditionerIds.getOrDefault(c.getId(), Collections.emptyList());
            vo.setConditionerCount(cIds.size());
            if (cIds.isEmpty()) {
                vo.setConditionerOnlineAll(false);
                vo.setConditionerHasOffline(false);
            } else {
                boolean allOnline = true;
                boolean hasOffline = false;
                for (Integer cid : cIds) {
                    YwConditioner cond = conditionerMap.get(cid);
                    if (cond == null) continue;
                    if (!ONLINE_TEXT.equals(cond.getOnline())) {
                        allOnline = false;
                        hasOffline = true;
                    }
                }
                vo.setConditionerOnlineAll(allOnline);
                vo.setConditionerHasOffline(hasOffline);
            }
            YwCustomerGs gs = gsMap.get(c.getId());
            if (gs != null) {
                vo.setAcBalance(gs.getLeftMoney());
                vo.setAcBalanceSyncDate(gs.getSyncDate());
            }
            list.add(vo);
        }
        return list;
    }
    @Override
    public YwCustomerRechargeDetailVO getDetail(Integer customerId) {
        YwCustomer customer = requireCustomer(customerId);
        YwCustomerRechargeDetailVO vo = new YwCustomerRechargeDetailVO();
        vo.setCustomerId(customer.getId());
        vo.setCustomerName(customer.getName());
        vo.setPhone(customer.getPhone());
        vo.setGsConfig(getCustomerGsConfig(customerId));
        vo.setElectricalList(loadCustomerElectricalList(customerId));
        vo.setConditionerList(loadCustomerConditionerList(customerId));
        return vo;
    }
    @Override
    public PageData<YwElectrical> listCustomerElectrical(PageWrap<YwElectrical> pageWrap, Integer customerId) {
        requireCustomer(customerId);
        List<Integer> ids = listBoundElectricalIds(customerId);
        if (ids.isEmpty()) {
            return emptyPage(pageWrap);
        }
        return pageElectricalByIds(pageWrap, ids);
    }
    @Override
    public PageData<YwElectrical> pageSelectableElectrical(PageWrap<YwElectrical> pageWrap, Integer customerId) {
        requireCustomer(customerId);
        Set<Integer> excluded = listAllBoundElectricalIds();
        IPage<YwElectrical> page = new Page<>(pageWrap.getPage(), pageWrap.getCapacity());
        YwElectrical model = pageWrap.getModel();
        QueryWrapper<YwElectrical> qw = new QueryWrapper<>();
        qw.lambda()
                .eq(YwElectrical::getIsdeleted, Constants.ZERO)
                .notIn(!excluded.isEmpty(), YwElectrical::getId, excluded)
                .and(model != null && StringUtils.isNotBlank(model.getName()), w -> w
                        .like(YwElectrical::getName, model.getName())
                        .or().like(YwElectrical::getAddress, model.getName()))
                .orderByDesc(YwElectrical::getCreateDate);
        IPage<YwElectrical> result = ywElectricalMapper.selectPage(page, qw);
        ywElectricalBizService.enrichList(result.getRecords());
        return PageData.from(result);
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void saveCustomerElectrical(YwCustomerElectricalSaveDTO dto, LoginUserInfo user) {
        requireCustomer(dto.getCustomerId());
        if (CollectionUtils.isEmpty(dto.getElectricalIds())) {
            throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "请选择电表");
        }
        Set<Integer> excluded = listAllBoundElectricalIdsExcept(dto.getCustomerId());
        for (Integer eid : dto.getElectricalIds()) {
            if (excluded.contains(eid)) {
                throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "电表已被其他商户绑定:" + eid);
            }
            YwCustomerElectrical exist = ywCustomerElectricalMapper.selectOne(new QueryWrapper<YwCustomerElectrical>().lambda()
                    .eq(YwCustomerElectrical::getCustomerId, dto.getCustomerId())
                    .eq(YwCustomerElectrical::getElectricalId, eid)
                    .eq(YwCustomerElectrical::getIsdeleted, Constants.ZERO)
                    .last("limit 1"));
            if (exist != null) {
                continue;
            }
            YwCustomerElectrical rel = new YwCustomerElectrical();
            rel.setCreator(user.getId());
            rel.setCreateDate(new Date());
            rel.setEditor(user.getId());
            rel.setEditDate(new Date());
            rel.setIsdeleted(Constants.ZERO);
            rel.setCustomerId(dto.getCustomerId());
            rel.setElectricalId(eid);
            ywCustomerElectricalMapper.insert(rel);
        }
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void removeCustomerElectrical(Integer customerId, Integer electricalId, LoginUserInfo user) {
        ywCustomerElectricalMapper.update(null, new UpdateWrapper<YwCustomerElectrical>().lambda()
                .set(YwCustomerElectrical::getIsdeleted, Constants.ONE)
                .set(YwCustomerElectrical::getEditDate, new Date())
                .set(YwCustomerElectrical::getEditor, user.getId())
                .eq(YwCustomerElectrical::getCustomerId, customerId)
                .eq(YwCustomerElectrical::getElectricalId, electricalId)
                .eq(YwCustomerElectrical::getIsdeleted, Constants.ZERO));
    }
    @Override
    public PageData<YwConditioner> listCustomerConditioner(PageWrap<YwConditioner> pageWrap, Integer customerId) {
        requireCustomer(customerId);
        List<YwConditioner> list = loadCustomerConditionerList(customerId);
        if (CollectionUtils.isEmpty(list)) {
            return emptyPage(pageWrap);
        }
        int p = (int) Math.max(pageWrap.getPage(), 1);
        int size = (int) Math.max(pageWrap.getCapacity(), 1);
        int from = (p - 1) * size;
        int to = Math.min(from + size, list.size());
        PageData<YwConditioner> data = new PageData<>();
        data.setTotal(list.size());
        data.setPage(p);
        data.setCapacity(size);
        data.setRecords(from >= list.size() ? Collections.emptyList() : list.subList(from, to));
        return data;
    }
    @Override
    public YwCustomerGs getCustomerGsConfig(Integer customerId) {
        return ywCustomerGsMapper.selectOne(new QueryWrapper<YwCustomerGs>().lambda()
                .eq(YwCustomerGs::getCustomerId, customerId)
                .eq(YwCustomerGs::getIsdeleted, Constants.ZERO)
                .last("limit 1"));
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void saveCustomerGsConfig(YwCustomerGsConfigDTO dto, LoginUserInfo user) {
        YwCustomer customer = requireCustomer(dto.getCustomerId());
        if (CollectionUtils.isEmpty(dto.getConditioners())) {
            throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "请至少关联一台空调内机");
        }
        conditionerBizService.ensureLogin();
        List<Integer> conditionerIds = dto.getConditioners().stream()
                .map(YwCustomerGsConfigDTO.ConditionerItem::getConditionerId)
                .filter(Objects::nonNull)
                .collect(Collectors.toList());
        List<YwConditioner> conditioners = ywConditionerMapper.selectBatchIds(conditionerIds);
        Map<Integer, YwConditioner> condMap = conditioners.stream()
                .filter(c -> !Objects.equals(c.getIsdeleted(), Constants.ONE))
                .collect(Collectors.toMap(YwConditioner::getId, c -> c, (a, b) -> a));
        List<Integer> liDev = new ArrayList<>();
        Map<String, Object> dDev = new LinkedHashMap<>();
        for (YwCustomerGsConfigDTO.ConditionerItem item : dto.getConditioners()) {
            YwConditioner cond = condMap.get(item.getConditionerId());
            if (cond == null || cond.getPlatformDevId() == null) {
                throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "内机缺少平台设备 ID");
            }
            liDev.add(cond.getPlatformDevId());
            int ratio = item.getDevRatio() != null ? item.getDevRatio() : 100;
            dDev.put(String.valueOf(cond.getPlatformDevId()), ratio);
        }
        YwCustomerGs gs = getCustomerGsConfig(dto.getCustomerId());
        if (gs == null) {
            gs = new YwCustomerGs();
            gs.setCreator(user.getId());
            gs.setCreateDate(new Date());
            gs.setIsdeleted(Constants.ZERO);
            gs.setCustomerId(dto.getCustomerId());
        }
        gs.setEditor(user.getId());
        gs.setEditDate(new Date());
        gs.setIsPwr(dto.getIsPwr() != null ? dto.getIsPwr() : Constants.ONE);
        gs.setIsRestStop(dto.getIsRestStop() != null ? dto.getIsRestStop() : Constants.ZERO);
        gs.setGsBz(StringUtils.defaultString(dto.getGsBz()));
        gs.setStopMoney(dto.getStopMoney() != null ? dto.getStopMoney() : BigDecimal.ZERO);
        if (StringUtils.isBlank(customer.getName())) {
            throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "客户名称不能为空");
        }
        if (liDev.isEmpty()) {
            throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "关联内机平台设备 ID æ— æ•ˆ");
        }
        CompanyGsManageRequest companyReq = new CompanyGsManageRequest();
        companyReq.fillSessionDefaults();
        companyReq.setGs_name(customer.getName().trim());
        companyReq.setIs_pwr(gs.getIsPwr());
        companyReq.setIs_rest_stop(gs.getIsRestStop());
        companyReq.setGs_bz(gs.getGsBz());
        companyReq.setStop_money(gs.getStopMoney());
        if (gs.getPlatformGsId() == null) {
            ConditionerBaseResponse<Object> addResp = ConditionerUtil.addGs(companyReq);
            if (addResp == null || !addResp.isSuccess()) {
                throw new BusinessException(ResponseStatus.SERVER_ERROR.getCode(), apiMsg(addResp, "addGs å¤±è´¥"));
            }
            gs.setPlatformGsId(parseAddGsId(addResp.getData()));
            if (gs.getPlatformGsId() == null) {
                throw new BusinessException(ResponseStatus.SERVER_ERROR.getCode(), "addGs æœªè¿”回公司 ID");
            }
        }
        CompanyGsManageRequest changeReq = new CompanyGsManageRequest();
        changeReq.fillSessionDefaults();
        changeReq.setId(gs.getPlatformGsId());
        changeReq.setGs_name(companyReq.getGs_name());
        changeReq.setIs_pwr(companyReq.getIs_pwr());
        changeReq.setIs_rest_stop(companyReq.getIs_rest_stop());
        changeReq.setGs_bz(companyReq.getGs_bz());
        changeReq.setStop_money(gs.getStopMoney());
        changeReq.setLi_dev(liDev);
        changeReq.setD_dev(dDev);
        if (gs.getLeftMoney() != null) {
            changeReq.setLeft_money(gs.getLeftMoney());
        }
        ConditionerBaseResponse<Object> changeResp = ConditionerUtil.changeGs(changeReq);
        if (changeResp == null || !changeResp.isSuccess()) {
            throw new BusinessException(ResponseStatus.SERVER_ERROR.getCode(), apiMsg(changeResp, "changeGs å¤±è´¥"));
        }
        refreshGsLeftMoney(gs);
        if (gs.getId() == null) {
            ywCustomerGsMapper.insert(gs);
        } else {
            ywCustomerGsMapper.updateById(gs);
        }
        ywCustomerConditionerMapper.update(null, new UpdateWrapper<YwCustomerConditioner>().lambda()
                .set(YwCustomerConditioner::getIsdeleted, Constants.ONE)
                .set(YwCustomerConditioner::getEditDate, new Date())
                .set(YwCustomerConditioner::getEditor, user.getId())
                .eq(YwCustomerConditioner::getCustomerId, dto.getCustomerId())
                .eq(YwCustomerConditioner::getIsdeleted, Constants.ZERO));
        for (YwCustomerGsConfigDTO.ConditionerItem item : dto.getConditioners()) {
            YwCustomerConditioner rel = new YwCustomerConditioner();
            rel.setCreator(user.getId());
            rel.setCreateDate(new Date());
            rel.setEditor(user.getId());
            rel.setEditDate(new Date());
            rel.setIsdeleted(Constants.ZERO);
            rel.setCustomerId(dto.getCustomerId());
            rel.setConditionerId(item.getConditionerId());
            rel.setDevRatio(item.getDevRatio() != null ? item.getDevRatio() : 100);
            ywCustomerConditionerMapper.insert(rel);
        }
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public String rechargeElectrical(YwCustomerRechargeElectricalDTO dto, LoginUserInfo user) {
        requireCustomer(dto.getCustomerId());
        assertElectricalBound(dto.getCustomerId(), dto.getElectricalId());
        YwElectricalOperateDTO op = new YwElectricalOperateDTO();
        op.setElectricalId(dto.getElectricalId());
        op.setAction("recharge");
        op.setMoney(dto.getMoney());
        op.setRemark(dto.getRemark());
        op.setCustomerId(dto.getCustomerId());
        return ywElectricalBizService.operate(op, user);
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public String resetElectricalAccount(YwCustomerRechargeElectricalDTO dto, LoginUserInfo user) {
        requireCustomer(dto.getCustomerId());
        assertElectricalBound(dto.getCustomerId(), dto.getElectricalId());
        String action = StringUtils.defaultIfBlank(dto.getResetAction(), "resetPrepay");
        YwElectricalOperateDTO op = new YwElectricalOperateDTO();
        op.setElectricalId(dto.getElectricalId());
        op.setAction(action);
        op.setCustomerId(dto.getCustomerId());
        return ywElectricalBizService.operate(op, user);
    }
    @Override
    public Map<String, Object> getElectricalRemoteInfo(Integer electricalId) {
        return ywElectricalBizService.getRemoteInfo(electricalId);
    }
    @Override
    public Map<String, Object> readMeterAndRefresh(Integer customerId, Integer electricalId, LoginUserInfo user) {
        assertElectricalBound(customerId, electricalId);
        YwElectricalOperateDTO op = new YwElectricalOperateDTO();
        op.setElectricalId(electricalId);
        op.setAction("readMeter");
        op.setCustomerId(customerId);
        String msg = ywElectricalBizService.operate(op, user);
        Map<String, Object> info = ywElectricalBizService.getRemoteInfo(electricalId);
        info.put("message", msg);
        return info;
    }
    @Override
    public Map<String, Object> getConditionerRechargeInfo(Integer customerId) {
        YwCustomerGs gs = requireGsConfig(customerId);
        conditionerBizService.ensureLogin();
        refreshGsLeftMoney(gs);
        ywCustomerGsMapper.updateById(gs);
        Map<String, Object> map = new LinkedHashMap<>();
        map.put("gsConfig", gs);
        map.put("customerName", requireCustomer(customerId).getName());
        CompanyGsManageRequest req = new CompanyGsManageRequest();
        req.fillSessionDefaults();
        req.setId(gs.getPlatformGsId());
        ConditionerBaseResponse<List<CompanyGsInfoResponse>> resp = ConditionerUtil.getGs(req);
        if (resp != null && resp.isSuccess() && !CollectionUtils.isEmpty(resp.getData())) {
            CompanyGsInfoResponse info = resp.getData().get(0);
            map.put("platformInfo", info);
        }
        return map;
    }
    @Override
    public String rechargeConditioner(YwCustomerRechargeConditionerDTO dto, LoginUserInfo user) {
        YwCustomerGs gs = requireGsConfig(dto.getCustomerId());
        if (dto.getMoney() == null || dto.getMoney().compareTo(BigDecimal.ZERO) <= 0) {
            throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "充值金额须大于 0");
        }
        conditionerBizService.ensureLogin();
        AddMoneyRequest req = new AddMoneyRequest();
        req.fillSessionDefaults();
        req.setId(gs.getPlatformGsId());
        req.setCz_money(dto.getMoney().toPlainString());
        ConditionerBaseResponse<Object> resp = ConditionerUtil.addMoney(req);
        YwElectricalCharge charge = buildConditionerCharge(dto, gs, user);
        if (resp != null && resp.isSuccess()) {
            refreshGsLeftMoney(gs);
            ywCustomerGsMapper.updateById(gs);
            charge.setStatus(Constants.ONE);
            charge.setStatusInfo("充值成功");
            charge.setBalanceAfter(gs.getLeftMoney());
            ywElectricalChargeMapper.insert(charge);
            return "充值成功";
        }
        charge.setStatus(Constants.TWO);
        charge.setStatusInfo(apiMsg(resp, "充值失败"));
        ywElectricalChargeMapper.insert(charge);
        throw new BusinessException(ResponseStatus.SERVER_ERROR.getCode(), charge.getStatusInfo());
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public String cleanConditionerAccount(Integer customerId, LoginUserInfo user) {
        YwCustomerGs gs = requireGsConfig(customerId);
        conditionerBizService.ensureLogin();
        CompanyGsManageRequest req = new CompanyGsManageRequest();
        req.fillSessionDefaults();
        req.setId(gs.getPlatformGsId());
        ConditionerBaseResponse<Object> resp = ConditionerUtil.cleanMoney(req);
        if (resp == null || !resp.isSuccess()) {
            throw new BusinessException(ResponseStatus.SERVER_ERROR.getCode(), apiMsg(resp, "清零失败"));
        }
        refreshGsLeftMoney(gs);
        gs.setEditor(user.getId());
        gs.setEditDate(new Date());
        ywCustomerGsMapper.updateById(gs);
        return "清零成功";
    }
    @Override
    public PageData<YwCustomerRechargeRecordVO> findRechargeRecordPage(PageWrap<YwCustomerRechargeRecordQueryDTO> pageWrap) {
        Utils.MP.blankToNull(pageWrap.getModel());
        YwCustomerRechargeRecordQueryDTO query = pageWrap.getModel() != null ? pageWrap.getModel() : new YwCustomerRechargeRecordQueryDTO();
        IPage<YwCustomerRechargeRecordVO> page = new Page<>(pageWrap.getPage(), pageWrap.getCapacity());
        MPJLambdaWrapper<YwElectricalCharge> wrapper = new MPJLambdaWrapper<YwElectricalCharge>()
                .selectAll(YwElectricalCharge.class)
                .selectAs(YwCustomer::getName, YwCustomerRechargeRecordVO::getCustomerName)
                .leftJoin(YwCustomer.class, YwCustomer::getId, YwElectricalCharge::getCustomerId)
                .isNotNull(YwElectricalCharge::getCustomerId)
                .eq(YwElectricalCharge::getIsdeleted, Constants.ZERO)
                .eq(query.getType() != null, YwElectricalCharge::getType, query.getType())
                .eq(query.getStatus() != null, YwElectricalCharge::getStatus, query.getStatus())
                .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())
                .orderByDesc(YwElectricalCharge::getCreateDate);
        IPage<YwCustomerRechargeRecordVO> result = ywElectricalChargeMapper.selectJoinPage(page, YwCustomerRechargeRecordVO.class, wrapper);
        result.getRecords().forEach(this::fillRecordText);
        return PageData.from(result);
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public String retryRecharge(Integer id, LoginUserInfo user) {
        YwElectricalCharge charge = requireCharge(id);
        if (!Objects.equals(charge.getStatus(), Constants.TWO)) {
            throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "仅失败记录可再次提交");
        }
        if (Objects.equals(charge.getType(), Constants.ZERO)) {
            YwCustomerRechargeElectricalDTO dto = new YwCustomerRechargeElectricalDTO();
            dto.setCustomerId(charge.getCustomerId());
            dto.setElectricalId(charge.getObjId());
            dto.setMoney(charge.getMoney());
            dto.setRemark(charge.getRemark());
            return rechargeElectrical(dto, user);
        }
        if (Objects.equals(charge.getType(), Constants.ONE)) {
            YwCustomerRechargeConditionerDTO dto = new YwCustomerRechargeConditionerDTO();
            dto.setCustomerId(charge.getCustomerId());
            dto.setMoney(charge.getMoney());
            dto.setRemark(charge.getRemark());
            return rechargeConditioner(dto, user);
        }
        throw new BusinessException(ResponseStatus.BAD_REQUEST);
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public String syncRechargeStatus(Integer id, LoginUserInfo user) {
        YwElectricalCharge charge = requireCharge(id);
        if (!Objects.equals(charge.getStatus(), Constants.ZERO)) {
            throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "仅充值中记录可同步");
        }
        if (Objects.equals(charge.getType(), Constants.ZERO)) {
            return syncElectricalChargeStatus(charge, user);
        }
        if (Objects.equals(charge.getType(), Constants.ONE)) {
            return syncConditionerChargeStatus(charge, user);
        }
        throw new BusinessException(ResponseStatus.BAD_REQUEST);
    }
    @Override
    public void exportRechargeRecord(PageWrap<YwCustomerRechargeRecordQueryDTO> pageWrap, HttpServletResponse response) {
        pageWrap.setPage(1);
        pageWrap.setCapacity(Integer.MAX_VALUE);
        List<YwCustomerRechargeRecordVO> records = findRechargeRecordPage(pageWrap).getRecords();
        ExcelExporter.build(YwCustomerRechargeRecordVO.class).export(records, "商户充值记录", response);
    }
    private String syncElectricalChargeStatus(YwElectricalCharge charge, LoginUserInfo user) {
        return ywElectricalBizService.syncAsyncActionStatus(charge.getOprId());
    }
    private String syncConditionerChargeStatus(YwElectricalCharge charge, LoginUserInfo user) {
        YwCustomerGs gs = requireGsConfig(charge.getCustomerId());
        conditionerBizService.ensureLogin();
        LogQueryRequest req = new LogQueryRequest();
        req.fillSessionDefaults();
        req.setPage(1);
        req.setPageSize(50);
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
        if (charge.getCreateDate() != null) {
            String day = sdf.format(charge.getCreateDate());
            req.setStart_time(day);
            req.setEnd_time(day);
        }
        ConditionerBaseResponse<List<Object>> resp = ConditionerUtil.getCzLog(req);
        if (resp == null || !resp.isSuccess() || CollectionUtils.isEmpty(resp.getData())) {
            return "未匹配到平台充值日志,仍为充值中";
        }
        for (Object row : resp.getData()) {
            JSONObject o = (JSONObject) JSON.toJSON(row);
            if (!matchCzLog(o, gs.getPlatformGsId(), charge)) {
                continue;
            }
            refreshGsLeftMoney(gs);
            ywCustomerGsMapper.updateById(gs);
            updateChargeStatus(charge.getId(), Constants.ONE, "充值成功", gs.getLeftMoney());
            return "已同步为充值成功";
        }
        return "未匹配到平台充值日志,仍为充值中";
    }
    private boolean matchCzLog(JSONObject o, Integer gsId, YwElectricalCharge charge) {
        Integer logGsId = o.getInteger("gs_id");
        if (logGsId == null) {
            logGsId = o.getInteger("id");
        }
        if (!Objects.equals(logGsId, gsId)) {
            return false;
        }
        String money = o.getString("cz_money");
        if (money == null) {
            money = o.getString("money");
        }
        if (charge.getMoney() != null && money != null) {
            try {
                return charge.getMoney().compareTo(new BigDecimal(money)) == 0;
            } catch (NumberFormatException ignored) {
            }
        }
        return true;
    }
    private void updateChargeStatus(Integer id, int status, String statusInfo, BigDecimal balanceAfter) {
        UpdateWrapper<YwElectricalCharge> uw = new UpdateWrapper<>();
        uw.lambda()
                .set(YwElectricalCharge::getStatus, status)
                .set(YwElectricalCharge::getStatusInfo, statusInfo)
                .set(YwElectricalCharge::getStatusTime, new Date())
                .set(YwElectricalCharge::getEditDate, new Date())
                .eq(YwElectricalCharge::getId, id);
        if (balanceAfter != null) {
            uw.lambda().set(YwElectricalCharge::getBalanceAfter, balanceAfter);
        }
        ywElectricalChargeMapper.update(null, uw);
    }
    private YwElectricalCharge buildConditionerCharge(YwCustomerRechargeConditionerDTO dto, YwCustomerGs gs, LoginUserInfo user) {
        YwCustomer customer = requireCustomer(dto.getCustomerId());
        YwElectricalCharge charge = new YwElectricalCharge();
        charge.setCreator(user.getId());
        charge.setCreateDate(new Date());
        charge.setEditor(user.getId());
        charge.setEditDate(new Date());
        charge.setIsdeleted(Constants.ZERO);
        charge.setType(Constants.ONE);
        charge.setCustomerId(dto.getCustomerId());
        charge.setObjId(gs.getId());
        charge.setName(customer.getName());
        charge.setMoney(dto.getMoney());
        charge.setRemark(dto.getRemark());
        charge.setBanlance(gs.getLeftMoney());
        charge.setStatus(Constants.ZERO);
        charge.setStatusTime(new Date());
        charge.setStatusInfo("充值中");
        charge.setDeviceInfo("GS-" + gs.getPlatformGsId() + " " + customer.getName());
        return charge;
    }
    private void fillRecordText(YwCustomerRechargeRecordVO vo) {
        if (StringUtils.isBlank(vo.getDeviceInfo())) {
            if (StringUtils.isNotBlank(vo.getAddress()) || StringUtils.isNotBlank(vo.getName())) {
                vo.setDeviceInfo(StringUtils.defaultString(vo.getAddress()) + " " + StringUtils.defaultString(vo.getName()));
            }
        }
        vo.setTypeText(Objects.equals(vo.getType(), Constants.ONE) ? "空调" : "电表");
        if (Objects.equals(vo.getStatus(), Constants.ZERO)) {
            vo.setStatusText("充值中");
        } else if (Objects.equals(vo.getStatus(), Constants.ONE)) {
            vo.setStatusText("充值成功");
        } else if (Objects.equals(vo.getStatus(), Constants.TWO)) {
            vo.setStatusText("充值失败");
        }
    }
    private YwElectricalCharge requireCharge(Integer id) {
        YwElectricalCharge charge = ywElectricalChargeMapper.selectById(id);
        if (charge == null || Objects.equals(charge.getIsdeleted(), Constants.ONE) || charge.getCustomerId() == null) {
            throw new BusinessException(ResponseStatus.DATA_EMPTY);
        }
        return charge;
    }
    private void refreshGsLeftMoney(YwCustomerGs gs) {
        if (gs.getPlatformGsId() == null) {
            return;
        }
        CompanyGsManageRequest req = new CompanyGsManageRequest();
        req.fillSessionDefaults();
        req.setId(gs.getPlatformGsId());
        ConditionerBaseResponse<List<CompanyGsInfoResponse>> resp = ConditionerUtil.getGs(req);
        if (resp == null || !resp.isSuccess() || CollectionUtils.isEmpty(resp.getData())) {
            return;
        }
        CompanyGsInfoResponse info = resp.getData().stream()
                .filter(i -> Objects.equals(i.getId(), gs.getPlatformGsId()))
                .findFirst().orElse(resp.getData().get(0));
        gs.setLeftMoney(toBigDecimal(info.getLeft_money()));
        gs.setSyncDate(new Date());
    }
    private Integer parseAddGsId(Object data) {
        if (data == null) {
            return null;
        }
        if (data instanceof Number) {
            return ((Number) data).intValue();
        }
        JSONObject obj = (JSONObject) JSON.toJSON(data);
        if (obj.containsKey("id")) {
            return obj.getInteger("id");
        }
        return null;
    }
    private BigDecimal toBigDecimal(Object val) {
        if (val == null) {
            return null;
        }
        if (val instanceof BigDecimal) {
            return (BigDecimal) val;
        }
        if (val instanceof Number) {
            return BigDecimal.valueOf(((Number) val).doubleValue());
        }
        try {
            return new BigDecimal(String.valueOf(val));
        } catch (NumberFormatException e) {
            return null;
        }
    }
    private String apiMsg(ConditionerBaseResponse<?> resp, String def) {
        return resp != null && StringUtils.isNotBlank(resp.getMessage()) ? resp.getMessage() : def;
    }
    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);
        }
        return c;
    }
    private YwCustomerGs requireGsConfig(Integer customerId) {
        YwCustomerGs gs = getCustomerGsConfig(customerId);
        if (gs == null || gs.getPlatformGsId() == null) {
            throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "请先配置并保存空调关联");
        }
        return gs;
    }
    private void assertElectricalBound(Integer customerId, Integer electricalId) {
        Long cnt = ywCustomerElectricalMapper.selectCount(new QueryWrapper<YwCustomerElectrical>().lambda()
                .eq(YwCustomerElectrical::getCustomerId, customerId)
                .eq(YwCustomerElectrical::getElectricalId, electricalId)
                .eq(YwCustomerElectrical::getIsdeleted, Constants.ZERO));
        if (cnt == null || cnt == 0) {
            throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "电表未关联该商户");
        }
    }
    private List<Integer> listBoundElectricalIds(Integer customerId) {
        return ywCustomerElectricalMapper.selectList(new QueryWrapper<YwCustomerElectrical>().lambda()
                        .eq(YwCustomerElectrical::getCustomerId, customerId)
                        .eq(YwCustomerElectrical::getIsdeleted, Constants.ZERO))
                .stream().map(YwCustomerElectrical::getElectricalId).collect(Collectors.toList());
    }
    private Set<Integer> listAllBoundElectricalIds() {
        return ywCustomerElectricalMapper.selectList(new QueryWrapper<YwCustomerElectrical>().lambda()
                        .eq(YwCustomerElectrical::getIsdeleted, Constants.ZERO))
                .stream().map(YwCustomerElectrical::getElectricalId).collect(Collectors.toSet());
    }
    private Set<Integer> listAllBoundElectricalIdsExcept(Integer customerId) {
        return ywCustomerElectricalMapper.selectList(new QueryWrapper<YwCustomerElectrical>().lambda()
                        .eq(YwCustomerElectrical::getIsdeleted, Constants.ZERO)
                        .ne(YwCustomerElectrical::getCustomerId, customerId))
                .stream().map(YwCustomerElectrical::getElectricalId).collect(Collectors.toSet());
    }
    private List<YwElectrical> loadCustomerElectricalList(Integer customerId) {
        List<Integer> ids = listBoundElectricalIds(customerId);
        if (ids.isEmpty()) {
            return Collections.emptyList();
        }
        List<YwElectrical> list = ywElectricalMapper.selectBatchIds(ids);
        list = list.stream().filter(e -> !Objects.equals(e.getIsdeleted(), Constants.ONE)).collect(Collectors.toList());
        ywElectricalBizService.enrichList(list);
        return list;
    }
    private List<YwConditioner> loadCustomerConditionerList(Integer customerId) {
        List<YwCustomerConditioner> rels = ywCustomerConditionerMapper.selectList(new QueryWrapper<YwCustomerConditioner>().lambda()
                .eq(YwCustomerConditioner::getCustomerId, customerId)
                .eq(YwCustomerConditioner::getIsdeleted, Constants.ZERO));
        if (rels.isEmpty()) {
            return Collections.emptyList();
        }
        Map<Integer, Integer> ratioMap = rels.stream()
                .collect(Collectors.toMap(YwCustomerConditioner::getConditionerId, YwCustomerConditioner::getDevRatio, (a, b) -> a));
        List<YwConditioner> list = ywConditionerMapper.selectBatchIds(ratioMap.keySet());
        list = list.stream().filter(c -> !Objects.equals(c.getIsdeleted(), Constants.ONE)).collect(Collectors.toList());
        for (YwConditioner c : list) {
            c.setDevRatio(ratioMap.get(c.getId()));
        }
        return list;
    }
    private PageData<YwElectrical> pageElectricalByIds(PageWrap<YwElectrical> pageWrap, List<Integer> ids) {
        IPage<YwElectrical> page = new Page<>(pageWrap.getPage(), pageWrap.getCapacity());
        YwElectrical model = pageWrap.getModel();
        QueryWrapper<YwElectrical> qw = new QueryWrapper<>();
        qw.lambda()
                .in(YwElectrical::getId, ids)
                .eq(YwElectrical::getIsdeleted, Constants.ZERO)
                .and(model != null && StringUtils.isNotBlank(model.getName()), w -> w
                        .like(YwElectrical::getName, model.getName())
                        .or().like(YwElectrical::getAddress, model.getName()))
                .orderByDesc(YwElectrical::getCreateDate);
        IPage<YwElectrical> result = ywElectricalMapper.selectPage(page, qw);
        ywElectricalBizService.enrichList(result.getRecords());
        return PageData.from(result);
    }
    private <T> PageData<T> emptyPage(PageWrap<?> pageWrap) {
        PageData<T> data = new PageData<>();
        data.setRecords(Collections.emptyList());
        data.setTotal(0);
        data.setPage(pageWrap.getPage());
        data.setCapacity(pageWrap.getCapacity());
        return data;
    }
}
server/visits/dmvisit_service/src/main/java/com/doumee/service/business/impl/YwElectricalActionsServiceImpl.java
@@ -2,6 +2,8 @@
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.PageData;
import com.doumee.core.model.PageWrap;
import com.doumee.core.utils.Constants;
@@ -10,9 +12,13 @@
import com.doumee.dao.business.model.YwElectrical;
import com.doumee.dao.business.model.YwElectricalActions;
import com.doumee.service.business.YwElectricalActionsService;
import com.doumee.service.business.YwElectricalBizService;
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 java.util.Objects;
/**
 * ç”µè¡¨è¿œç¨‹æ“ä½œè®°å½• Service å®žçް
@@ -22,6 +28,8 @@
    @Autowired
    private YwElectricalActionsMapper ywElectricalActionsMapper;
    @Autowired
    private YwElectricalBizService ywElectricalBizService;
    @Override
    public PageData<YwElectricalActions> findPage(PageWrap<YwElectricalActions> pageWrap) {
@@ -50,4 +58,22 @@
        IPage<YwElectricalActions> result = ywElectricalActionsMapper.selectJoinPage(page, YwElectricalActions.class, queryWrapper);
        return PageData.from(result);
    }
    @Override
    public String queryAsyncResult(Integer id) {
        if (id == null) {
            throw new BusinessException(ResponseStatus.BAD_REQUEST);
        }
        YwElectricalActions act = ywElectricalActionsMapper.selectById(id);
        if (act == null || Objects.equals(act.getIsdeleted(), Constants.ONE)) {
            throw new BusinessException(ResponseStatus.DATA_EMPTY.getCode(), "操作记录不存在");
        }
        if (!Objects.equals(act.getStatus(), Constants.ZERO)) {
            throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "仅处理中记录可查询");
        }
        if (StringUtils.isBlank(act.getOprId())) {
            throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "缺少任务 ID");
        }
        return ywElectricalBizService.syncAsyncActionStatus(act.getOprId().trim());
    }
}
server/visits/dmvisit_service/src/main/java/com/doumee/service/business/impl/YwElectricalBizServiceImpl.java
@@ -12,6 +12,7 @@
import com.doumee.core.device.model.request.*;
import com.doumee.core.device.model.response.ElectronicBaseResponse;
import com.doumee.core.device.model.response.ElectronicDataResponse;
import com.doumee.core.device.model.response.MeterDealResponse;
import com.doumee.core.device.model.response.QueryDataInfoResponse;
import com.doumee.core.device.model.response.QueryDataV2Response;
import com.doumee.core.exception.BusinessException;
@@ -51,6 +52,11 @@
    public static final int ACTION_OPEN = 6;
    public static final int ACTION_RECHARGE = 7;
    public static final int ACTION_READ = 8;
    private static final long FIRST_STATUS_QUERY_DELAY_MS = 30_000L;
    private static final long STATUS_QUERY_MIN_INTERVAL_MS = 3_600_000L;
    private static final int STATUS_QUERY_BATCH_SIZE = 50;
    private static final int STATUS_QUERY_MAX_PENDING = 100;
    /** ele_read æŠ„电表数据操作类型(与 queryData functionids=253 ä¸åŒï¼‰ */
    private static final int ELE_READ_TYPE_METER = 3;
@@ -170,6 +176,7 @@
    }
    private String doSecurityReset(YwElectrical e, String paymentMode, int actionType, LoginUserInfo user) {
        assertNoPendingAsyncAction(e.getId(), actionType);
        String oprId = newOprId();
        SecurityResetRequest req = new SecurityResetRequest();
        req.setOpr_id(oprId);
@@ -188,6 +195,7 @@
    }
    private String doEleControl(YwElectrical e, int type, int actionType, LoginUserInfo user) {
        assertNoPendingAsyncAction(e.getId(), actionType);
        String oprId = newOprId();
        EleControlApiRequest req = new EleControlApiRequest();
        req.setOpr_id(oprId);
@@ -205,6 +213,7 @@
    }
    private String doOpenAccount(YwElectrical e, YwElectricalOperateDTO dto, LoginUserInfo user) {
        assertNoPendingAsyncAction(e.getId(), ACTION_OPEN);
        String oprId = newOprId();
        OpenAccountRequest req = buildOpenAccountRequest(e, oprId, dto.getMoney(), dto.getRemark());
        String reqJson = JSON.toJSONString(Collections.singletonList(req));
@@ -216,6 +225,7 @@
        if (!Objects.equals(e.getAccountStatus(), Constants.ONE)) {
            throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "电表未开户,请先开户");
        }
        assertNoPendingAsyncAction(e.getId(), ACTION_RECHARGE);
        String oprId = newOprId();
        OpenAccountRequest req = buildOpenAccountRequest(e, oprId, dto.getMoney(), dto.getRemark());
        String reqJson = JSON.toJSONString(Collections.singletonList(req));
@@ -228,6 +238,7 @@
    }
    private String doReadMeter(YwElectrical e, LoginUserInfo user) {
        assertNoPendingAsyncAction(e.getId(), ACTION_READ);
        String oprId = newOprId();
        EleReadRequest req = new EleReadRequest();
        req.setOpr_id(oprId);
@@ -244,7 +255,7 @@
            throw new BusinessException(ResponseStatus.SERVER_ERROR.getCode(), msg);
        }
        boolean synced = syncMeterDataForElectrical(e);
        refreshBalanceFromData(e);
        refreshBalanceFromData(e, null, true);
        return synced ? "抄表成功,用量余额已更新" : "抄表请求已提交,暂无新抄表数据,请稍后刷新查看";
    }
@@ -288,6 +299,21 @@
        }
        ywElectricalActionsMapper.insert(act);
        return isSuccess(resp) ? "提交成功,请稍后在操作记录或充值记录中查看结果" : act.getResultMsg();
    }
    /** åŒä¸€ç”µè¡¨ã€ç›¸åŒæ“ä½œç±»åž‹å­˜åœ¨å¤„理中记录时,禁止重复提交异步任务 */
    private void assertNoPendingAsyncAction(Integer electricalId, int actionType) {
        if (electricalId == null) {
            return;
        }
        Long pending = ywElectricalActionsMapper.selectCount(new QueryWrapper<YwElectricalActions>().lambda()
                .eq(YwElectricalActions::getElectricalId, electricalId)
                .eq(YwElectricalActions::getActionType, actionType)
                .eq(YwElectricalActions::getStatus, Constants.ZERO)
                .eq(YwElectricalActions::getIsdeleted, Constants.ZERO));
        if (pending != null && pending > 0) {
            throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "需要等待上一次操作结果执行结束");
        }
    }
    private void saveLog(YwElectrical e, String apiName, String reqJson, ElectronicBaseResponse resp, LoginUserInfo user) {
@@ -336,26 +362,32 @@
    }
    private void applyNotifyItem(JSONObject item) {
        if (item == null) return;
        String oprId = item.getString("opr_id");
        if (StringUtils.isBlank(oprId)) return;
        YwElectricalActions act = ywElectricalActionsMapper.selectOne(new QueryWrapper<YwElectricalActions>().lambda()
                .eq(YwElectricalActions::getOprId, oprId)
                .eq(YwElectricalActions::getIsdeleted, Constants.ZERO)
                .orderByDesc(YwElectricalActions::getCreateDate)
                .last("limit 1"));
        if (act == null) {
            log.info("electricalNotify no action for opr_id={}", oprId);
        if (item == null) {
            return;
        }
        ElectronicNotifyStatus ns = ElectronicNotifyStatus.fromCode(item.getString("status"));
        String oprId = item.getString("opr_id");
        String status = item.getString("status");
        String errMsg = item.get("error_msg") != null ? String.valueOf(item.get("error_msg")) : null;
        applyActionStatusItem(oprId, status, errMsg, item.toJSONString(), true);
    }
    private void applyActionStatusItem(String oprId, String platformStatus, String errMsg, String responseBody,
                                       boolean fromNotify) {
        if (StringUtils.isBlank(oprId)) {
            return;
        }
        YwElectricalActions act = findActionByOprId(oprId);
        if (act == null) {
            log.info("electrical action not found for opr_id={}", oprId);
            return;
        }
        ElectronicNotifyStatus ns = ElectronicNotifyStatus.fromCode(platformStatus);
        String resultMsg = ns.getLabel() + (StringUtils.isNotBlank(errMsg) ? ":" + errMsg : "");
        YwElectricalActions upd = new YwElectricalActions();
        upd.setId(act.getId());
        upd.setEditDate(new Date());
        upd.setResponseBody(item.toJSONString());
        upd.setResponseBody(responseBody);
        upd.setResultMsg(resultMsg);
        if (ns.isTerminalSuccess()) {
            upd.setStatus(Constants.ONE);
@@ -367,8 +399,12 @@
        ywElectricalActionsMapper.updateById(upd);
        YwElectrical e = ywElectricalMapper.selectById(act.getElectricalId());
        if (e == null) return;
        saveNotifyLog(e, item, ns);
        if (e == null) {
            return;
        }
        if (fromNotify) {
            saveNotifyLog(e, JSON.parseObject(responseBody), ns);
        }
        if (ns.isTerminalSuccess()) {
            applyNotifySideEffect(e, act.getActionType());
            if (Objects.equals(act.getActionType(), ACTION_RECHARGE)) {
@@ -377,6 +413,183 @@
        } else if (ns.isTerminalFail() && Objects.equals(act.getActionType(), ACTION_RECHARGE)) {
            updateChargeByNotify(oprId, Constants.TWO, resultMsg);
        }
    }
    private YwElectricalActions findActionByOprId(String oprId) {
        return ywElectricalActionsMapper.selectOne(new QueryWrapper<YwElectricalActions>().lambda()
                .eq(YwElectricalActions::getOprId, oprId)
                .eq(YwElectricalActions::getIsdeleted, Constants.ZERO)
                .orderByDesc(YwElectricalActions::getCreateDate)
                .last("limit 1"));
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public String syncAsyncActionStatus(String oprId) {
        if (StringUtils.isBlank(oprId)) {
            throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "缺少任务 ID");
        }
        String trimmed = oprId.trim();
        if (findActionByOprId(trimmed) == null) {
            throw new BusinessException(ResponseStatus.DATA_EMPTY.getCode(), "未找到对应操作记录");
        }
        ElectronicBaseResponse resp = ElectronicToolUtil.requestStatus(trimmed);
        return handleStatusQueryResponse(resp, trimmed);
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public String syncChargeStatusById(Integer chargeId) {
        if (chargeId == null) {
            throw new BusinessException(ResponseStatus.BAD_REQUEST);
        }
        YwElectricalCharge charge = ywElectricalChargeMapper.selectById(chargeId);
        if (charge == null || !Objects.equals(charge.getIsdeleted(), Constants.ZERO)) {
            throw new BusinessException(ResponseStatus.DATA_EMPTY.getCode(), "充值记录不存在");
        }
        if (!Objects.equals(charge.getStatus(), Constants.ZERO)) {
            throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "仅充值中记录可同步");
        }
        if (!Objects.equals(charge.getType(), Constants.ZERO)) {
            throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "仅电表充值记录可同步");
        }
        return syncAsyncActionStatus(charge.getOprId());
    }
    @Override
    public void syncPendingAsyncActionsScheduled() {
        try {
            Date minCreateTime = new Date(System.currentTimeMillis() - FIRST_STATUS_QUERY_DELAY_MS);
            List<YwElectricalActions> pending = ywElectricalActionsMapper.selectList(
                    new QueryWrapper<YwElectricalActions>().lambda()
                            .eq(YwElectricalActions::getStatus, Constants.ZERO)
                            .eq(YwElectricalActions::getIsdeleted, Constants.ZERO)
                            .isNotNull(YwElectricalActions::getOprId)
                            .le(YwElectricalActions::getCreateDate, minCreateTime)
                            .orderByAsc(YwElectricalActions::getCreateDate)
                            .last("limit " + STATUS_QUERY_MAX_PENDING));
            if (CollectionUtils.isEmpty(pending)) {
                return;
            }
            long nowMs = System.currentTimeMillis();
            List<String> oprIds = pending.stream()
                    .filter(act -> shouldPollAction(act, nowMs))
                    .map(YwElectricalActions::getOprId)
                    .filter(StringUtils::isNotBlank)
                    .distinct()
                    .collect(Collectors.toList());
            if (oprIds.isEmpty()) {
                return;
            }
            int updated = 0;
            for (int i = 0; i < oprIds.size(); i += STATUS_QUERY_BATCH_SIZE) {
                List<String> batch = oprIds.subList(i, Math.min(i + STATUS_QUERY_BATCH_SIZE, oprIds.size()));
                ElectronicBaseResponse resp = ElectronicToolUtil.requestStatusByOprIds(batch);
                updated += applyStatusQueryBatch(resp);
            }
            log.info("syncPendingAsyncActionsScheduled queried={}, updated={}", oprIds.size(), updated);
        } catch (Exception e) {
            log.warn("syncPendingAsyncActionsScheduled failed", e);
        }
    }
    private int applyStatusQueryBatch(ElectronicBaseResponse resp) {
        if (resp == null || !"SUCCESS".equalsIgnoreCase(resp.getStatus())) {
            return 0;
        }
        List<MeterDealResponse> items = extractMeterDealList(resp);
        if (CollectionUtils.isEmpty(items)) {
            return 0;
        }
        int updated = 0;
        for (MeterDealResponse item : items) {
            if (item == null || StringUtils.isBlank(item.getOpr_id())) {
                continue;
            }
            YwElectricalActions before = findActionByOprId(item.getOpr_id());
            if (before == null || !Objects.equals(before.getStatus(), Constants.ZERO)) {
                continue;
            }
            applyActionStatusItem(item.getOpr_id(), item.getStatus(), item.getError_msg(),
                    JSON.toJSONString(item), false);
            YwElectricalActions after = findActionByOprId(item.getOpr_id());
            if (after != null && !Objects.equals(before.getStatus(), after.getStatus())) {
                updated++;
            }
        }
        return updated;
    }
    private String handleStatusQueryResponse(ElectronicBaseResponse resp, String oprId) {
        if (resp == null) {
            return "平台无响应,请稍后重试";
        }
        if (!"SUCCESS".equalsIgnoreCase(resp.getStatus())) {
            return "查询失败:" + StringUtils.defaultIfBlank(resp.getError_msg(), resp.getStatus());
        }
        List<MeterDealResponse> items = extractMeterDealList(resp);
        MeterDealResponse item = items.stream()
                .filter(i -> i != null && oprId.equals(i.getOpr_id()))
                .findFirst()
                .orElse(CollectionUtils.isEmpty(items) ? null : items.get(0));
        if (item == null) {
            return "未查询到任务结果,请稍后重试";
        }
        applyActionStatusItem(oprId, item.getStatus(), item.getError_msg(), JSON.toJSONString(item), false);
        YwElectricalActions act = findActionByOprId(oprId);
        if (act == null) {
            return "未找到对应操作记录";
        }
        if (Objects.equals(act.getStatus(), Constants.ONE)) {
            return Objects.equals(act.getActionType(), ACTION_RECHARGE) ? "已同步为充值成功" : "操作已成功";
        }
        if (Objects.equals(act.getStatus(), Constants.TWO)) {
            return Objects.equals(act.getActionType(), ACTION_RECHARGE)
                    ? "已同步为充值失败:" + StringUtils.defaultString(act.getResultMsg())
                    : "操作已失败:" + StringUtils.defaultString(act.getResultMsg());
        }
        if (ElectronicToolUtil.isAsyncStatusFinal(item.getStatus())) {
            return "平台状态:" + item.getStatus();
        }
        return "任务处理中,请稍后重试";
    }
    @SuppressWarnings("unchecked")
    private List<MeterDealResponse> extractMeterDealList(ElectronicBaseResponse resp) {
        Object content = resp.getResponse_content();
        if (content == null) {
            return Collections.emptyList();
        }
        if (content instanceof List) {
            List<?> list = (List<?>) content;
            List<MeterDealResponse> result = new ArrayList<>();
            for (Object o : list) {
                if (o instanceof MeterDealResponse) {
                    result.add((MeterDealResponse) o);
                } else {
                    result.add(JSON.parseObject(JSON.toJSONString(o), MeterDealResponse.class));
                }
            }
            return result;
        }
        return JSON.parseArray(JSON.toJSONString(content), MeterDealResponse.class);
    }
    private boolean shouldPollAction(YwElectricalActions act, long nowMs) {
        Date create = act.getCreateDate();
        if (create == null || StringUtils.isBlank(act.getOprId())) {
            return false;
        }
        long createMs = create.getTime();
        if (nowMs - createMs < FIRST_STATUS_QUERY_DELAY_MS) {
            return false;
        }
        long editMs = act.getEditDate() != null ? act.getEditDate().getTime() : createMs;
        if (editMs - createMs < 5_000L) {
            return true;
        }
        return nowMs - editMs >= STATUS_QUERY_MIN_INTERVAL_MS;
    }
    private void saveChargeRecord(YwElectrical e, String oprId, YwElectricalOperateDTO dto, LoginUserInfo user) {
@@ -400,6 +613,12 @@
        charge.setStatusInfo("充值中");
        charge.setBanlance(e.getBalance());
        charge.setRoomNames(e.getRoomNames());
        if (dto.getCustomerId() != null) {
            charge.setCustomerId(dto.getCustomerId());
        }
        if (StringUtils.isNotBlank(e.getAddress()) || StringUtils.isNotBlank(e.getName())) {
            charge.setDeviceInfo(StringUtils.defaultString(e.getAddress()) + " " + StringUtils.defaultString(e.getName()));
        }
        if (StringUtils.isNotBlank(e.getParamId())) {
            try {
                charge.setParamId(Integer.parseInt(e.getParamId()));
@@ -410,13 +629,29 @@
    }
    private void updateChargeByNotify(String oprId, int status, String statusInfo) {
        ywElectricalChargeMapper.update(null, new UpdateWrapper<YwElectricalCharge>().lambda()
        YwElectricalCharge charge = ywElectricalChargeMapper.selectOne(new QueryWrapper<YwElectricalCharge>().lambda()
                .eq(YwElectricalCharge::getOprId, oprId)
                .eq(YwElectricalCharge::getIsdeleted, Constants.ZERO)
                .last("limit 1"));
        BigDecimal balanceAfter = null;
        if (charge != null && status == Constants.ONE) {
            balanceAfter = calcBalanceAfterRecharge(charge);
            if (charge.getObjId() != null) {
                updateElectricalBalance(charge.getObjId(), balanceAfter);
            }
        }
        UpdateWrapper<YwElectricalCharge> uw = new UpdateWrapper<>();
        uw.lambda()
                .set(YwElectricalCharge::getStatus, status)
                .set(YwElectricalCharge::getStatusTime, new Date())
                .set(YwElectricalCharge::getStatusInfo, statusInfo)
                .set(YwElectricalCharge::getEditDate, new Date())
                .eq(YwElectricalCharge::getOprId, oprId)
                .eq(YwElectricalCharge::getIsdeleted, Constants.ZERO));
                .eq(YwElectricalCharge::getIsdeleted, Constants.ZERO);
        if (balanceAfter != null) {
            uw.lambda().set(YwElectricalCharge::getBalanceAfter, balanceAfter);
        }
        ywElectricalChargeMapper.update(null, uw);
    }
    private void saveNotifyLog(YwElectrical e, JSONObject item, ElectronicNotifyStatus ns) {
@@ -454,34 +689,128 @@
                ywElectricalMapper.update(null, uw);
                break;
            case ACTION_RECHARGE:
                // å……值成功后余额由 updateChargeByNotify æŒ‰ã€Œå……值前+充值金额」累计,待定时抄表后再切回抄表余额
                break;
            case ACTION_READ:
                syncMeterDataForElectrical(e);
                refreshBalanceFromData(e);
                refreshBalanceFromData(e, null, true);
                break;
            default:
                break;
        }
    }
    /** å……值后累计余额 = å……值前余额 + å……值金额 */
    private BigDecimal calcBalanceAfterRecharge(YwElectricalCharge charge) {
        BigDecimal before = charge.getBanlance() != null ? charge.getBanlance() : BigDecimal.ZERO;
        BigDecimal money = charge.getMoney() != null ? charge.getMoney() : BigDecimal.ZERO;
        return before.add(money);
    }
    private void updateElectricalBalance(Integer electricalId, BigDecimal balance) {
        if (electricalId == null || balance == null) {
            return;
        }
        UpdateWrapper<YwElectrical> uw = new UpdateWrapper<>();
        uw.lambda()
                .eq(YwElectrical::getId, electricalId)
                .set(YwElectrical::getBalance, balance)
                .set(YwElectrical::getBalanceTime, new Date())
                .set(YwElectrical::getEditDate, new Date());
        ywElectricalMapper.update(null, uw);
    }
    private YwElectricalCharge findLastSuccessfulRecharge(Integer electricalId) {
        if (electricalId == null) {
            return null;
        }
        return ywElectricalChargeMapper.selectOne(new QueryWrapper<YwElectricalCharge>().lambda()
                .eq(YwElectricalCharge::getObjId, electricalId)
                .eq(YwElectricalCharge::getType, Constants.ZERO)
                .eq(YwElectricalCharge::getStatus, Constants.ONE)
                .eq(YwElectricalCharge::getIsdeleted, Constants.ZERO)
                .orderByDesc(YwElectricalCharge::getStatusTime)
                .orderByDesc(YwElectricalCharge::getId)
                .last("limit 1"));
    }
    private Date parseDataTime(YwElectricalData data) {
        if (data == null) {
            return null;
        }
        if (StringUtils.isNotBlank(data.getAddTime())) {
            try {
                return new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss").parse(data.getAddTime().trim());
            } catch (Exception ignored) {
            }
        }
        return data.getCreateDate();
    }
    /**
     * æœ€è¿‘一次充值成功后,若抄表时间仍早于充值成功时间,则暂不以抄表余额覆盖累计余额。
     */
    private boolean shouldUseMeterBalance(Integer electricalId, YwElectricalData data) {
        YwElectricalCharge lastRecharge = findLastSuccessfulRecharge(electricalId);
        if (lastRecharge == null || lastRecharge.getStatusTime() == null) {
            return true;
        }
        Date dataTime = parseDataTime(data);
        if (dataTime == null) {
            return true;
        }
        return !dataTime.before(lastRecharge.getStatusTime());
    }
    private void refreshBalanceFromData(YwElectrical e) {
        YwElectricalData data = findLatestData(e.getId(), e.getAddress());
        if (data == null) return;
        refreshBalanceFromData(e, null, false);
    }
    private void refreshBalanceFromData(YwElectrical e, YwElectricalData data, boolean forceFromMeter) {
        if (e == null) {
            return;
        }
        if (data == null) {
            data = findLatestData(e.getId(), e.getAddress());
        }
        if (data == null) {
            return;
        }
        UpdateWrapper<YwElectrical> uw = new UpdateWrapper<>();
        uw.lambda().eq(YwElectrical::getId, e.getId());
        if (StringUtils.isNotBlank(data.getZhygzdl())) {
            uw.lambda().set(YwElectrical::getBalanceBattery, data.getZhygzdl());
        }
        if (StringUtils.isNotBlank(data.getYe())) {
        boolean useMeterBalance = forceFromMeter || shouldUseMeterBalance(e.getId(), data);
        if (useMeterBalance && StringUtils.isNotBlank(data.getYe())) {
            try {
                uw.lambda().set(YwElectrical::getBalance, new BigDecimal(data.getYe()));
            } catch (Exception ignored) {
            }
        }
        uw.lambda().set(YwElectrical::getBalanceTime, new Date());
        if (isPurchaseCountPositive(data.getCountnum())) {
            uw.lambda().set(YwElectrical::getAccountStatus, Constants.ONE);
        }
        if (useMeterBalance) {
            Date dataTime = parseDataTime(data);
            uw.lambda().set(YwElectrical::getBalanceTime, dataTime != null ? dataTime : new Date());
        }
        uw.lambda().set(YwElectrical::getEditDate, new Date());
        ywElectricalMapper.update(null, uw);
    }
    /** è´­ä¹°æ¬¡æ•°å¤§äºŽ 0 è§†ä¸ºå·²å¼€æˆ· */
    private static boolean isPurchaseCountPositive(String purchaseCount) {
        if (StringUtils.isBlank(purchaseCount)) {
            return false;
        }
        try {
            return new BigDecimal(purchaseCount.trim()).compareTo(BigDecimal.ZERO) > 0;
        } catch (NumberFormatException e) {
            return false;
        }
    }
    private YwElectricalData findLatestData(Integer electricalId, String address) {
        QueryWrapper<YwElectricalData> q = new QueryWrapper<>();
        q.lambda().eq(YwElectricalData::getIsdeleted, Constants.ZERO)
server/visits/dmvisit_service/src/main/java/com/doumee/service/business/impl/YwElectricalServiceImpl.java
@@ -37,6 +37,7 @@
import com.github.yulichang.wrapper.MPJLambdaWrapper;
import com.doumee.core.model.LoginUserInfo;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
@@ -360,7 +361,6 @@
        model.setName(door.getDescription());
        model.setCId(door.getCid());
        model.setDId(door.getId());
        model.setAccountStatus(Constants.TWO);
        model.setPriceid(door.getPriceid());
        model.setParamId(door.getParam_id());
        model.setDeviceType(door.getDevice_type());
@@ -370,9 +370,23 @@
        model.setType(door.getType());
        model.setRelayStatus(door.getRelay_state());
        model.setCsq(door.getCsq());
        model.setAccountStatus(resolveAccountStatusByPurchaseCount(door.getCount()));
        model.setRemark(JSONObject.toJSONString(device));
        return  model;
    }
    /** è´­ä¹°æ¬¡æ•°å¤§äºŽ 0 è§†ä¸ºå·²å¼€æˆ· */
    private Integer resolveAccountStatusByPurchaseCount(String purchaseCount) {
        if (StringUtils.isBlank(purchaseCount)) {
            return Constants.ZERO;
        }
        try {
            return new BigDecimal(purchaseCount.trim()).compareTo(BigDecimal.ZERO) > 0
                    ? Constants.ONE : Constants.ZERO;
        } catch (NumberFormatException e) {
            return Constants.ZERO;
        }
    }
    private MeterInfoResponse getDeviceByid(String indexCode, List<MeterInfoResponse> allHkList ) {
        if(allHkList!=null && allHkList.size()>0){
            for(MeterInfoResponse info : allHkList){