Pārlūkot izejas kodu

重构分组-站点关系为多对多模型

- 分组(t_recharge_config_group)不再绑定 station_id,改为 is_default 标记默认分组
- 新增关联表 t_recharge_config_station_group(group_id + station_id,uk_station_id)
- 分组可自由创建,后续通过关联表灵活分配站点,一个分组可关联多个站点
- 创建站点时不再自动创建分组,默认使用平台默认分组
- 新增 assignStationDialog.vue 支持为分组分配/移除站点
- 管理后台每行分组显示已关联站点标签,支持动态增删

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
skyline 3 dienas atpakaļ
vecāks
revīzija
d5180233f5

+ 88 - 0
admin-web/src/views/admin/platform/rechargeConfig/assignStationDialog.vue

@@ -0,0 +1,88 @@
+<template>
+  <div class="system-dialog-container">
+    <el-dialog
+        title="关联站点到分组"
+        v-model="state.dialog.isShowDialog"
+        width="500px"
+        draggable
+        destroy-on-close
+        :close-on-click-modal="false"
+        align-center>
+      <el-form :model="state.ruleForm" label-position="top" ref="formRef" size="default">
+        <el-form-item label="选择站点">
+          <el-select-v2
+              v-model="state.ruleForm.stationId"
+              :options="state.stationOptions"
+              placeholder="选择要关联的站点"
+              filterable
+              style="width: 100%"
+          />
+        </el-form-item>
+      </el-form>
+
+      <template #footer>
+        <el-button @click="onCancel" size="default">取 消</el-button>
+        <el-button :loading="state.btnLoading" type="primary" @click="onSubmit" size="default">确 定</el-button>
+      </template>
+    </el-dialog>
+  </div>
+</template>
+
+<script setup lang="ts" name="AssignStationDialog">
+import { reactive, ref } from 'vue';
+import { Msg } from "/@/utils/message";
+import { $body } from "/@/utils/request";
+
+const emit = defineEmits(['refresh']);
+const formRef = ref();
+
+const initState = () => ({
+  ruleForm: { groupId: null as number | null, stationId: null as string | null },
+  btnLoading: false,
+  dialog: { isShowDialog: false },
+  stationOptions: [] as Array<{ value: string; label: string }>,
+})
+
+const state = reactive(initState());
+
+const open = (groupId: number) => {
+  state.ruleForm.groupId = groupId;
+  state.ruleForm.stationId = null;
+  state.dialog.isShowDialog = true;
+
+  $body('/washStation/list', { pageNum: 1, pageSize: 200 }).then((res: any) => {
+    state.stationOptions = (res.list || []).map((s: any) => ({
+      value: s.stationId,
+      label: s.stationName + ' (' + s.stationId + ')',
+    }));
+  });
+};
+
+const onClose = () => {
+  state.dialog.isShowDialog = false;
+  Object.assign(state, initState());
+};
+const onCancel = () => onClose();
+
+const onSubmit = () => {
+  if (!state.ruleForm.stationId) {
+    Msg.message('请选择站点', 'warning');
+    return;
+  }
+  state.btnLoading = true;
+  $body('/rechargeConfig/group/assignStation', {
+    groupId: state.ruleForm.groupId,
+    stationId: state.ruleForm.stationId,
+  }).then(() => {
+    state.btnLoading = false;
+    Msg.message('操作成功');
+    onClose();
+    emit('refresh');
+  }).catch(() => {
+    state.btnLoading = false;
+    Msg.message('操作失败', 'error');
+  });
+};
+
+defineExpose({ open });
+</script>

+ 10 - 60
admin-web/src/views/admin/platform/rechargeConfig/groupDialog.vue

@@ -1,44 +1,22 @@
-<style scoped lang="scss">
-
-</style>
 <template>
   <div class="system-dialog-container">
     <el-dialog
         :title="state.dialog.title"
         v-model="state.dialog.isShowDialog"
-        width="500px"
+        width="450px"
         draggable
         destroy-on-close
         :close-on-click-modal="false"
         align-center>
-      <el-form
-          :model="state.ruleForm"
-          label-position="top"
-          ref="formRef"
-          size="default">
-        <el-form-item label="关联站点" prop="stationId">
-          <el-select-v2
-              v-model="state.ruleForm.stationId"
-              :options="state.stationOptions"
-              placeholder="留空则为平台默认分组"
-              clearable
-              style="width: 100%"
-          />
-        </el-form-item>
-        <el-form-item v-if="false" label="分组名称">
-          <el-input
-              v-model="state.ruleForm.name"
-              placeholder="可选,如「VIP套餐」"
-              clearable>
-          </el-input>
+      <el-form :model="state.ruleForm" label-position="top" ref="formRef" size="default">
+        <el-form-item label="分组名称">
+          <el-input v-model="state.ruleForm.name" placeholder="如:VIP套餐(可选)" clearable />
         </el-form-item>
       </el-form>
 
       <template #footer>
-        <span 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>
-        </span>
+        <el-button @click="onCancel" size="default">取 消</el-button>
+        <el-button :loading="state.btnLoading" type="primary" @click="onSubmit" size="default">{{ state.dialog.submitTxt }}</el-button>
       </template>
     </el-dialog>
   </div>
@@ -54,19 +32,9 @@ const emit = defineEmits(['refresh']);
 const formRef = ref();
 
 const initState = () => ({
-  ruleForm: {
-    id: null as number | null,
-    stationId: null as string | null,
-    name: '' as string,
-  },
+  ruleForm: { id: null as number | null, name: '' },
   btnLoading: false,
-  dialog: {
-    isShowDialog: false,
-    type: '',
-    title: '',
-    submitTxt: '',
-  },
-  stationOptions: [] as Array<{ value: string; label: string }>,
+  dialog: { isShowDialog: false, type: '', title: '', submitTxt: '' },
 })
 
 const state = reactive(initState());
@@ -75,18 +43,8 @@ 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;
-
-  // 加载已有分组的站点列表(避免重复创建)
-  $body('/washStation/list', { pageNum: 1, pageSize: 200 }).then((res: any) => {
-    state.stationOptions = (res.list || []).map((s: any) => ({
-      value: s.stationId,
-      label: s.stationName + ' (' + s.stationId + ')',
-    }));
-  });
-
   if (action !== 'add' && row) {
     state.ruleForm.id = row.id;
-    state.ruleForm.stationId = row.stationId || null;
     state.ruleForm.name = row.name || '';
   }
 };
@@ -95,20 +53,12 @@ const onClose = () => {
   state.dialog.isShowDialog = false;
   Object.assign(state, initState());
 };
-
-const onCancel = () => {
-  onClose();
-};
+const onCancel = () => onClose();
 
 const onSubmit = () => {
   state.btnLoading = true;
-  const payload = {
-    id: state.ruleForm.id,
-    stationId: state.ruleForm.stationId || null,
-    name: state.ruleForm.name || null,
-  };
   const url = !!state.ruleForm.id ? "/rechargeConfig/group/modify" : "/rechargeConfig/group/add";
-  $body(url, payload).then(() => {
+  $body(url, { id: state.ruleForm.id, name: state.ruleForm.name || null, stationIds: [] }).then(() => {
     state.btnLoading = false;
     Msg.message('操作成功');
     onClose();

+ 46 - 16
admin-web/src/views/admin/platform/rechargeConfig/index.vue

@@ -36,6 +36,13 @@
     padding: 0;
   }
 }
+
+.station-tags {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 4px;
+  margin-top: 4px;
+}
 </style>
 <template>
   <div class="system-container layout-padding">
@@ -43,7 +50,7 @@
       <div class="page-search mb10" style="display: flex; align-items: center; gap: 10px;">
         <el-button v-auth="'rechargeConfig.add'" size="default" plain type="success" @click="handleGroupAdd">
           <SvgIcon name="ele-FolderAdd"/>
-          新增默认分组
+          创建分组
         </el-button>
       </div>
 
@@ -54,24 +61,31 @@
           <div class="group-header">
             <div class="group-info">
               <span class="group-title">
-                <el-tag v-if="g.group.stationId" type="success" effect="light">{{ g.group.stationId }}</el-tag>
-                <el-tag v-else type="primary" effect="light">平台默认配置</el-tag>
+                <el-tag v-if="g.group.isDefault" type="danger" effect="light">默认</el-tag>
+                {{ g.group.name || (g.group.isDefault ? '平台默认配置' : '未命名分组') }}
               </span>
-              <span v-if="g.group.name" style="color: var(--el-text-color-secondary); font-size: 13px;">{{ g.group.name }}</span>
+              <div class="station-tags">
+                <el-tag
+                    v-for="sid in g.stationIds"
+                    :key="sid"
+                    type="success"
+                    size="small"
+                    closable
+                    @close="handleRemoveStation(g.group.id, sid)">
+                  {{ sid }}
+                </el-tag>
+                <el-button size="small" text type="primary" @click="handleAssignStation(g.group.id)">+ 关联站点</el-button>
+              </div>
             </div>
             <div class="group-actions">
               <el-button v-auth="'rechargeConfig.add'" size="small" type="primary" plain @click="handleItemAdd(g.group.id)">+ 配置项</el-button>
-              <el-button v-if="g.group.stationId" v-auth="'rechargeConfig.remove'" size="small" type="danger" plain @click="handleGroupRemove(g.group)">删除分组</el-button>
+              <el-button v-auth="'rechargeConfig.remove'" size="small" type="danger" plain @click="handleGroupRemove(g.group)">删除</el-button>
             </div>
           </div>
           <div class="item-table-wrap">
-            <el-table
-                border
-                :data="g.items"
-                size="small"
-                v-loading="g.itemsLoading">
+            <el-table border :data="g.items" size="small" v-loading="g.itemsLoading">
               <template #empty>
-                <el-empty :image-size="40" description="暂无配置项,请新增" />
+                <el-empty :image-size="40" description="暂无配置项" />
               </template>
               <el-table-column label="充值金额" prop="rechargeAmount" width="140">
                 <template #default="{ row }">
@@ -101,23 +115,27 @@
     </el-card>
   </div>
   <GroupDialog ref="groupDialogRef" @refresh="loadData" />
+  <AssignStationDialog ref="assignStationDialogRef" @refresh="loadData" />
   <ItemDialog ref="itemDialogRef" @refresh="loadData" />
 </template>
 
 <script setup lang="ts" name="AdminRechargeConfig">
 import { defineAsyncComponent, reactive, onMounted, ref } from 'vue';
-import { $get } from "/@/utils/request";
+import { $body, $get } from "/@/utils/request";
 import u from '/@/utils/u'
 import { Msg } from "/@/utils/message";
 
 const GroupDialog = defineAsyncComponent(() => import("./groupDialog.vue"));
+const AssignStationDialog = defineAsyncComponent(() => import("./assignStationDialog.vue"));
 const ItemDialog = defineAsyncComponent(() => import("./itemDialog.vue"));
 
 const groupDialogRef = ref();
+const assignStationDialogRef = ref();
 const itemDialogRef = ref();
 
 interface GroupRow {
   group: any;
+  stationIds: string[];
   items: any[];
   itemsLoading: boolean;
 }
@@ -134,7 +152,12 @@ onMounted(() => {
 const loadData = () => {
   state.loading = true;
   $get('/rechargeConfig/group/list').then((groups: any) => {
-    state.groups = (groups || []).map((g: any) => ({ group: g, items: [], itemsLoading: false }));
+    state.groups = (groups || []).map((g: any) => ({
+      group: g.group,
+      stationIds: g.stationIds || [],
+      items: [],
+      itemsLoading: false,
+    }));
     state.groups.forEach((g) => loadItems(g));
     state.loading = false;
   }).catch(() => {
@@ -152,8 +175,6 @@ const loadItems = (g: GroupRow) => {
   });
 };
 
-// ==================== 分组操作 ====================
-
 const handleGroupAdd = () => {
   groupDialogRef.value.open('add', null);
 };
@@ -169,7 +190,16 @@ const handleGroupRemove = (group: any) => {
   });
 };
 
-// ==================== 配置项操作 ====================
+const handleAssignStation = (groupId: number) => {
+  assignStationDialogRef.value.open(groupId);
+};
+
+const handleRemoveStation = (groupId: number, stationId: string) => {
+  $body('/rechargeConfig/group/removeStation', { groupId, stationId }).then(() => {
+    Msg.message("已移除", 'success');
+    loadData();
+  });
+};
 
 const handleItemAdd = (groupId: number) => {
   itemDialogRef.value.open('add', { groupId });

+ 62 - 32
car-wash-admin/src/main/java/com/kym/admin/controller/RechargeConfigController.java

@@ -7,10 +7,14 @@ import com.kym.entity.RechargeConfig;
 import com.kym.entity.RechargeConfigGroup;
 import com.kym.service.RechargeConfigGroupService;
 import com.kym.service.RechargeConfigService;
+import com.kym.service.RechargeConfigStationGroupService;
 import org.springframework.web.bind.annotation.*;
 
+import java.util.List;
+import java.util.Map;
+
 /**
- * 充值配置管理(分组 + 配置项两级管理)
+ * 充值配置管理(分组 + 配置项两级管理,分组通过关联表绑定站点
  *
  * @author skyline
  */
@@ -20,42 +24,50 @@ public class RechargeConfigController {
 
     private final RechargeConfigService itemService;
     private final RechargeConfigGroupService groupService;
+    private final RechargeConfigStationGroupService stationGroupService;
 
-    public RechargeConfigController(RechargeConfigService itemService, RechargeConfigGroupService groupService) {
+    public RechargeConfigController(RechargeConfigService itemService,
+                                     RechargeConfigGroupService groupService,
+                                     RechargeConfigStationGroupService stationGroupService) {
         this.itemService = itemService;
         this.groupService = groupService;
+        this.stationGroupService = stationGroupService;
     }
 
     // ==================== 分组管理 ====================
 
     /**
-     * 所有分组列表
+     * 所有分组列表(含关联站点)
      */
     @SaCheckPermission("rechargeConfig.list")
     @GetMapping("/group/list")
     R<?> groupList() {
-        return R.success(groupService.list());
+        var groups = groupService.list();
+        var result = groups.stream().map(g -> {
+            var stationIds = stationGroupService.listStationIdsByGroupId(g.getId());
+            return Map.of("group", g, "stationIds", stationIds);
+        }).toList();
+        return R.success(result);
     }
 
     /**
-     * 新增分组
+     * 创建分组(可选关联站点列表)
      */
     @SaCheckPermission("rechargeConfig.add")
-    @SysLog("新增充值配置分组")
+    @SysLog("创建充值配置分组")
     @PostMapping("/group/add")
-    R<?> groupAdd(@RequestBody RechargeConfigGroup group) {
-        if (group.getStationId() != null) {
-            var existing = groupService.getByStationId(group.getStationId());
-            if (existing != null) {
-                return R.fail("该站点已有配置分组");
-            }
-        }
-        groupService.save(group);
+    R<?> groupAdd(@RequestBody Map<String, Object> body) {
+        var group = new RechargeConfigGroup();
+        group.setName((String) body.getOrDefault("name", null));
+        group.setIsDefault(Boolean.TRUE.equals(body.get("isDefault")));
+        @SuppressWarnings("unchecked")
+        var stationIds = (List<String>) body.get("stationIds");
+        groupService.createGroup(group, stationIds);
         return R.success();
     }
 
     /**
-     * 修改分组信息
+     * 修改分组名称
      */
     @SaCheckPermission("rechargeConfig.modify")
     @SysLog("修改充值配置分组")
@@ -76,19 +88,49 @@ public class RechargeConfigController {
         return R.success();
     }
 
-    // ==================== 配置项管理 ====================
+    // ==================== 分组-站点关联管理 ====================
+
+    /**
+     * 查询某分组关联的站点列表
+     */
+    @GetMapping("/group/stationIds")
+    R<?> groupStationIds(@RequestParam Long groupId) {
+        return R.success(stationGroupService.listStationIdsByGroupId(groupId));
+    }
+
+    /**
+     * 为分组分配一个站点
+     */
+    @SaCheckPermission("rechargeConfig.modify")
+    @SysLog("分配站点到充值配置分组")
+    @PostMapping("/group/assignStation")
+    R<?> assignStation(@RequestBody Map<String, Object> body) {
+        String stationId = (String) body.get("stationId");
+        Long groupId = Long.valueOf(body.get("groupId").toString());
+        stationGroupService.assignStationToGroup(stationId, groupId);
+        return R.success();
+    }
 
     /**
-     * 查询某分组下的配置项列表
+     * 移除站点与分组的关联
      */
+    @SaCheckPermission("rechargeConfig.modify")
+    @SysLog("移除站点充值配置分组关联")
+    @PostMapping("/group/removeStation")
+    R<?> removeStation(@RequestBody Map<String, Object> body) {
+        String stationId = (String) body.get("stationId");
+        Long groupId = Long.valueOf(body.get("groupId").toString());
+        stationGroupService.removeStationFromGroup(stationId, groupId);
+        return R.success();
+    }
+
+    // ==================== 配置项管理 ====================
+
     @GetMapping("/item/list")
     R<?> itemList(@RequestParam Long groupId) {
         return R.success(itemService.listByGroupId(groupId));
     }
 
-    /**
-     * 新增配置项
-     */
     @SaCheckPermission("rechargeConfig.add")
     @SysLog("新增充值配置项")
     @PostMapping("/item/add")
@@ -97,9 +139,6 @@ public class RechargeConfigController {
         return R.success();
     }
 
-    /**
-     * 修改配置项
-     */
     @SaCheckPermission("rechargeConfig.modify")
     @SysLog("修改充值配置项")
     @PostMapping("/item/modify")
@@ -108,9 +147,6 @@ public class RechargeConfigController {
         return R.success();
     }
 
-    /**
-     * 删除配置项
-     */
     @SaCheckPermission("rechargeConfig.remove")
     @SysLog("删除充值配置项")
     @GetMapping("/item/remove/{id}")
@@ -121,18 +157,12 @@ public class RechargeConfigController {
 
     // ==================== 兼容旧接口 ====================
 
-    /**
-     * 按站点筛选配置项(返回扁平列表,兼容小程序查询)
-     */
     @SaCheckPermission("rechargeConfig.list")
     @GetMapping("/list")
     R<?> list(@RequestParam(required = false) String stationId) {
         return R.success(itemService.listByStationId(stationId));
     }
 
-    /**
-     * 全部配置项
-     */
     @SaCheckPermission("rechargeConfig.list")
     @GetMapping("/listAll")
     R<?> listAll() {

+ 3 - 3
car-wash-entity/src/main/java/com/kym/entity/RechargeConfigGroup.java

@@ -5,7 +5,7 @@ import lombok.Getter;
 import lombok.Setter;
 
 /**
- * 充值配置分组 — 每个站点(或平台默认)拥有一个配置组,组内包含多条充值金额配置项
+ * 充值配置分组 — 组内包含多条充值金额配置项,可关联多个站点
  *
  * @author skyline
  * @since 2026-05-26
@@ -23,7 +23,7 @@ public class RechargeConfigGroup extends BaseEntity {
     private String name;
 
     /**
-     * 站点ID,NULL表示平台默认配置组
+     * 是否默认分组(未分配站点的自动使用)
      */
-    private String stationId;
+    private Boolean isDefault;
 }

+ 23 - 0
car-wash-entity/src/main/java/com/kym/entity/RechargeConfigStationGroup.java

@@ -0,0 +1,23 @@
+package com.kym.entity;
+
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Getter;
+import lombok.Setter;
+
+/**
+ * 充值配置分组-站点关联
+ *
+ * @author skyline
+ * @since 2026-05-26
+ */
+@Getter
+@Setter
+@TableName("t_recharge_config_station_group")
+public class RechargeConfigStationGroup extends BaseEntity {
+
+    private static final long serialVersionUID = 1L;
+
+    private Long groupId;
+
+    private String stationId;
+}

+ 55 - 23
car-wash-entity/src/main/resources/sql/v2_settlement.sql

@@ -149,15 +149,26 @@ WHERE NOT EXISTS (SELECT 1 FROM `t_permission` WHERE `value` = 'rechargeConfig.r
 CREATE TABLE IF NOT EXISTS `t_recharge_config_group` (
     `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
     `name` VARCHAR(64) DEFAULT NULL COMMENT '分组名称',
-    `station_id` VARCHAR(64) DEFAULT NULL COMMENT '站点ID,NULL表示平台默认配置',
+    `is_default` TINYINT(1) DEFAULT 0 COMMENT '是否默认分组(未分配站点的自动使用)',
     `company_id` BIGINT DEFAULT NULL COMMENT '公司(租户)ID',
     `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
     `update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
-    PRIMARY KEY (`id`),
-    UNIQUE KEY `uk_station_id` (`station_id`)
+    PRIMARY KEY (`id`)
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='充值配置分组表';
 
--- 9b. 确保 t_recharge_config 有 station_id 列(兼容 V2.1 未执行的情况)
+-- 9b. 创建分组-站点关联表
+CREATE TABLE IF NOT EXISTS `t_recharge_config_station_group` (
+    `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
+    `group_id` BIGINT NOT NULL COMMENT '分组ID',
+    `station_id` VARCHAR(64) NOT NULL COMMENT '站点ID',
+    `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+    `update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
+    PRIMARY KEY (`id`),
+    UNIQUE KEY `uk_station_id` (`station_id`),
+    KEY `idx_group_id` (`group_id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='充值配置分组-站点关联表';
+
+-- 9c. 确保 t_recharge_config 有 station_id 列(兼容 V2.1 未执行的情况)
 DROP PROCEDURE IF EXISTS add_station_id_if_missing;
 DELIMITER //
 CREATE PROCEDURE add_station_id_if_missing()
@@ -177,43 +188,64 @@ DELIMITER ;
 CALL add_station_id_if_missing();
 DROP PROCEDURE IF EXISTS add_station_id_if_missing;
 
--- 9c. 为已有的 station_id(含NULL默认值)创建分组
-INSERT INTO `t_recharge_config_group` (`station_id`, `name`, `create_time`, `update_time`)
-SELECT DISTINCT `station_id`, NULL, NOW(), NOW()
+-- 9d. 为已有的 station_id 创建分组(每个唯一 station_id 一个分组)
+INSERT INTO `t_recharge_config_group` (`name`, `is_default`, `create_time`, `update_time`)
+SELECT DISTINCT NULL, IF(`station_id` IS NULL, 1, 0), NOW(), NOW()
 FROM `t_recharge_config`
 WHERE NOT EXISTS (
-    SELECT 1 FROM `t_recharge_config_group` g
-    WHERE (g.`station_id` <=> `t_recharge_config`.`station_id`)
+    SELECT 1 FROM `t_recharge_config_group`
+);
+
+-- 9e. 确保平台默认分组存在并标记为默认
+INSERT INTO `t_recharge_config_group` (`name`, `is_default`, `create_time`, `update_time`)
+SELECT NULL, 1, NOW(), NOW()
+WHERE NOT EXISTS (
+    SELECT 1 FROM `t_recharge_config_group` WHERE `is_default` = 1
 );
 
--- 9d. 确保平台默认分组存在(station_id IS NULL)
-INSERT INTO `t_recharge_config_group` (`station_id`, `name`, `create_time`, `update_time`)
-SELECT NULL, NULL, NOW(), NOW()
+-- 9f. 将旧 station_id 数据迁入关联表(非NULL的station_id)
+INSERT INTO `t_recharge_config_station_group` (`group_id`, `station_id`, `create_time`, `update_time`)
+SELECT g.id, c.`station_id`, NOW(), NOW()
+FROM (SELECT DISTINCT `station_id` FROM `t_recharge_config` WHERE `station_id` IS NOT NULL) c
+INNER JOIN `t_recharge_config_group` g ON IFNULL(g.name, '') = '' AND g.`is_default` = 0
 WHERE NOT EXISTS (
-    SELECT 1 FROM `t_recharge_config_group` WHERE `station_id` IS NULL
+    SELECT 1 FROM `t_recharge_config_station_group` sg WHERE sg.`station_id` = c.`station_id`
 );
 
--- 9e. t_recharge_config 新增 group_id 列
+-- 如果上面的 INSERT 未分配成功(group 匹配不上),则为每个非NULL station_id 各自创建分组并关联
+INSERT INTO `t_recharge_config_group` (`name`, `is_default`, `create_time`, `update_time`)
+SELECT DISTINCT c.`station_id`, 0, NOW(), NOW()
+FROM `t_recharge_config` c
+WHERE c.`station_id` IS NOT NULL
+  AND NOT EXISTS (SELECT 1 FROM `t_recharge_config_station_group` sg WHERE sg.`station_id` = c.`station_id`)
+  AND NOT EXISTS (SELECT 1 FROM `t_recharge_config_group` g WHERE g.`name` = c.`station_id`);
+
+INSERT INTO `t_recharge_config_station_group` (`group_id`, `station_id`, `create_time`, `update_time`)
+SELECT g.id, g.`name`, NOW(), NOW()
+FROM `t_recharge_config_group` g
+WHERE g.`is_default` = 0
+  AND NOT EXISTS (SELECT 1 FROM `t_recharge_config_station_group` sg WHERE sg.`group_id` = g.id);
+
+-- 9g. t_recharge_config 新增 group_id 列
 ALTER TABLE `t_recharge_config`
     ADD COLUMN `group_id` BIGINT DEFAULT NULL COMMENT '所属分组ID'
     AFTER `id`;
 
--- 9f. 回填 group_id
+-- 9h. 回填 group_id:通过旧 station_id 匹配到关联表的 group_id
 UPDATE `t_recharge_config` c
-    INNER JOIN `t_recharge_config_group` g
-        ON (c.`station_id` <=> g.`station_id`)
-SET c.`group_id` = g.`id`;
+    INNER JOIN `t_recharge_config_station_group` sg ON c.`station_id` = sg.`station_id`
+SET c.`group_id` = sg.`group_id`;
 
--- 9g. 未关联到任何分组的行归入默认分组
+-- 9i. station_id 为 NULL 的行归入默认分组
 UPDATE `t_recharge_config`
-SET `group_id` = (SELECT id FROM `t_recharge_config_group` WHERE `station_id` IS NULL LIMIT 1)
+SET `group_id` = (SELECT id FROM `t_recharge_config_group` WHERE `is_default` = 1 LIMIT 1)
 WHERE `group_id` IS NULL;
 
--- 9h. group_id 设为 NOT NULL
+-- 9j. group_id 设为 NOT NULL
 ALTER TABLE `t_recharge_config`
     MODIFY COLUMN `group_id` BIGINT NOT NULL COMMENT '所属分组ID';
 
--- 9i. 删除旧 station_id 列
+-- 9k. 删除旧 station_id 列
 DROP PROCEDURE IF EXISTS drop_station_id;
 DELIMITER //
 CREATE PROCEDURE drop_station_id()
@@ -231,7 +263,7 @@ DELIMITER ;
 CALL drop_station_id();
 DROP PROCEDURE IF EXISTS drop_station_id;
 
--- 9j. 组内金额唯一约束
+-- 9l. 组内金额唯一约束
 ALTER TABLE `t_recharge_config`
     ADD INDEX `idx_group_id` (`group_id`),
     ADD UNIQUE INDEX `uk_group_recharge_amount` (`group_id`, `recharge_amount`);

+ 1 - 1
car-wash-mapper/src/main/java/com/kym/mapper/RechargeConfigGroupMapper.java

@@ -1,7 +1,7 @@
 package com.kym.mapper;
 
 import com.kym.entity.RechargeConfigGroup;
-import com.kym.service.mybatisplus.MyBaseMapper;
+import com.kym.mapper.mybatisplus.MyBaseMapper;
 
 /**
  * 充值配置分组 Mapper

+ 13 - 0
car-wash-mapper/src/main/java/com/kym/mapper/RechargeConfigStationGroupMapper.java

@@ -0,0 +1,13 @@
+package com.kym.mapper;
+
+import com.kym.entity.RechargeConfigStationGroup;
+import com.kym.service.mybatisplus.MyBaseMapper;
+
+/**
+ * 充值配置分组-站点关联 Mapper
+ *
+ * @author skyline
+ * @since 2026-05-26
+ */
+public interface RechargeConfigStationGroupMapper extends MyBaseMapper<RechargeConfigStationGroup> {
+}

+ 4 - 4
car-wash-service/src/main/java/com/kym/service/RechargeConfigGroupService.java

@@ -12,17 +12,17 @@ import com.kym.service.mybatisplus.MyBaseService;
 public interface RechargeConfigGroupService extends MyBaseService<RechargeConfigGroup> {
 
     /**
-     * 根据站点ID获取分组
+     * 根据站点ID获取其关联的分组(通过关联表查询),无关联时返回默认分组
      */
     RechargeConfigGroup getByStationId(String stationId);
 
     /**
-     * 获取平台默认分组(station_id IS NULL
+     * 获取默认分组(is_default = 1
      */
     RechargeConfigGroup getDefaultGroup();
 
     /**
-     * 为站点创建分组并复制默认分组的配置项
+     * 创建分组。如果站点列表非空,同时建立关联
      */
-    RechargeConfigGroup createGroupForStation(String stationId);
+    RechargeConfigGroup createGroup(RechargeConfigGroup group, java.util.List<String> stationIds);
 }

+ 35 - 0
car-wash-service/src/main/java/com/kym/service/RechargeConfigStationGroupService.java

@@ -0,0 +1,35 @@
+package com.kym.service;
+
+import com.kym.entity.RechargeConfigStationGroup;
+import com.kym.service.mybatisplus.MyBaseService;
+
+import java.util.List;
+
+/**
+ * 充值配置分组-站点关联服务
+ *
+ * @author skyline
+ * @since 2026-05-26
+ */
+public interface RechargeConfigStationGroupService extends MyBaseService<RechargeConfigStationGroup> {
+
+    /**
+     * 根据站点ID查询其关联的分组ID
+     */
+    Long getGroupIdByStationId(String stationId);
+
+    /**
+     * 查询某分组关联的所有站点ID
+     */
+    List<String> listStationIdsByGroupId(Long groupId);
+
+    /**
+     * 为站点分配分组
+     */
+    void assignStationToGroup(String stationId, Long groupId);
+
+    /**
+     * 移除站点与分组的关联
+     */
+    void removeStationFromGroup(String stationId, Long groupId);
+}

+ 21 - 6
car-wash-service/src/main/java/com/kym/service/impl/RechargeConfigGroupServiceImpl.java

@@ -5,6 +5,7 @@ import com.kym.entity.RechargeConfigGroup;
 import com.kym.mapper.RechargeConfigGroupMapper;
 import com.kym.service.RechargeConfigGroupService;
 import com.kym.service.RechargeConfigService;
+import com.kym.service.RechargeConfigStationGroupService;
 import com.kym.service.mybatisplus.MyBaseServiceImpl;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
@@ -23,28 +24,34 @@ public class RechargeConfigGroupServiceImpl
         implements RechargeConfigGroupService {
 
     private final RechargeConfigService itemService;
+    private final RechargeConfigStationGroupService stationGroupService;
 
-    public RechargeConfigGroupServiceImpl(RechargeConfigService itemService) {
+    public RechargeConfigGroupServiceImpl(RechargeConfigService itemService,
+                                           RechargeConfigStationGroupService stationGroupService) {
         this.itemService = itemService;
+        this.stationGroupService = stationGroupService;
     }
 
     @Override
     public RechargeConfigGroup getByStationId(String stationId) {
-        return lambdaQuery().eq(RechargeConfigGroup::getStationId, stationId).one();
+        Long groupId = stationGroupService.getGroupIdByStationId(stationId);
+        if (groupId != null) {
+            return getById(groupId);
+        }
+        return null;
     }
 
     @Override
     public RechargeConfigGroup getDefaultGroup() {
-        return lambdaQuery().isNull(RechargeConfigGroup::getStationId).one();
+        return lambdaQuery().eq(RechargeConfigGroup::getIsDefault, true).one();
     }
 
     @Override
     @Transactional(rollbackFor = Exception.class)
-    public RechargeConfigGroup createGroupForStation(String stationId) {
-        RechargeConfigGroup group = new RechargeConfigGroup();
-        group.setStationId(stationId);
+    public RechargeConfigGroup createGroup(RechargeConfigGroup group, List<String> stationIds) {
         save(group);
 
+        // 复制默认分组的配置项
         RechargeConfigGroup defaultGroup = getDefaultGroup();
         if (defaultGroup != null) {
             List<RechargeConfig> defaultItems = itemService.listByGroupId(defaultGroup.getId());
@@ -60,6 +67,14 @@ public class RechargeConfigGroupServiceImpl
                 itemService.saveBatch(newItems);
             }
         }
+
+        // 关联站点
+        if (stationIds != null) {
+            for (String stationId : stationIds) {
+                stationGroupService.assignStationToGroup(stationId, group.getId());
+            }
+        }
+
         return group;
     }
 }

+ 57 - 0
car-wash-service/src/main/java/com/kym/service/impl/RechargeConfigStationGroupServiceImpl.java

@@ -0,0 +1,57 @@
+package com.kym.service.impl;
+
+import com.kym.entity.RechargeConfigStationGroup;
+import com.kym.mapper.RechargeConfigStationGroupMapper;
+import com.kym.service.RechargeConfigStationGroupService;
+import com.kym.service.mybatisplus.MyBaseServiceImpl;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+
+/**
+ * 充值配置分组-站点关联服务实现
+ *
+ * @author skyline
+ * @since 2026-05-26
+ */
+@Service
+public class RechargeConfigStationGroupServiceImpl
+        extends MyBaseServiceImpl<RechargeConfigStationGroupMapper, RechargeConfigStationGroup>
+        implements RechargeConfigStationGroupService {
+
+    @Override
+    public Long getGroupIdByStationId(String stationId) {
+        var record = lambdaQuery().eq(RechargeConfigStationGroup::getStationId, stationId).one();
+        return record != null ? record.getGroupId() : null;
+    }
+
+    @Override
+    public List<String> listStationIdsByGroupId(Long groupId) {
+        return lambdaQuery()
+                .eq(RechargeConfigStationGroup::getGroupId, groupId)
+                .list()
+                .stream()
+                .map(RechargeConfigStationGroup::getStationId)
+                .toList();
+    }
+
+    @Override
+    public void assignStationToGroup(String stationId, Long groupId) {
+        // 一个站点只能属于一个分组,先删除旧关联
+        lambdaUpdate()
+                .eq(RechargeConfigStationGroup::getStationId, stationId)
+                .remove();
+        var record = new RechargeConfigStationGroup();
+        record.setStationId(stationId);
+        record.setGroupId(groupId);
+        save(record);
+    }
+
+    @Override
+    public void removeStationFromGroup(String stationId, Long groupId) {
+        lambdaUpdate()
+                .eq(RechargeConfigStationGroup::getStationId, stationId)
+                .eq(RechargeConfigStationGroup::getGroupId, groupId)
+                .remove();
+    }
+}

+ 2 - 2
car-wash-service/src/main/java/com/kym/service/impl/WashStationServiceImpl.java

@@ -54,8 +54,8 @@ public class WashStationServiceImpl extends MyBaseServiceImpl<WashStationMapper,
         CommUtil.asserts(count == 0, "站点已存在");
         save(station);
 
-        // 自动创建站点专属充值配置分组(复制平台默认分组的配置项)
-        rechargeConfigGroupService.createGroupForStation(station.getStationId());
+        // 新站点不自动创建分组,默认使用平台默认分组。管理员可在充值配置中为该站点分配专属分组。
+        // 如需自动创建:rechargeConfigGroupService.createGroup(new RechargeConfigGroup(), List.of(station.getStationId()));
     }
 
     @Override