skyline преди 2 месеца
родител
ревизия
d04b99d8dd
променени са 31 файла, в които са добавени 1970 реда и са изтрити 2 реда
  1. 48 0
      admin-web/src/router/route.ts
  2. 14 0
      admin-web/src/utils/request.ts
  3. 216 0
      admin-web/src/views/admin/whitelist/batch.vue
  4. 194 0
      admin-web/src/views/admin/whitelist/config.vue
  5. 170 0
      admin-web/src/views/admin/whitelist/dialog.vue
  6. 286 0
      admin-web/src/views/admin/whitelist/index.vue
  7. 31 0
      admin/src/main/java/com/kym/admin/controller/AccountController.java
  8. 108 0
      admin/src/main/java/com/kym/admin/controller/StationWhitelistController.java
  9. 20 0
      admin/src/main/resources/static/index.html
  10. BIN
      admin/src/main/resources/static/logo.png
  11. 4 0
      common/src/main/java/com/kym/common/constant/ResponseEnum.java
  12. 39 0
      entity/src/main/java/com/kym/entity/admin/StationWhitelist.java
  13. 31 0
      entity/src/main/java/com/kym/entity/admin/StationWhitelistConfig.java
  14. 44 0
      entity/src/main/java/com/kym/entity/admin/StationWhitelistLog.java
  15. 18 0
      entity/src/main/java/com/kym/entity/admin/dto/BatchAddWhitelistDto.java
  16. 17 0
      entity/src/main/java/com/kym/entity/admin/queryParams/StationWhitelistQueryParam.java
  17. 24 0
      entity/src/main/java/com/kym/entity/admin/vo/StationWhitelistConfigVo.java
  18. 32 0
      entity/src/main/java/com/kym/entity/admin/vo/StationWhitelistVo.java
  19. 10 0
      entity/src/main/java/com/kym/entity/common/RedisKeys.java
  20. 8 0
      mapper/src/main/java/com/kym/mapper/admin/StationWhitelistConfigMapper.java
  21. 8 0
      mapper/src/main/java/com/kym/mapper/admin/StationWhitelistLogMapper.java
  22. 8 0
      mapper/src/main/java/com/kym/mapper/admin/StationWhitelistMapper.java
  23. 20 0
      service/src/main/java/com/kym/service/admin/StationWhitelistConfigService.java
  24. 19 0
      service/src/main/java/com/kym/service/admin/StationWhitelistLogService.java
  25. 33 0
      service/src/main/java/com/kym/service/admin/StationWhitelistService.java
  26. 128 0
      service/src/main/java/com/kym/service/admin/impl/StationWhitelistConfigServiceImpl.java
  27. 100 0
      service/src/main/java/com/kym/service/admin/impl/StationWhitelistLogServiceImpl.java
  28. 307 0
      service/src/main/java/com/kym/service/admin/impl/StationWhitelistServiceImpl.java
  29. 2 0
      service/src/main/java/com/kym/service/miniapp/UserService.java
  30. 20 2
      service/src/main/java/com/kym/service/miniapp/impl/ChargeServiceImpl.java
  31. 11 0
      service/src/main/java/com/kym/service/miniapp/impl/UserServiceImpl.java

+ 48 - 0
admin-web/src/router/route.ts

@@ -379,6 +379,54 @@ export const adminRoutes: Array<RouteRecordRaw> = [
                     perm:"invoice.list",
                 },
             },
+            {
+                path: '/whitelist',
+                name: 'adminWhitelist',
+                component: () => import('/@/layout/routerView/parent.vue'),
+                redirect: '/whitelist/config',
+                meta: {
+                    title: '白名单管理',
+                    isLink: '',
+                    isHide: false,
+                    isKeepAlive: true,
+                    isAffix: false,
+                    isIframe: false,
+                    icon: 'ele-Stamp',
+                    perm:"station.whitelist.list,station.whitelist.config.list",
+                },
+                children: [
+                    {
+                        path: '/whitelist/config',
+                        name: 'whitelistConfig',
+                        component: () => import('/@/views/admin/whitelist/config.vue'),
+                        meta: {
+                            title: '白名单配置',
+                            isLink: '',
+                            isHide: false,
+                            isKeepAlive: true,
+                            isAffix: false,
+                            isIframe: false,
+                            perm:"station.whitelist.config.list",
+                            icon: 'ele-Setting',
+                        },
+                    },
+                    {
+                        path: '/whitelist/list',
+                        name: 'whitelistList',
+                        component: () => import('/@/views/admin/whitelist/index.vue'),
+                        meta: {
+                            title: '白名单用户',
+                            isLink: '',
+                            isHide: false,
+                            isKeepAlive: true,
+                            isAffix: false,
+                            isIframe: false,
+                            perm:"station.whitelist.list",
+                            icon: 'ele-UserFilled',
+                        },
+                    },
+                ]
+            },
             {
                 path: '/org',
                 name: 'adminOrg',

+ 14 - 0
admin-web/src/utils/request.ts

@@ -167,6 +167,20 @@ export function $body(url: string, data = {}, showLoading = false) {
     })
 }
 
+export function $delete(url: string, data = {}, showLoading = false) {
+    //if(showLoading)Spin.show();
+    return new Promise((resolve, reject) => {
+        service.delete(url, {data: data})
+            .then(response => {
+                ////if(showLoading)Spin.hide();
+                resolve(response.data);
+            }, err => {
+                ////if(showLoading)Spin.hide();
+                reject(err)
+            })
+    })
+}
+
 export function $upload(url: string, formData = {}, showLoading = false) {
     //if(showLoading)Spin.show();
     return new Promise((resolve, reject) => {

+ 216 - 0
admin-web/src/views/admin/whitelist/batch.vue

@@ -0,0 +1,216 @@
+<style scoped lang="scss">
+
+</style>
+<template>
+  <div class="system-dialog-container">
+    <el-drawer
+        title="批量添加白名单用户"
+        v-model="state.dialog.isShowDialog"
+        size="600px"
+        append-to-body
+        destroy-on-close
+        class="pd10"
+        :close-on-click-modal="false"
+        @close="onClose">
+      <el-form
+          :model="state.ruleForm"
+          :rules="state.rules"
+          label-position="top"
+          ref="formRef"
+          size="default"
+          label-width="100px"
+          class="mt5 pd10">
+        
+        <el-form-item label="选择站点" prop="stationId">
+          <ext-select
+              v-model="state.ruleForm.stationId"
+              placeholder="请选择站点"
+              url="station/listStation"
+              urlMethod="get"
+              data-key=""
+              label-key="stationName"
+              value-key="stationId"
+              clearable
+              class="w100"
+              @on-change="handleStationChange">
+          </ext-select>
+        </el-form-item>
+
+        <el-form-item label="搜索用户" prop="searchPhone">
+          <div class="search-container w100">
+            <el-input
+                v-model="state.searchPhone"
+                placeholder="请输入手机号搜索用户"
+                clearable
+                class="search-input">
+              <template #append>
+                <el-button @click="handleSearchUser" :loading="state.searchLoading">
+                  <SvgIcon name="ele-Search"/>
+                  搜索
+                </el-button>
+              </template>
+            </el-input>
+          </div>
+        </el-form-item>
+
+        <el-form-item label="搜索结果">
+          <div class="search-result w100">
+            <el-table
+                :data="state.searchResult"
+                border
+                stripe
+                max-height="200"
+                v-loading="state.searchLoading"
+                @selection-change="handleSelectionChange">
+              <el-table-column type="selection" width="55" align="center"/>
+              <el-table-column prop="mobilePhone" label="手机号" width="130"/>
+              <el-table-column prop="nickname" label="昵称"/>
+            </el-table>
+            <div class="selected-info mt10" v-if="state.selectedUsers.length > 0">
+              <el-tag type="success">已选择 {{ state.selectedUsers.length }} 个用户</el-tag>
+            </div>
+          </div>
+        </el-form-item>
+
+        <el-form-item label="备注" prop="remark">
+          <el-input
+              v-model="state.ruleForm.remark"
+              placeholder="请输入备注(可选)"
+              type="textarea"
+              :rows="3"
+              class="w100">
+          </el-input>
+        </el-form-item>
+      </el-form>
+
+      <template #footer>
+        <div class="dialog-footer">
+          <el-button @click="onCancel" size="default">取 消</el-button>
+          <el-button :loading="state.btnLoading" type="primary" @click="onSubmit" size="default" :disabled="state.selectedUsers.length === 0">
+            批量添加
+          </el-button>
+        </div>
+      </template>
+    </el-drawer>
+  </div>
+</template>
+
+<script setup lang="ts" name="BatchAddWhitelistDialog">
+import { reactive, ref } from 'vue';
+import { Msg } from "/@/utils/message";
+import { $body, $get } from "/@/utils/request";
+import type { FormInstance, FormRules } from 'element-plus';
+import ExtSelect from "/@/components/form/ExtSelect.vue";
+
+const emit = defineEmits(['refresh']);
+const formRef = ref();
+
+const initState = () => ({
+  ruleForm: {
+    stationId: '',
+    remark: ''
+  },
+  searchPhone: '',
+  searchLoading: false,
+  searchResult: [] as Array<any>,
+  selectedUsers: [] as Array<any>,
+  btnLoading: false,
+  dialog: {
+    isShowDialog: false
+  },
+  rules: {
+    stationId: [{ required: true, message: '请选择站点', trigger: 'change' }]
+  }
+})
+
+const state = reactive(initState());
+
+const open = () => {
+  state.dialog.isShowDialog = true;
+};
+
+const onClose = () => {
+  state.dialog.isShowDialog = false;
+  Object.assign(state, initState());
+};
+
+const onCancel = () => {
+  onClose();
+};
+
+const handleStationChange = () => {
+  state.selectedUsers = [];
+  state.searchResult = [];
+};
+
+const handleSearchUser = () => {
+  if (!state.searchPhone || state.searchPhone.length < 4) {
+    Msg.message('请输入至少4位手机号', 'warning');
+    return;
+  }
+  state.searchLoading = true;
+  $get(`/account/list`, { mobilePhone: state.searchPhone, pageNum: 1, pageSize: 50 }).then((res: any) => {
+    let { list } = res;
+    state.searchResult = list || [];
+    state.searchLoading = false;
+  }).catch(() => {
+    state.searchResult = [];
+    state.searchLoading = false;
+  });
+};
+
+const handleSelectionChange = (selection: any) => {
+  state.selectedUsers = selection;
+};
+
+const onSubmit = () => {
+  formRef.value.validate((valid) => {
+    if (valid) {
+      if (state.selectedUsers.length === 0) {
+        Msg.message('请选择要添加的用户', 'warning');
+        return;
+      }
+      state.btnLoading = true;
+      const userIds = state.selectedUsers.map(u => u.id);
+      $body(`/station-whitelist/batchAdd`, {
+        stationId: state.ruleForm.stationId,
+        userIds: userIds,
+        remark: state.ruleForm.remark
+      }).then(() => {
+        Msg.message(`成功添加 ${userIds.length} 个用户`, 'success');
+        emit('refresh');
+        state.btnLoading = false;
+        onClose();
+      }).catch(() => {
+        state.btnLoading = false;
+      })
+    } else {
+      Msg.message('表单校验失败', 'error');
+    }
+  })
+};
+
+defineExpose({
+  open
+});
+</script>
+
+<style scoped lang="scss">
+.search-container {
+  display: flex;
+  
+  .search-input {
+    width: 100%;
+  }
+}
+
+.search-result {
+  border: 1px solid #dcdfe6;
+  border-radius: 4px;
+  padding: 10px;
+}
+
+.selected-info {
+  text-align: right;
+}
+</style>

+ 194 - 0
admin-web/src/views/admin/whitelist/config.vue

@@ -0,0 +1,194 @@
+<style scoped lang="scss">
+.system-container {
+
+  :deep(.el-card__body) {
+    display: flex;
+    flex-direction: column;
+    justify-content: space-between;
+    flex: 1;
+    overflow: auto;
+
+    .el-table {
+      flex: 1;
+    }
+
+  }
+}
+
+.page-content {
+  margin-bottom: 20px;
+}
+
+.page-pager {
+  background-color: #fff;
+  height: 24px;
+}
+
+.tip-card {
+  margin-bottom: 15px;
+  
+  :deep(.el-card__body) {
+    padding: 12px 20px;
+  }
+}
+</style>
+<template>
+  <div class="system-container layout-padding">
+    <el-card shadow="hover" class="layout-padding-auto">
+
+      <el-card class="tip-card" shadow="never">
+        <el-alert
+            title="白名单功能说明"
+            type="info"
+            :closable="false"
+            show-icon>
+          <template #default>
+            <p>1. 启用站点白名单后,只有白名单中的用户才能在该站点启动充电</p>
+            <p>2. 适用于内部专用站点或需要限制使用人数的场景</p>
+            <p>3. 白名单启用后,非白名单用户尝试充电将被拒绝</p>
+          </template>
+        </el-alert>
+      </el-card>
+
+      <el-form
+          :model="state.formQuery"
+          ref="queryRef"
+          size="default" label-width="0px" class="mt5 mb5">
+
+        <ext-select
+            v-model="state.formQuery.stationId"
+            placeholder="选择站点"
+            clearable
+            url="station/listStation"
+            urlMethod="get"
+            data-key=""
+            label-key="stationName"
+            value-key="stationId"
+            @on-change="loadData(true)"
+            class="wd200 mr10">
+        </ext-select>
+
+        <el-button class="ml10" plain size="default" type="success" @click="loadData(true)">
+          <SvgIcon name="ele-Search"/>
+          查询
+        </el-button>
+      </el-form>
+
+      <el-table
+          border
+          stripe
+          :height="state.tableData.height"
+          highlight-current-row
+          current-row-key="id"
+          row-key="id"
+          :data="state.tableData.data"
+          v-loading="state.tableData.loading">
+        <template #empty>
+          <el-empty></el-empty>
+        </template>
+        <el-table-column
+            v-for="field in state.tableData.columns"
+            :key="field.prop"
+            :label="field.label"
+            :column-key="field.prop"
+            :width="field.width"
+            :min-width="field.minWidth"
+            :fixed="field.fixed"
+            :sortable="field.sortable"
+            :show-overflow-tooltip="!field.fixed && field.width > 150">
+          <template #default="{ row }">
+            <template v-if="'enabled' === field.prop">
+              <el-switch
+                  v-model="row.enabled"
+                  :active-value="1"
+                  :inactive-value="0"
+                  active-text="启用"
+                  inactive-text="禁用"
+                  @change="handleStatusChange(row)"
+                  v-auth="'station.whitelist.config.modify'">
+              </el-switch>
+            </template>
+            <template v-else-if="'action' === field.prop">
+              <el-button v-auth="'station.whitelist.list'" size="small" plain type="primary" @click="handleManageWhitelist(row)">
+                管理白名单
+              </el-button>
+            </template>
+            <template v-else>
+              <div>{{ row[field.prop] }}</div>
+            </template>
+          </template>
+        </el-table-column>
+      </el-table>
+    </el-card>
+  </div>
+</template>
+
+<script setup lang="ts" name="StationWhitelistConfigList">
+import { reactive, onMounted, ref, nextTick } from 'vue';
+import { $body, $get, $put } from "/@/utils/request";
+import { Msg } from "/@/utils/message";
+import ExtSelect from "/@/components/form/ExtSelect.vue";
+import { useRouter } from 'vue-router';
+
+const router = useRouter();
+const queryRef = ref();
+
+const state = reactive({
+  formQuery: {
+    stationId: ''
+  },
+  tableData: {
+    height: 500,
+    data: [] as Array<any>,
+    loading: false,
+    columns: [
+      {label: '站点ID', prop: 'stationId', width: 160, resizable: true},
+      {label: '站点名称', prop: 'stationName', width: 200, resizable: true},
+      {label: '白名单状态', prop: 'enabled', width: 180, align: 'center'},
+      {label: '创建时间', prop: 'createTime', width: 180, resizable: true},
+      {label: '更新时间', prop: 'updateTime', width: 180, resizable: true},
+      {
+        label: '操作', prop: 'action', width: 150, align: 'center', fixed: 'right',
+      }
+    ],
+  }
+})
+
+onMounted(() => {
+  loadData();
+
+  nextTick(() => {
+    let bodyHeight = document.body.clientHeight;
+    let queryHeight = queryRef.value.$el.clientHeight;
+    state.tableData.height = bodyHeight - queryHeight - 380
+  })
+});
+
+const loadData = (refresh: boolean = false) => {
+  state.tableData.loading = true;
+  $get(`/station-whitelist/config/list`, state.formQuery).then((res: any) => {
+    state.tableData.data = res || [];
+    state.tableData.loading = false;
+  }).catch(e => {
+    console.error(e)
+    state.tableData.loading = false;
+  })
+};
+
+const handleStatusChange = (row: any) => {
+  const statusText = row.enabled === 1 ? '启用' : '禁用';
+  $put(`/station-whitelist/config/update?stationId=${row.stationId}&enabled=${row.enabled}`).then(() => {
+    Msg.message(`${statusText}成功`, 'success');
+  }).catch(() => {
+    Msg.message(`${statusText}失败`, 'error');
+    row.enabled = row.enabled === 1 ? 0 : 1;
+  });
+};
+
+const handleManageWhitelist = (row: any) => {
+  router.push({
+    path: '/whitelist/list',
+    query: { stationId: row.stationId }
+  });
+};
+</script>

+ 170 - 0
admin-web/src/views/admin/whitelist/dialog.vue

@@ -0,0 +1,170 @@
+<style scoped lang="scss">
+
+</style>
+<template>
+  <div class="system-dialog-container">
+    <el-drawer
+        :title="state.dialog.title"
+        v-model="state.dialog.isShowDialog"
+        size="500px"
+        append-to-body
+        destroy-on-close
+        class="pd10"
+        :close-on-click-modal="false"
+        @close="onClose">
+      <el-form
+          :model="state.ruleForm"
+          :rules="state.rules"
+          label-position="top"
+          ref="formRef"
+          size="default"
+          label-width="100px"
+          class="mt5 pd10">
+        
+        <el-form-item label="选择站点" prop="stationId">
+          <ext-select
+              v-model="state.ruleForm.stationId"
+              placeholder="请选择站点"
+              url="station/listStation"
+              urlMethod="get"
+              data-key=""
+              label-key="stationName"
+              value-key="stationId"
+              clearable
+              class="w100">
+          </ext-select>
+        </el-form-item>
+
+        <el-form-item label="选择用户" prop="userId">
+          <el-select-v2
+              v-model="state.ruleForm.userId"
+              :options="state.userList"
+              filterable
+              remote
+              :remote-method="handleUserSearch"
+              placeholder="请输入手机号搜索用户"
+              class="w100"
+              clearable>
+            <template #default="{ item }">
+              <span>{{ item.label }}</span>
+            </template>
+          </el-select-v2>
+        </el-form-item>
+
+        <el-form-item label="备注" prop="remark">
+          <el-input
+              v-model="state.ruleForm.remark"
+              placeholder="请输入备注"
+              type="textarea"
+              :rows="3"
+              class="w100">
+          </el-input>
+        </el-form-item>
+      </el-form>
+
+      <template #footer>
+        <div class="dialog-footer">
+          <el-button @click="onCancel" size="default">取 消</el-button>
+          <el-button :loading="state.btnLoading" type="primary" @click="onSubmit" size="default">{{ state.dialog.submitTxt }}</el-button>
+        </div>
+      </template>
+    </el-drawer>
+  </div>
+</template>
+
+<script setup lang="ts" name="WhitelistDialog">
+import { reactive, ref } from 'vue';
+import { Msg } from "/@/utils/message";
+import { $body, $get } from "/@/utils/request";
+import u from '/@/utils/u';
+import type { FormInstance, FormRules } from 'element-plus';
+import ExtSelect from "/@/components/form/ExtSelect.vue";
+
+const emit = defineEmits(['refresh']);
+const formRef = ref();
+
+const initState = () => ({
+  ruleForm: {
+    id: 0,
+    stationId: '',
+    userId: null as number | null,
+    remark: ''
+  },
+  btnLoading: false,
+  dialog: {
+    isShowDialog: false,
+    type: '',
+    title: '',
+    submitTxt: '',
+  },
+  rules: {
+    stationId: [{ required: true, message: '请选择站点', trigger: 'change' }],
+    userId: [{ required: true, message: '请选择用户', trigger: 'change' }]
+  },
+  userList: [] as Array<{ value: number, label: string }>
+})
+
+const state = reactive(initState());
+
+const open = (action: string = 'add', row: any) => {
+  state.dialog.title = u.dialog.actions[action].title + "『白名单用户』"
+  state.dialog.submitTxt = u.dialog.actions[action].btn + "『白名单用户』"
+  state.dialog.isShowDialog = true;
+  state.dialog.type = action;
+  
+  if (action !== 'add' && row) {
+    state.ruleForm.id = row.id;
+    state.ruleForm.stationId = row.stationId;
+    state.ruleForm.userId = row.userId;
+    state.ruleForm.remark = row.remark;
+    if (row.userPhone) {
+      state.userList = [{ value: row.userId, label: `${row.userPhone} (${row.userNickname || ''})` }];
+    }
+  }
+};
+
+const onClose = () => {
+  state.dialog.isShowDialog = false;
+  Object.assign(state, initState());
+};
+
+const onCancel = () => {
+  onClose();
+};
+
+const onSubmit = () => {
+  formRef.value.validate((valid) => {
+    if (valid) {
+      state.btnLoading = true;
+      $body(`/station-whitelist/add`, state.ruleForm).then(() => {
+        Msg.message('操作成功', 'success');
+        emit('refresh');
+        state.btnLoading = false;
+        onClose();
+      }).catch(() => {
+        state.btnLoading = false;
+      })
+    } else {
+      Msg.message('表单校验失败', 'error');
+    }
+  })
+};
+
+const handleUserSearch = (query: string) => {
+  if (!query || query.length < 4) {
+    return;
+  }
+  $get(`/account/list`, { mobilePhone: query, pageNum: 1, pageSize: 20 }).then((res: any) => {
+    let { list } = res;
+    state.userList = list.map((k: any) => {
+      return { value: k.id, label: `${k.mobilePhone} (${k.nickname || '未设置昵称'})` }
+    });
+  }).catch(() => {
+    state.userList = [];
+  });
+};
+
+defineExpose({
+  open
+});
+</script>

+ 286 - 0
admin-web/src/views/admin/whitelist/index.vue

@@ -0,0 +1,286 @@
+<style scoped lang="scss">
+.system-container {
+
+  :deep(.el-card__body) {
+    display: flex;
+    flex-direction: column;
+    justify-content: space-between;
+    flex: 1;
+    overflow: auto;
+
+    .el-table {
+      flex: 1;
+    }
+
+  }
+}
+
+.page-content {
+  margin-bottom: 20px;
+}
+
+.page-pager {
+  background-color: #fff;
+  height: 24px;
+}
+</style>
+<template>
+  <div class="system-container layout-padding">
+    <el-card shadow="hover" class="layout-padding-auto">
+
+      <el-form
+          :model="state.formQuery"
+          ref="queryRef"
+          size="default" label-width="0px" class="mt5 mb5">
+
+        <ext-select
+            v-model="state.formQuery.stationId"
+            placeholder="选择站点"
+            clearable
+            url="station/listStation"
+            urlMethod="get"
+            data-key=""
+            label-key="stationName"
+            value-key="stationId"
+            @on-change="loadData(true)"
+            class="wd200 mr10">
+        </ext-select>
+
+        <el-input
+            v-model="state.formQuery.userPhone"
+            placeholder="用户手机号"
+            clearable
+            @blur="loadData(true)"
+            class="wd150 mr10">
+        </el-input>
+
+        <el-input
+            v-model="state.formQuery.userNickname"
+            placeholder="用户昵称"
+            clearable
+            @blur="loadData(true)"
+            class="wd150 mr10">
+        </el-input>
+
+        <el-select
+            v-model="state.formQuery.status"
+            placeholder="状态"
+            clearable
+            @change="loadData(true)"
+            class="wd120 mr10">
+          <el-option label="启用" :value="1"/>
+          <el-option label="禁用" :value="0"/>
+        </el-select>
+
+        <el-button class="ml10" plain size="default" type="success" @click="loadData(true)">
+          <SvgIcon name="ele-Search"/>
+          查询
+        </el-button>
+
+        <el-button v-auth="'station.whitelist.add'" size="default" plain type="primary" class="ml10" @click="onRowClick('add', null)">
+          <SvgIcon name="ele-Plus"/>
+          添加用户
+        </el-button>
+
+        <el-button v-auth="'station.whitelist.add'" size="default" plain type="warning" class="ml10" @click="onBatchAdd">
+          <SvgIcon name="ele-DocumentAdd"/>
+          批量添加
+        </el-button>
+
+        <el-button v-auth="'station.whitelist.delete'" size="default" plain type="danger" class="ml10" @click="onBatchRemove" :disabled="state.selectedIds.length === 0">
+          <SvgIcon name="ele-Delete"/>
+          批量移除
+        </el-button>
+      </el-form>
+
+      <el-table
+          border
+          stripe
+          :height="state.tableData.height"
+          highlight-current-row
+          current-row-key="id"
+          row-key="id"
+          :data="state.tableData.data"
+          v-loading="state.tableData.loading"
+          @selection-change="handleTableSelectionChange">
+        <template #empty>
+          <el-empty></el-empty>
+        </template>
+        <el-table-column type="selection" width="55" align="center"/>
+        <el-table-column
+            v-for="field in state.tableData.columns"
+            :key="field.prop"
+            :label="field.label"
+            :column-key="field.prop"
+            :width="field.width"
+            :min-width="field.minWidth"
+            :fixed="field.fixed"
+            :sortable="field.sortable"
+            :show-overflow-tooltip="!field.fixed && field.width > 150">
+          <template #default="{ row }">
+            <template v-if="'status' === field.prop">
+              <el-tag :type="row[field.prop] === 1 ? 'success' : 'danger'">
+                {{ row[field.prop] === 1 ? '启用' : '禁用' }}
+              </el-tag>
+            </template>
+            <template v-else-if="'action' === field.prop">
+              <el-button v-auth="'station.whitelist.modify'" size="small" plain type="warning" @click="onRowClick('edit', row)">编辑</el-button>
+              <el-button v-auth="'station.whitelist.modify'" size="small" plain :type="row.status === 1 ? 'info' : 'success'" @click="onToggleStatus(row)">
+                {{ row.status === 1 ? '禁用' : '启用' }}
+              </el-button>
+              <el-button v-auth="'station.whitelist.delete'" size="small" plain type="danger" @click="onRowDel(row)">移除</el-button>
+            </template>
+            <template v-else>
+              <div>{{ row[field.prop] }}</div>
+            </template>
+          </template>
+        </el-table-column>
+      </el-table>
+
+      <ext-page class="page-pager" v-model:value="state.pageQuery" @change="loadData(false)"/>
+    </el-card>
+  </div>
+  <WhitelistDialog ref="whitelistDialogRef" @refresh="loadData(true)"/>
+  <BatchAddDialog ref="batchAddDialogRef" @refresh="loadData(true)"/>
+</template>
+
+<script setup lang="ts" name="StationWhitelistList">
+import {defineAsyncComponent, reactive, onMounted, onBeforeMount, ref, nextTick, onBeforeUnmount} from 'vue';
+import {$body, $get, $delete, $put} from "/@/utils/request";
+import {Msg} from "/@/utils/message";
+import ExtPage from '/@/components/form/ExtPage.vue'
+import mittBus from '/@/utils/mitt';
+import ExtSelect from "/@/components/form/ExtSelect.vue";
+import {useRoute} from "vue-router";
+
+const route = useRoute();
+const WhitelistDialog = defineAsyncComponent(() => import("/@/views/admin/whitelist/dialog.vue"));
+const BatchAddDialog = defineAsyncComponent(() => import("/@/views/admin/whitelist/batch.vue"));
+
+const queryRef = ref();
+const whitelistDialogRef = ref();
+const batchAddDialogRef = ref();
+
+const state = reactive({
+  formQuery: {
+    stationId: '',
+    userPhone: '',
+    userNickname: '',
+    status: null as number | null
+  },
+  pageQuery: {
+    pageNum: 1,
+    pageSize: 10,
+    total: 0
+  },
+  tableData: {
+    height: 500,
+    data: [] as Array<any>,
+    loading: false,
+    columns: [
+      {label: '站点名称', prop: 'stationName', width: 180, resizable: true},
+      {label: '用户手机号', prop: 'userPhone', width: 140, resizable: true},
+      {label: '用户昵称', prop: 'userNickname', width: 150, resizable: true},
+      {label: '状态', prop: 'status', width: 100, align: 'center'},
+      {label: '备注', prop: 'remark', minWidth: 200, resizable: true},
+      {label: '创建时间', prop: 'createTime', width: 180, resizable: true},
+      {
+        label: '操作', prop: 'action', width: 260, align: 'center', fixed: 'right',
+      }
+    ],
+  },
+  selectedIds: [] as number[]
+})
+
+onBeforeMount(() => {
+  const stationId = route.query.stationId as string;
+  if (stationId) {
+    state.formQuery.stationId = stationId;
+  }
+})
+
+onMounted(() => {
+  loadData();
+
+  nextTick(() => {
+    let bodyHeight = document.body.clientHeight;
+    let queryHeight = queryRef.value.$el.clientHeight;
+    state.tableData.height = bodyHeight - queryHeight - 320
+  })
+
+  mittBus.on("stationWhitelist.refresh", () => {
+    loadData();
+  })
+});
+
+onBeforeUnmount(() => {
+  mittBus.off("stationWhitelist.refresh")
+})
+
+const loadData = (refresh: boolean = false) => {
+  if (refresh) {
+    state.pageQuery.pageNum = 1;
+  }
+  state.tableData.loading = true;
+  $get(`/station-whitelist/list`, { ...state.formQuery, ...state.pageQuery }).then((res: any) => {
+    let { list, total } = res;
+    state.tableData.data = list;
+    state.pageQuery.total = total;
+    state.tableData.loading = false;
+  }).catch(e => {
+    console.error(e)
+    state.tableData.loading = false;
+  })
+};
+
+const onRowClick = (type: string, row: any) => {
+  whitelistDialogRef.value.open(type, row);
+};
+
+const onBatchAdd = () => {
+  batchAddDialogRef.value.open();
+};
+
+const onBatchRemove = () => {
+  if (state.selectedIds.length === 0) {
+    Msg.message('请选择要移除的用户', 'warning');
+    return;
+  }
+  Msg.confirm(`此操作将移除选中的 ${state.selectedIds.length} 个用户,是否继续?`).then(() => {
+    $delete(`/station-whitelist/batchRemove`, state.selectedIds).then(() => {
+      Msg.message("移除成功", 'success');
+      loadData(true);
+    }).catch(() => {
+      Msg.message("移除失败", 'error');
+    })
+  });
+};
+
+const onRowDel = (row: any) => {
+  Msg.confirm(`此操作将移除用户『${row.userNickname || row.userPhone}』,是否继续?`).then(() => {
+    $delete(`/station-whitelist/remove/${row.id}`).then(() => {
+      Msg.message("移除成功", 'success');
+      loadData(true);
+    }).catch(() => {
+      Msg.message("移除失败", 'error');
+    })
+  });
+};
+
+const onToggleStatus = (row: any) => {
+  const newStatus = row.status === 1 ? 0 : 1;
+  const statusText = newStatus === 1 ? '启用' : '禁用';
+  Msg.confirm(`确定要${statusText}用户『${row.userNickname || row.userPhone}』吗?`).then(() => {
+    $put(`/station-whitelist/status/${row.id}?status=${newStatus}`).then(() => {
+      Msg.message(`${statusText}成功`, 'success');
+      loadData(true);
+    }).catch(() => {
+      Msg.message(`${statusText}失败`, 'error');
+    })
+  });
+};
+
+const handleTableSelectionChange = (selection: any) => {
+  state.selectedIds = selection.map((item: any) => item.id);
+}
+</script>

+ 31 - 0
admin/src/main/java/com/kym/admin/controller/AccountController.java

@@ -0,0 +1,31 @@
+package com.kym.admin.controller;
+
+import cn.dev33.satoken.annotation.SaCheckPermission;
+import cn.dev33.satoken.annotation.SaMode;
+import com.kym.common.R;
+import com.kym.common.annotation.SysLog;
+import com.kym.common.controller.IController;
+import com.kym.entity.admin.queryParams.CommonQueryParam;
+import com.kym.service.miniapp.UserService;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.ModelAttribute;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+@RestController
+@RequestMapping("/account")
+public class AccountController extends IController {
+
+    private final UserService userService;
+
+    public AccountController(UserService userService) {
+        this.userService = userService;
+    }
+
+    @GetMapping("/list")
+    @SaCheckPermission(value = {"account.list", "station.whitelist.add"}, mode = SaMode.OR)
+    @SysLog("查询用户列表(简略)")
+    public R<?> list(@ModelAttribute CommonQueryParam params) {
+        return R.success(userService.listSimpleUser(params));
+    }
+}

+ 108 - 0
admin/src/main/java/com/kym/admin/controller/StationWhitelistController.java

@@ -0,0 +1,108 @@
+package com.kym.admin.controller;
+
+import cn.dev33.satoken.annotation.SaCheckPermission;
+import com.kym.common.R;
+import com.kym.common.annotation.SysLog;
+import com.kym.common.controller.IController;
+import com.kym.entity.admin.StationWhitelist;
+import com.kym.entity.admin.StationWhitelistConfig;
+import com.kym.entity.admin.dto.BatchAddWhitelistDto;
+import com.kym.entity.admin.queryParams.StationWhitelistQueryParam;
+import com.kym.entity.admin.vo.StationWhitelistConfigVo;
+import com.kym.service.admin.StationWhitelistConfigService;
+import com.kym.service.admin.StationWhitelistService;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+@RestController
+@RequestMapping("/station-whitelist")
+public class StationWhitelistController extends IController {
+
+    private final StationWhitelistService stationWhitelistService;
+    private final StationWhitelistConfigService stationWhitelistConfigService;
+
+    public StationWhitelistController(StationWhitelistService stationWhitelistService,
+                                       StationWhitelistConfigService stationWhitelistConfigService) {
+        this.stationWhitelistService = stationWhitelistService;
+        this.stationWhitelistConfigService = stationWhitelistConfigService;
+    }
+
+    @GetMapping("/list")
+    @SaCheckPermission("station.whitelist.list")
+    @SysLog("站点白名单-查询列表")
+    public R<?> list(@ModelAttribute StationWhitelistQueryParam param) {
+        return R.success(stationWhitelistService.listWhitelist(param));
+    }
+
+    @PostMapping("/add")
+    @SaCheckPermission("station.whitelist.add")
+    @SysLog(value = "站点白名单-添加用户", ignoreParams = true)
+    public R<?> add(@RequestBody StationWhitelist whitelist) {
+        stationWhitelistService.addWhitelist(whitelist);
+        return R.success();
+    }
+
+    @PostMapping("/batchAdd")
+    @SaCheckPermission("station.whitelist.add")
+    @SysLog(value = "站点白名单-批量添加用户", ignoreParams = true)
+    public R<?> batchAdd(@RequestBody BatchAddWhitelistDto dto) {
+        stationWhitelistService.batchAddWhitelist(dto);
+        return R.success();
+    }
+
+    @DeleteMapping("/remove/{id}")
+    @SaCheckPermission("station.whitelist.delete")
+    @SysLog("站点白名单-移除用户")
+    public R<?> remove(@PathVariable Long id) {
+        stationWhitelistService.removeWhitelist(id);
+        return R.success();
+    }
+
+    @DeleteMapping("/batchRemove")
+    @SaCheckPermission("station.whitelist.delete")
+    @SysLog("站点白名单-批量移除用户")
+    public R<?> batchRemove(@RequestBody List<Long> ids) {
+        stationWhitelistService.batchRemoveWhitelist(ids);
+        return R.success();
+    }
+
+    @PutMapping("/status/{id}")
+    @SaCheckPermission("station.whitelist.modify")
+    @SysLog("站点白名单-更新状态")
+    public R<?> updateStatus(@PathVariable Long id, @RequestParam Integer status) {
+        stationWhitelistService.updateStatus(id, status);
+        return R.success();
+    }
+
+    @GetMapping("/config/list")
+    @SaCheckPermission("station.whitelist.config.list")
+    @SysLog("站点白名单配置-查询列表")
+    public R<?> listConfig() {
+        return R.success(stationWhitelistConfigService.listConfig());
+    }
+
+    @GetMapping("/config/station/{stationId}")
+    @SaCheckPermission("station.whitelist.config.list")
+    @SysLog("站点白名单配置-查询站点配置")
+    public R<?> getConfigByStationId(@PathVariable String stationId) {
+        StationWhitelistConfig config = stationWhitelistConfigService.getConfigByStationId(stationId);
+        return R.success(config);
+    }
+
+    @PutMapping("/config/update")
+    @SaCheckPermission("station.whitelist.config.modify")
+    @SysLog("站点白名单配置-更新配置")
+    public R<?> updateConfig(@RequestParam String stationId, @RequestParam Integer enabled) {
+        stationWhitelistConfigService.updateConfig(stationId, enabled);
+        return R.success();
+    }
+
+    @PostMapping("/refreshCache/{stationId}")
+    @SaCheckPermission("station.whitelist.modify")
+    @SysLog("站点白名单-刷新缓存")
+    public R<?> refreshCache(@PathVariable String stationId) {
+        stationWhitelistService.refreshCache(stationId);
+        return R.success();
+    }
+}

+ 20 - 0
admin/src/main/resources/static/index.html

@@ -0,0 +1,20 @@
+<!DOCTYPE html>
+<html lang="zh-CN">
+<head>
+    <meta charset="utf-8"/>
+    <meta http-equiv="X-UA-Compatible" content="IE=edge"/>
+    <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
+    <meta name="keywords" content="vue-next-admin"/>
+    <meta name="description" content="快与慢运营管理平台"/>
+    <link rel="icon" href="./assets/logo.46850193.png"/>
+    <title>快与慢运营管理平台</title>
+
+  <script type="module" crossorigin src="./assets/index.569521c2.js"></script>
+  <link rel="modulepreload" crossorigin href="./assets/vue.6ae8aee9.js">
+  <link rel="stylesheet" href="./assets/index.63d29cea.css">
+</head>
+<body>
+<div id="app"></div>
+
+</body>
+</html>

BIN
admin/src/main/resources/static/logo.png


+ 4 - 0
common/src/main/java/com/kym/common/constant/ResponseEnum.java

@@ -48,6 +48,10 @@ public enum ResponseEnum implements BusinessExceptionAssert {
     PLATFORM_EQUIP_STOP_FAIL(20012, "设备停止充电失败"),
     PLATFORM_EQUIP_EXIST_ORDER_UNFINISHED(20013, "设备存在未完成的订单"),
     ORDER_IN_BOOKING(20014, "用户有预约中的订单"),
+    STATION_WHITELIST_NOT_ENABLED(20015, "站点未启用白名单功能"),
+    USER_NOT_IN_WHITELIST(20016, "您不在该站点的白名单中,无法启动充电"),
+    STATION_WHITELIST_ALREADY_EXISTS(20017, "用户已在该站点白名单中"),
+    STATION_WHITELIST_NOT_EXISTS(20018, "白名单记录不存在"),
 
 
     // 互联互通平台

+ 39 - 0
entity/src/main/java/com/kym/entity/admin/StationWhitelist.java

@@ -0,0 +1,39 @@
+package com.kym.entity.admin;
+
+import com.baomidou.mybatisplus.annotation.TableName;
+import com.kym.entity.BaseEntity;
+import lombok.Getter;
+import lombok.Setter;
+import lombok.experimental.Accessors;
+
+import java.io.Serializable;
+
+@Getter
+@Setter
+@TableName("t_station_whitelist")
+@Accessors(chain = true)
+public class StationWhitelist extends BaseEntity implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    public static final int STATUS_禁用 = 0;
+    public static final int STATUS_启用 = 1;
+
+    private Long companyId;
+
+    private String stationId;
+
+    private String stationName;
+
+    private Long userId;
+
+    private String userPhone;
+
+    private String userNickname;
+
+    private Integer status;
+
+    private String remark;
+
+    private Long createBy;
+}

+ 31 - 0
entity/src/main/java/com/kym/entity/admin/StationWhitelistConfig.java

@@ -0,0 +1,31 @@
+package com.kym.entity.admin;
+
+import com.baomidou.mybatisplus.annotation.TableName;
+import com.kym.entity.BaseEntity;
+import lombok.Getter;
+import lombok.Setter;
+import lombok.experimental.Accessors;
+
+import java.io.Serializable;
+
+@Getter
+@Setter
+@TableName("t_station_whitelist_config")
+@Accessors(chain = true)
+public class StationWhitelistConfig extends BaseEntity implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    public static final int ENABLED_否 = 0;
+    public static final int ENABLED_是 = 1;
+
+    private Long companyId;
+
+    private String stationId;
+
+    private String stationName;
+
+    private Integer enabled;
+
+    private Long createBy;
+}

+ 44 - 0
entity/src/main/java/com/kym/entity/admin/StationWhitelistLog.java

@@ -0,0 +1,44 @@
+package com.kym.entity.admin;
+
+import com.baomidou.mybatisplus.annotation.TableName;
+import com.kym.entity.BaseEntity;
+import lombok.Getter;
+import lombok.Setter;
+import lombok.experimental.Accessors;
+
+import java.io.Serializable;
+
+@Getter
+@Setter
+@TableName("t_station_whitelist_log")
+@Accessors(chain = true)
+public class StationWhitelistLog extends BaseEntity implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    public static final int OPERATION_TYPE_添加 = 1;
+    public static final int OPERATION_TYPE_删除 = 2;
+    public static final int OPERATION_TYPE_启用 = 3;
+    public static final int OPERATION_TYPE_禁用 = 4;
+    public static final int OPERATION_TYPE_批量添加 = 5;
+    public static final int OPERATION_TYPE_批量删除 = 6;
+    public static final int OPERATION_TYPE_配置变更 = 7;
+
+    private Long companyId;
+
+    private String stationId;
+
+    private String stationName;
+
+    private Long userId;
+
+    private String userPhone;
+
+    private Integer operationType;
+
+    private String operationDesc;
+
+    private Long operatorId;
+
+    private String operatorName;
+}

+ 18 - 0
entity/src/main/java/com/kym/entity/admin/dto/BatchAddWhitelistDto.java

@@ -0,0 +1,18 @@
+package com.kym.entity.admin.dto;
+
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.List;
+
+@Data
+public class BatchAddWhitelistDto implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    private String stationId;
+
+    private List<Long> userIds;
+
+    private String remark;
+}

+ 17 - 0
entity/src/main/java/com/kym/entity/admin/queryParams/StationWhitelistQueryParam.java

@@ -0,0 +1,17 @@
+package com.kym.entity.admin.queryParams;
+
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class StationWhitelistQueryParam extends CommonQueryParam {
+
+    private String stationId;
+
+    private String userPhone;
+
+    private String userNickname;
+
+    private Integer status;
+}

+ 24 - 0
entity/src/main/java/com/kym/entity/admin/vo/StationWhitelistConfigVo.java

@@ -0,0 +1,24 @@
+package com.kym.entity.admin.vo;
+
+import lombok.Data;
+
+import java.io.Serializable;
+import java.time.LocalDateTime;
+
+@Data
+public class StationWhitelistConfigVo implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    private Long id;
+
+    private String stationId;
+
+    private String stationName;
+
+    private Integer enabled;
+
+    private LocalDateTime createTime;
+
+    private LocalDateTime updateTime;
+}

+ 32 - 0
entity/src/main/java/com/kym/entity/admin/vo/StationWhitelistVo.java

@@ -0,0 +1,32 @@
+package com.kym.entity.admin.vo;
+
+import lombok.Data;
+
+import java.io.Serializable;
+import java.time.LocalDateTime;
+
+@Data
+public class StationWhitelistVo implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    private Long id;
+
+    private String stationId;
+
+    private String stationName;
+
+    private Long userId;
+
+    private String userPhone;
+
+    private String userNickname;
+
+    private Integer status;
+
+    private String remark;
+
+    private LocalDateTime createTime;
+
+    private LocalDateTime updateTime;
+}

+ 10 - 0
entity/src/main/java/com/kym/entity/common/RedisKeys.java

@@ -35,4 +35,14 @@ public interface RedisKeys {
      * 站点费率
      */
     String STATION_ID_POLICY_INFO = "STATION_ID_POLICY_INFO:";
+
+    /**
+     * 站点白名单配置(站点ID -> 是否启用)
+     */
+    String STATION_WHITELIST_CONFIG = "STATION_WHITELIST_CONFIG:";
+
+    /**
+     * 站点白名单用户列表(站点ID -> 用户ID集合)
+     */
+    String STATION_WHITELIST_USERS = "STATION_WHITELIST_USERS:";
 }

+ 8 - 0
mapper/src/main/java/com/kym/mapper/admin/StationWhitelistConfigMapper.java

@@ -0,0 +1,8 @@
+package com.kym.mapper.admin;
+
+import com.github.yulichang.base.MPJBaseMapper;
+import com.kym.entity.admin.StationWhitelistConfig;
+
+public interface StationWhitelistConfigMapper extends MPJBaseMapper<StationWhitelistConfig> {
+
+}

+ 8 - 0
mapper/src/main/java/com/kym/mapper/admin/StationWhitelistLogMapper.java

@@ -0,0 +1,8 @@
+package com.kym.mapper.admin;
+
+import com.github.yulichang.base.MPJBaseMapper;
+import com.kym.entity.admin.StationWhitelistLog;
+
+public interface StationWhitelistLogMapper extends MPJBaseMapper<StationWhitelistLog> {
+
+}

+ 8 - 0
mapper/src/main/java/com/kym/mapper/admin/StationWhitelistMapper.java

@@ -0,0 +1,8 @@
+package com.kym.mapper.admin;
+
+import com.github.yulichang.base.MPJBaseMapper;
+import com.kym.entity.admin.StationWhitelist;
+
+public interface StationWhitelistMapper extends MPJBaseMapper<StationWhitelist> {
+
+}

+ 20 - 0
service/src/main/java/com/kym/service/admin/StationWhitelistConfigService.java

@@ -0,0 +1,20 @@
+package com.kym.service.admin;
+
+import com.github.yulichang.base.MPJBaseService;
+import com.kym.entity.admin.StationWhitelistConfig;
+import com.kym.entity.admin.vo.StationWhitelistConfigVo;
+
+import java.util.List;
+
+public interface StationWhitelistConfigService extends MPJBaseService<StationWhitelistConfig> {
+
+    List<StationWhitelistConfigVo> listConfig();
+
+    StationWhitelistConfig getConfigByStationId(String stationId);
+
+    void updateConfig(String stationId, Integer enabled);
+
+    boolean isWhitelistEnabled(String stationId);
+
+    void refreshConfigCache();
+}

+ 19 - 0
service/src/main/java/com/kym/service/admin/StationWhitelistLogService.java

@@ -0,0 +1,19 @@
+package com.kym.service.admin;
+
+import com.github.yulichang.base.MPJBaseService;
+import com.kym.entity.admin.StationWhitelistLog;
+
+public interface StationWhitelistLogService extends MPJBaseService<StationWhitelistLog> {
+
+    void logAdd(String stationId, String stationName, Long userId, String userPhone, Long operatorId, String operatorName);
+
+    void logBatchAdd(String stationId, String stationName, int count, Long operatorId, String operatorName);
+
+    void logRemove(String stationId, String stationName, Long userId, String userPhone, Long operatorId, String operatorName);
+
+    void logBatchRemove(String stationId, String stationName, int count, Long operatorId, String operatorName);
+
+    void logStatusChange(String stationId, String stationName, Long userId, String userPhone, Integer status, Long operatorId, String operatorName);
+
+    void logConfigChange(String stationId, String stationName, Integer enabled, Long operatorId, String operatorName);
+}

+ 33 - 0
service/src/main/java/com/kym/service/admin/StationWhitelistService.java

@@ -0,0 +1,33 @@
+package com.kym.service.admin;
+
+import com.github.yulichang.base.MPJBaseService;
+import com.kym.entity.admin.StationWhitelist;
+import com.kym.entity.admin.dto.BatchAddWhitelistDto;
+import com.kym.entity.admin.queryParams.StationWhitelistQueryParam;
+import com.kym.entity.admin.vo.StationWhitelistVo;
+import com.kym.entity.common.PageBean;
+
+import java.util.List;
+
+public interface StationWhitelistService extends MPJBaseService<StationWhitelist> {
+
+    PageBean<StationWhitelistVo> listWhitelist(StationWhitelistQueryParam param);
+
+    void addWhitelist(StationWhitelist whitelist);
+
+    void batchAddWhitelist(BatchAddWhitelistDto dto);
+
+    void removeWhitelist(Long id);
+
+    void batchRemoveWhitelist(List<Long> ids);
+
+    void updateStatus(Long id, Integer status);
+
+    boolean isInWhitelist(String stationId, Long userId);
+
+    List<Long> getWhitelistUserIds(String stationId);
+
+    void refreshCache(String stationId);
+
+    void refreshAllCache();
+}

+ 128 - 0
service/src/main/java/com/kym/service/admin/impl/StationWhitelistConfigServiceImpl.java

@@ -0,0 +1,128 @@
+package com.kym.service.admin.impl;
+
+import cn.dev33.satoken.stp.StpUtil;
+import com.baomidou.dynamic.datasource.annotation.DS;
+import com.baomidou.dynamic.datasource.toolkit.DynamicDataSourceContextHolder;
+import com.github.yulichang.base.MPJBaseServiceImpl;
+import com.github.yulichang.wrapper.MPJLambdaWrapper;
+import com.kym.common.utils.CommUtil;
+import com.kym.common.utils.IDGenerator;
+import com.kym.entity.admin.Station;
+import com.kym.entity.admin.StationWhitelistConfig;
+import com.kym.entity.admin.vo.StationWhitelistConfigVo;
+import com.kym.entity.common.RedisKeys;
+import com.kym.mapper.admin.StationWhitelistConfigMapper;
+import com.kym.service.admin.StationService;
+import com.kym.service.admin.StationWhitelistConfigService;
+import com.kym.service.admin.StationWhitelistLogService;
+import jakarta.annotation.PostConstruct;
+import org.springframework.data.redis.core.StringRedisTemplate;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+@Service
+@DS("db-admin")
+public class StationWhitelistConfigServiceImpl extends MPJBaseServiceImpl<StationWhitelistConfigMapper, StationWhitelistConfig> implements StationWhitelistConfigService {
+
+    private static final long CACHE_EXPIRE_HOURS = 24;
+    private final IDGenerator idGenerator = new IDGenerator();
+    private final StationWhitelistConfigMapper stationWhitelistConfigMapper;
+    private final StationService stationService;
+    private final StationWhitelistLogService stationWhitelistLogService;
+    private final StringRedisTemplate redisTemplate;
+
+    public StationWhitelistConfigServiceImpl(StationWhitelistConfigMapper stationWhitelistConfigMapper,
+                                              StationService stationService,
+                                              StationWhitelistLogService stationWhitelistLogService,
+                                              StringRedisTemplate redisTemplate) {
+        this.stationWhitelistConfigMapper = stationWhitelistConfigMapper;
+        this.stationService = stationService;
+        this.stationWhitelistLogService = stationWhitelistLogService;
+        this.redisTemplate = redisTemplate;
+    }
+
+    @PostConstruct
+    public void init() {
+        DynamicDataSourceContextHolder.push("db-admin");
+        refreshConfigCache();
+        DynamicDataSourceContextHolder.poll();
+    }
+
+    @Override
+    public List<StationWhitelistConfigVo> listConfig() {
+        return stationWhitelistConfigMapper.selectJoinList(StationWhitelistConfigVo.class,
+                new MPJLambdaWrapper<StationWhitelistConfig>()
+                        .selectAll(StationWhitelistConfig.class)
+                        .orderByDesc(StationWhitelistConfig::getCreateTime)
+        );
+    }
+
+    @Override
+    public StationWhitelistConfig getConfigByStationId(String stationId) {
+        return lambdaQuery()
+                .eq(StationWhitelistConfig::getStationId, stationId)
+                .one();
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void updateConfig(String stationId, Integer enabled) {
+        CommUtil.assertsNonNull(stationId, "站点ID不能为空");
+
+        Station station = stationService.lambdaQuery()
+                .eq(Station::getStationId, stationId)
+                .one();
+        CommUtil.assertsNonNull(station, "站点不存在");
+
+        StationWhitelistConfig config = getConfigByStationId(stationId);
+        Long operatorId = StpUtil.getLoginIdAsLong();
+        String operatorName = StpUtil.getSession().getString("mobilePhone");
+
+        if (config == null) {
+            config = new StationWhitelistConfig();
+            config.setId(idGenerator.nextId());
+            config.setStationId(stationId);
+            config.setStationName(station.getStationName());
+            config.setEnabled(enabled);
+            config.setCreateBy(operatorId);
+            save(config);
+        } else {
+            lambdaUpdate()
+                    .set(StationWhitelistConfig::getEnabled, enabled)
+                    .eq(StationWhitelistConfig::getStationId, stationId)
+                    .update();
+        }
+
+        String key = RedisKeys.STATION_WHITELIST_CONFIG + stationId;
+        redisTemplate.opsForValue().set(key, String.valueOf(enabled), CACHE_EXPIRE_HOURS, TimeUnit.HOURS);
+
+        stationWhitelistLogService.logConfigChange(stationId, station.getStationName(), enabled, operatorId, operatorName);
+    }
+
+    @Override
+    public boolean isWhitelistEnabled(String stationId) {
+        String key = RedisKeys.STATION_WHITELIST_CONFIG + stationId;
+        String value = redisTemplate.opsForValue().get(key);
+        if (value != null) {
+            return "1".equals(value);
+        }
+        StationWhitelistConfig config = getConfigByStationId(stationId);
+        boolean enabled = config != null && config.getEnabled() == StationWhitelistConfig.ENABLED_是;
+        redisTemplate.opsForValue().set(key, String.valueOf(enabled ? 1 : 0), CACHE_EXPIRE_HOURS, TimeUnit.HOURS);
+        return enabled;
+    }
+
+    @Override
+    public void refreshConfigCache() {
+        List<StationWhitelistConfig> list = list();
+        for (StationWhitelistConfig config : list) {
+            String key = RedisKeys.STATION_WHITELIST_CONFIG + config.getStationId();
+            redisTemplate.opsForValue().set(key,
+                    String.valueOf(config.getEnabled()),
+                    CACHE_EXPIRE_HOURS, TimeUnit.HOURS);
+        }
+    }
+}

+ 100 - 0
service/src/main/java/com/kym/service/admin/impl/StationWhitelistLogServiceImpl.java

@@ -0,0 +1,100 @@
+package com.kym.service.admin.impl;
+
+import com.baomidou.dynamic.datasource.annotation.DS;
+import com.github.yulichang.base.MPJBaseServiceImpl;
+import com.kym.common.utils.IDGenerator;
+import com.kym.entity.admin.StationWhitelistLog;
+import com.kym.mapper.admin.StationWhitelistLogMapper;
+import com.kym.service.admin.StationWhitelistLogService;
+import org.springframework.stereotype.Service;
+
+@Service
+@DS("db-admin")
+public class StationWhitelistLogServiceImpl extends MPJBaseServiceImpl<StationWhitelistLogMapper, StationWhitelistLog> implements StationWhitelistLogService {
+
+    private final IDGenerator idGenerator = new IDGenerator();
+
+    @Override
+    public void logAdd(String stationId, String stationName, Long userId, String userPhone, Long operatorId, String operatorName) {
+        StationWhitelistLog log = new StationWhitelistLog();
+        log.setId(idGenerator.nextId());
+        log.setStationId(stationId);
+        log.setStationName(stationName);
+        log.setUserId(userId);
+        log.setUserPhone(userPhone);
+        log.setOperationType(StationWhitelistLog.OPERATION_TYPE_添加);
+        log.setOperationDesc("添加用户到白名单");
+        log.setOperatorId(operatorId);
+        log.setOperatorName(operatorName);
+        save(log);
+    }
+
+    @Override
+    public void logBatchAdd(String stationId, String stationName, int count, Long operatorId, String operatorName) {
+        StationWhitelistLog log = new StationWhitelistLog();
+        log.setId(idGenerator.nextId());
+        log.setStationId(stationId);
+        log.setStationName(stationName);
+        log.setOperationType(StationWhitelistLog.OPERATION_TYPE_批量添加);
+        log.setOperationDesc("批量添加" + count + "个用户到白名单");
+        log.setOperatorId(operatorId);
+        log.setOperatorName(operatorName);
+        save(log);
+    }
+
+    @Override
+    public void logRemove(String stationId, String stationName, Long userId, String userPhone, Long operatorId, String operatorName) {
+        StationWhitelistLog log = new StationWhitelistLog();
+        log.setId(idGenerator.nextId());
+        log.setStationId(stationId);
+        log.setStationName(stationName);
+        log.setUserId(userId);
+        log.setUserPhone(userPhone);
+        log.setOperationType(StationWhitelistLog.OPERATION_TYPE_删除);
+        log.setOperationDesc("从白名单移除用户");
+        log.setOperatorId(operatorId);
+        log.setOperatorName(operatorName);
+        save(log);
+    }
+
+    @Override
+    public void logBatchRemove(String stationId, String stationName, int count, Long operatorId, String operatorName) {
+        StationWhitelistLog log = new StationWhitelistLog();
+        log.setId(idGenerator.nextId());
+        log.setStationId(stationId);
+        log.setStationName(stationName);
+        log.setOperationType(StationWhitelistLog.OPERATION_TYPE_批量删除);
+        log.setOperationDesc("批量移除" + count + "个用户");
+        log.setOperatorId(operatorId);
+        log.setOperatorName(operatorName);
+        save(log);
+    }
+
+    @Override
+    public void logStatusChange(String stationId, String stationName, Long userId, String userPhone, Integer status, Long operatorId, String operatorName) {
+        StationWhitelistLog log = new StationWhitelistLog();
+        log.setId(idGenerator.nextId());
+        log.setStationId(stationId);
+        log.setStationName(stationName);
+        log.setUserId(userId);
+        log.setUserPhone(userPhone);
+        log.setOperationType(status == 1 ? StationWhitelistLog.OPERATION_TYPE_启用 : StationWhitelistLog.OPERATION_TYPE_禁用);
+        log.setOperationDesc(status == 1 ? "启用白名单用户" : "禁用白名单用户");
+        log.setOperatorId(operatorId);
+        log.setOperatorName(operatorName);
+        save(log);
+    }
+
+    @Override
+    public void logConfigChange(String stationId, String stationName, Integer enabled, Long operatorId, String operatorName) {
+        StationWhitelistLog log = new StationWhitelistLog();
+        log.setId(idGenerator.nextId());
+        log.setStationId(stationId);
+        log.setStationName(stationName);
+        log.setOperationType(StationWhitelistLog.OPERATION_TYPE_配置变更);
+        log.setOperationDesc(enabled == 1 ? "启用站点白名单功能" : "禁用站点白名单功能");
+        log.setOperatorId(operatorId);
+        log.setOperatorName(operatorName);
+        save(log);
+    }
+}

+ 307 - 0
service/src/main/java/com/kym/service/admin/impl/StationWhitelistServiceImpl.java

@@ -0,0 +1,307 @@
+package com.kym.service.admin.impl;
+
+import cn.dev33.satoken.stp.StpUtil;
+import com.baomidou.dynamic.datasource.annotation.DS;
+import com.baomidou.dynamic.datasource.toolkit.DynamicDataSourceContextHolder;
+import com.github.pagehelper.PageHelper;
+import com.github.yulichang.base.MPJBaseServiceImpl;
+import com.kym.common.exception.BusinessException;
+import com.kym.common.utils.CommUtil;
+import com.kym.common.utils.IDGenerator;
+import com.kym.entity.admin.Station;
+import com.kym.entity.admin.StationWhitelist;
+import com.kym.entity.admin.StationWhitelistLog;
+import com.kym.entity.admin.dto.BatchAddWhitelistDto;
+import com.kym.entity.admin.queryParams.StationWhitelistQueryParam;
+import com.kym.entity.admin.vo.StationWhitelistVo;
+import com.kym.entity.common.PageBean;
+import com.kym.entity.common.RedisKeys;
+import com.kym.entity.miniapp.User;
+import com.kym.mapper.admin.StationWhitelistMapper;
+import com.kym.service.admin.StationService;
+import com.kym.service.admin.StationWhitelistLogService;
+import com.kym.service.admin.StationWhitelistService;
+import com.kym.service.miniapp.UserService;
+import jakarta.annotation.PostConstruct;
+import org.springframework.data.redis.core.StringRedisTemplate;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.*;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
+
+@Service
+@DS("db-admin")
+public class StationWhitelistServiceImpl extends MPJBaseServiceImpl<StationWhitelistMapper, StationWhitelist> implements StationWhitelistService {
+
+    private static final long CACHE_EXPIRE_HOURS = 24;
+    private final IDGenerator idGenerator = new IDGenerator();
+    private final StationWhitelistMapper stationWhitelistMapper;
+    private final StationService stationService;
+    private final UserService userService;
+    private final StationWhitelistLogService stationWhitelistLogService;
+    private final StringRedisTemplate redisTemplate;
+
+    public StationWhitelistServiceImpl(StationWhitelistMapper stationWhitelistMapper,
+                                        StationService stationService,
+                                        UserService userService,
+                                        StationWhitelistLogService stationWhitelistLogService,
+                                        StringRedisTemplate redisTemplate) {
+        this.stationWhitelistMapper = stationWhitelistMapper;
+        this.stationService = stationService;
+        this.userService = userService;
+        this.stationWhitelistLogService = stationWhitelistLogService;
+        this.redisTemplate = redisTemplate;
+    }
+
+    @PostConstruct
+    public void init() {
+        DynamicDataSourceContextHolder.push("db-admin");
+        refreshAllCache();
+        DynamicDataSourceContextHolder.poll();
+    }
+
+    @Override
+    public PageBean<StationWhitelistVo> listWhitelist(StationWhitelistQueryParam param) {
+        PageHelper.startPage(param.getPageNum(), param.getPageSize());
+        List<StationWhitelistVo> list = stationWhitelistMapper.selectJoinList(StationWhitelistVo.class,
+                new com.github.yulichang.wrapper.MPJLambdaWrapper<StationWhitelist>()
+                        .selectAll(StationWhitelist.class)
+                        .select(User::getMobilePhone, User::getNickname)
+                        .leftJoin(User.class, User::getId, StationWhitelist::getUserId)
+                        .eq(!CommUtil.isEmptyOrNull(param.getStationId()), StationWhitelist::getStationId, param.getStationId())
+                        .like(!CommUtil.isEmptyOrNull(param.getUserPhone()), User::getMobilePhone, param.getUserPhone())
+                        .like(!CommUtil.isEmptyOrNull(param.getUserNickname()), User::getNickname, param.getUserNickname())
+                        .eq(!CommUtil.isEmptyOrNull(param.getStatus()), StationWhitelist::getStatus, param.getStatus())
+                        .orderByDesc(StationWhitelist::getCreateTime)
+        );
+        return new PageBean<>(list);
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void addWhitelist(StationWhitelist whitelist) {
+        CommUtil.assertsNonNull(whitelist.getStationId(), "站点ID不能为空");
+        CommUtil.asserts(whitelist.getUserId() != null && whitelist.getUserId() > 0, "用户ID不能为空");
+
+        Station station = stationService.lambdaQuery()
+                .eq(Station::getStationId, whitelist.getStationId())
+                .one();
+        CommUtil.assertsNonNull(station, "站点不存在");
+
+        User user = userService.getById(whitelist.getUserId());
+        CommUtil.assertsNonNull(user, "用户不存在");
+
+        StationWhitelist exists = lambdaQuery()
+                .eq(StationWhitelist::getStationId, whitelist.getStationId())
+                .eq(StationWhitelist::getUserId, whitelist.getUserId())
+                .one();
+        if (exists != null) {
+            throw new BusinessException("用户已在该站点白名单中");
+        }
+
+        whitelist.setId(idGenerator.nextId());
+        whitelist.setStationName(station.getStationName());
+        whitelist.setUserPhone(user.getMobilePhone());
+        whitelist.setUserNickname(user.getNickname());
+        whitelist.setStatus(StationWhitelist.STATUS_启用);
+        whitelist.setCreateBy(StpUtil.getLoginIdAsLong());
+
+        save(whitelist);
+
+        refreshCache(whitelist.getStationId());
+
+        stationWhitelistLogService.logAdd(whitelist.getStationId(), whitelist.getStationName(),
+                whitelist.getUserId(), whitelist.getUserPhone(),
+                StpUtil.getLoginIdAsLong(), StpUtil.getSession().getString("mobilePhone"));
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void batchAddWhitelist(BatchAddWhitelistDto dto) {
+        CommUtil.assertsNonNull(dto.getStationId(), "站点ID不能为空");
+        CommUtil.asserts(dto.getUserIds() != null && !dto.getUserIds().isEmpty(), "用户ID列表不能为空");
+
+        Station station = stationService.lambdaQuery()
+                .eq(Station::getStationId, dto.getStationId())
+                .one();
+        CommUtil.assertsNonNull(station, "站点不存在");
+
+        List<StationWhitelist> existingList = lambdaQuery()
+                .eq(StationWhitelist::getStationId, dto.getStationId())
+                .in(StationWhitelist::getUserId, dto.getUserIds())
+                .list();
+        Set<Long> existingUserIds = existingList.stream()
+                .map(StationWhitelist::getUserId)
+                .collect(Collectors.toSet());
+
+        List<User> users = userService.listByIds(dto.getUserIds());
+        Map<Long, User> userMap = users.stream()
+                .collect(Collectors.toMap(User::getId, u -> u));
+
+        Long operatorId = StpUtil.getLoginIdAsLong();
+        String operatorName = StpUtil.getSession().getString("mobilePhone");
+
+        List<StationWhitelist> toSave = new ArrayList<>();
+        for (Long userId : dto.getUserIds()) {
+            if (existingUserIds.contains(userId)) {
+                continue;
+            }
+            User user = userMap.get(userId);
+            if (user == null) {
+                continue;
+            }
+            StationWhitelist whitelist = new StationWhitelist();
+            whitelist.setId(idGenerator.nextId());
+            whitelist.setStationId(dto.getStationId());
+            whitelist.setStationName(station.getStationName());
+            whitelist.setUserId(userId);
+            whitelist.setUserPhone(user.getMobilePhone());
+            whitelist.setUserNickname(user.getNickname());
+            whitelist.setStatus(StationWhitelist.STATUS_启用);
+            whitelist.setRemark(dto.getRemark());
+            whitelist.setCreateBy(operatorId);
+            toSave.add(whitelist);
+        }
+
+        if (!toSave.isEmpty()) {
+            saveBatch(toSave);
+            refreshCache(dto.getStationId());
+            stationWhitelistLogService.logBatchAdd(dto.getStationId(), station.getStationName(),
+                    toSave.size(), operatorId, operatorName);
+        }
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void removeWhitelist(Long id) {
+        StationWhitelist whitelist = getById(id);
+        CommUtil.assertsNonNull(whitelist, "白名单记录不存在");
+
+        removeById(id);
+        refreshCache(whitelist.getStationId());
+
+        stationWhitelistLogService.logRemove(whitelist.getStationId(), whitelist.getStationName(),
+                whitelist.getUserId(), whitelist.getUserPhone(),
+                StpUtil.getLoginIdAsLong(), StpUtil.getSession().getString("mobilePhone"));
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void batchRemoveWhitelist(List<Long> ids) {
+        if (ids == null || ids.isEmpty()) {
+            return;
+        }
+        List<StationWhitelist> list = listByIds(ids);
+        if (list.isEmpty()) {
+            return;
+        }
+
+        removeByIds(ids);
+
+        Map<String, List<StationWhitelist>> groupByStation = list.stream()
+                .collect(Collectors.groupingBy(StationWhitelist::getStationId));
+        for (String stationId : groupByStation.keySet()) {
+            refreshCache(stationId);
+        }
+
+        stationWhitelistLogService.logBatchRemove(list.get(0).getStationId(), list.get(0).getStationName(),
+                list.size(), StpUtil.getLoginIdAsLong(), StpUtil.getSession().getString("mobilePhone"));
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void updateStatus(Long id, Integer status) {
+        StationWhitelist whitelist = getById(id);
+        CommUtil.assertsNonNull(whitelist, "白名单记录不存在");
+
+        lambdaUpdate()
+                .set(StationWhitelist::getStatus, status)
+                .eq(StationWhitelist::getId, id)
+                .update();
+
+        refreshCache(whitelist.getStationId());
+
+        stationWhitelistLogService.logStatusChange(whitelist.getStationId(), whitelist.getStationName(),
+                whitelist.getUserId(), whitelist.getUserPhone(), status,
+                StpUtil.getLoginIdAsLong(), StpUtil.getSession().getString("mobilePhone"));
+    }
+
+    @Override
+    public boolean isInWhitelist(String stationId, Long userId) {
+        String key = RedisKeys.STATION_WHITELIST_USERS + stationId;
+        if (Boolean.TRUE.equals(redisTemplate.hasKey(key))) {
+            return Boolean.TRUE.equals(redisTemplate.opsForSet().isMember(key, String.valueOf(userId)));
+        }
+        
+        // 缓存不存在,重建缓存
+        refreshCache(stationId);
+        
+        // 再次检查缓存
+        if (Boolean.TRUE.equals(redisTemplate.hasKey(key))) {
+            return Boolean.TRUE.equals(redisTemplate.opsForSet().isMember(key, String.valueOf(userId)));
+        }
+        
+        return false;
+    }
+
+    @Override
+    public List<Long> getWhitelistUserIds(String stationId) {
+        String key = RedisKeys.STATION_WHITELIST_USERS + stationId;
+        Set<String> members = redisTemplate.opsForSet().members(key);
+        if (members != null && !members.isEmpty()) {
+            return members.stream()
+                    .map(Long::valueOf)
+                    .collect(Collectors.toList());
+        }
+        List<StationWhitelist> list = lambdaQuery()
+                .eq(StationWhitelist::getStationId, stationId)
+                .eq(StationWhitelist::getStatus, StationWhitelist.STATUS_启用)
+                .list();
+        return list.stream()
+                .map(StationWhitelist::getUserId)
+                .collect(Collectors.toList());
+    }
+
+    @Override
+    public void refreshCache(String stationId) {
+        String key = RedisKeys.STATION_WHITELIST_USERS + stationId;
+        redisTemplate.delete(key);
+
+        List<StationWhitelist> list = lambdaQuery()
+                .eq(StationWhitelist::getStationId, stationId)
+                .eq(StationWhitelist::getStatus, StationWhitelist.STATUS_启用)
+                .list();
+
+        if (!list.isEmpty()) {
+            String[] userIds = list.stream()
+                    .map(w -> String.valueOf(w.getUserId()))
+                    .toArray(String[]::new);
+            redisTemplate.opsForSet().add(key, userIds);
+            redisTemplate.expire(key, CACHE_EXPIRE_HOURS, TimeUnit.HOURS);
+        }
+    }
+
+    @Override
+    public void refreshAllCache() {
+        List<StationWhitelist> allList = lambdaQuery()
+                .eq(StationWhitelist::getStatus, StationWhitelist.STATUS_启用)
+                .list();
+
+        Map<String, List<StationWhitelist>> groupByStation = allList.stream()
+                .collect(Collectors.groupingBy(StationWhitelist::getStationId));
+
+        for (Map.Entry<String, List<StationWhitelist>> entry : groupByStation.entrySet()) {
+            String stationId = entry.getKey();
+            List<StationWhitelist> list = entry.getValue();
+            String key = RedisKeys.STATION_WHITELIST_USERS + stationId;
+            redisTemplate.delete(key);
+            String[] userIds = list.stream()
+                    .map(w -> String.valueOf(w.getUserId()))
+                    .toArray(String[]::new);
+            redisTemplate.opsForSet().add(key, userIds);
+            redisTemplate.expire(key, CACHE_EXPIRE_HOURS, TimeUnit.HOURS);
+        }
+    }
+}

+ 2 - 0
service/src/main/java/com/kym/service/miniapp/UserService.java

@@ -28,4 +28,6 @@ public interface UserService extends MPJBaseService<User> {
     R<?> wxLogin(WxLoginParams params);
 
     PageBean<CustomUserVo> listCustomUser(CommonQueryParam params);
+
+    PageBean<User> listSimpleUser(CommonQueryParam params);
 }

+ 20 - 2
service/src/main/java/com/kym/service/miniapp/impl/ChargeServiceImpl.java

@@ -20,6 +20,8 @@ import com.kym.entity.platform.response.PlatformBusinessPolicy;
 import com.kym.service.admin.ConnectorInfoService;
 import com.kym.service.admin.EquipmentInfoService;
 import com.kym.service.admin.EquipmentRelationService;
+import com.kym.service.admin.StationWhitelistConfigService;
+import com.kym.service.admin.StationWhitelistService;
 import com.kym.service.cache.KymCache;
 import com.kym.service.jobs.DelayService;
 import com.kym.service.miniapp.*;
@@ -61,6 +63,8 @@ public class ChargeServiceImpl implements ChargeService {
     private final PlatformApiService platformApiService;
     private final DelayService<DelayChargeOrder> startDelayService;
     private final DelayService<DelayChargeOrder> stopDelayService;
+    private final StationWhitelistService stationWhitelistService;
+    private final StationWhitelistConfigService stationWhitelistConfigService;
 
     public ChargeServiceImpl(EquipmentRelationService equipmentRelationService,
                              EquipmentInfoService equipmentInfoService,
@@ -74,6 +78,8 @@ public class ChargeServiceImpl implements ChargeService {
                              PlatformApiService platformApiService,
                              @Qualifier("StartChargeDelayJob") @Lazy DelayService<DelayChargeOrder> startDelayService,
                              @Qualifier("StopChargeDelayJob") @Lazy DelayService<DelayChargeOrder> stopDelayService,
+                             StationWhitelistService stationWhitelistService,
+                             StationWhitelistConfigService stationWhitelistConfigService,
                              StringRedisTemplate redisTemplate) {
         this.equipmentRelationService = equipmentRelationService;
         this.equipmentInfoService = equipmentInfoService;
@@ -87,6 +93,8 @@ public class ChargeServiceImpl implements ChargeService {
         this.platformApiService = platformApiService;
         this.startDelayService = startDelayService;
         this.stopDelayService = stopDelayService;
+        this.stationWhitelistService = stationWhitelistService;
+        this.stationWhitelistConfigService = stationWhitelistConfigService;
         this.redisTemplate = redisTemplate;
     }
 
@@ -202,7 +210,7 @@ public class ChargeServiceImpl implements ChargeService {
         connectorId = connectorId2StationId.get("connectorId");
         var stationId = connectorId2StationId.get("stationId");
         LOGGER.info("用户:{},站点:{},设备:{}请求充电", userId, stationId, connectorId);
-        var account = checkCharge(userId, connectorId, isBooking, startTime);
+        var account = checkCharge(userId, connectorId, stationId, isBooking, startTime);
 
         // 是否有之前预约充电创建的订单记录,有则直接用,没有则创建
         ChargeOrder order = chargeOrderService.lambdaQuery()
@@ -312,11 +320,12 @@ public class ChargeServiceImpl implements ChargeService {
      *
      * @param userId
      * @param connectorId
+     * @param stationId
      * @param isBooking
      * @param startTime
      * @return
      */
-    private Account checkCharge(Long userId, String connectorId, Boolean isBooking, LocalDateTime startTime) {
+    private Account checkCharge(Long userId, String connectorId, String stationId, Boolean isBooking, LocalDateTime startTime) {
         if (CommUtil.isEmptyOrNull(connectorId)) {
             throw new BusinessException("请输入正确的设备编号");
         }
@@ -366,6 +375,15 @@ public class ChargeServiceImpl implements ChargeService {
             throw new BusinessException(ResponseEnum.INSUFFICIENT_USER_BALANCE);
         }
 
+        // 白名单验证
+        if (stationId != null && stationWhitelistConfigService.isWhitelistEnabled(stationId)) {
+            if (!stationWhitelistService.isInWhitelist(stationId, userId)) {
+                LOGGER.warn("用户:{}不在站点:{}的白名单中,拒绝充电", userId, stationId);
+                throw new BusinessException(ResponseEnum.USER_NOT_IN_WHITELIST);
+            }
+            LOGGER.info("用户:{}在站点:{}的白名单中,验证通过", userId, stationId);
+        }
+
         return account;
     }
 

+ 11 - 0
service/src/main/java/com/kym/service/miniapp/impl/UserServiceImpl.java

@@ -298,5 +298,16 @@ public class UserServiceImpl extends MPJBaseServiceImpl<UserMapper, User> implem
         return page;
     }
 
+    @Override
+    public PageBean<User> listSimpleUser(CommonQueryParam params) {
+        PageHelper.startPage(params.getPageNum(), params.getPageSize());
+        var list = lambdaQuery()
+                .select(User::getId, User::getMobilePhone, User::getNickname, User::getUsername)
+                .like(!CommUtil.isEmptyOrNull(params.getMobilePhone()), User::getMobilePhone, params.getMobilePhone())
+                .orderByDesc(User::getCreateTime)
+                .list();
+        return new PageBean<>(list);
+    }
+
 
 }