10 Commits 0776688ed0 ... 253ce27eb8

Author SHA1 Message Date
  skyline 253ce27eb8 添加 listByStationId 调试日志,排查分页返回多组数据问题 3 days ago
  skyline b8091388bc 修复小程序充值页 stationId 为空导致多组数据问题 3 days ago
  skyline d5180233f5 重构分组-站点关系为多对多模型 3 days ago
  skyline 1487fa19d4 修复 V2.2 迁移脚本:兼容 station_id 列不存在的情况 3 days ago
  skyline 66fcfb6396 充值配置分组化重构:group + item 两级模型 3 days ago
  skyline 3ee0cfd423 修复权限初始化脚本:t_role_permission 表不存在,改为手动赋权提示 3 days ago
  skyline 6cd5e0ce08 初始化充值配置管理权限(rechargeConfig.*) 3 days ago
  skyline ccd1b12221 admin-web 新增充值配置管理页面(支持站点关联) 3 days ago
  skyline c3127fdde3 api域名修改 3 days ago
  skyline 97e349c9c3 跨店分账优化:赠款参与分账 + 充值配置站点化 3 days ago
25 changed files with 1290 additions and 31 deletions
  1. 15 0
      admin-web/src/router/route.ts
  2. 88 0
      admin-web/src/views/admin/platform/rechargeConfig/assignStationDialog.vue
  3. 73 0
      admin-web/src/views/admin/platform/rechargeConfig/groupDialog.vue
  4. 222 0
      admin-web/src/views/admin/platform/rechargeConfig/index.vue
  5. 142 0
      admin-web/src/views/admin/platform/rechargeConfig/itemDialog.vue
  6. 171 0
      car-wash-admin/src/main/java/com/kym/admin/controller/RechargeConfigController.java
  7. 1 1
      car-wash-admin/src/main/resources/application-dev.yml
  8. 6 4
      car-wash-entity/src/main/java/com/kym/entity/RechargeConfig.java
  9. 29 0
      car-wash-entity/src/main/java/com/kym/entity/RechargeConfigGroup.java
  10. 23 0
      car-wash-entity/src/main/java/com/kym/entity/RechargeConfigStationGroup.java
  11. 160 0
      car-wash-entity/src/main/resources/sql/v2_settlement.sql
  12. 13 0
      car-wash-mapper/src/main/java/com/kym/mapper/RechargeConfigGroupMapper.java
  13. 13 0
      car-wash-mapper/src/main/java/com/kym/mapper/RechargeConfigStationGroupMapper.java
  14. 4 3
      car-wash-miniapp/src/main/java/com/kym/miniapp/controller/CommonController.java
  15. 8 1
      car-wash-mp/src/pages-user/wallet/recharge.vue
  16. 28 0
      car-wash-service/src/main/java/com/kym/service/RechargeConfigGroupService.java
  17. 20 4
      car-wash-service/src/main/java/com/kym/service/RechargeConfigService.java
  18. 35 0
      car-wash-service/src/main/java/com/kym/service/RechargeConfigStationGroupService.java
  19. 9 8
      car-wash-service/src/main/java/com/kym/service/awoara/event/handle/OrderCloseEventHandler.java
  20. 81 0
      car-wash-service/src/main/java/com/kym/service/impl/RechargeConfigGroupServiceImpl.java
  21. 64 5
      car-wash-service/src/main/java/com/kym/service/impl/RechargeConfigServiceImpl.java
  22. 57 0
      car-wash-service/src/main/java/com/kym/service/impl/RechargeConfigStationGroupServiceImpl.java
  23. 6 1
      car-wash-service/src/main/java/com/kym/service/impl/WashStationServiceImpl.java
  24. 6 2
      car-wash-service/src/main/java/com/kym/service/wechat/impl/WxPayServiceImpl.java
  25. 16 2
      docs/结算方案-业务说明.md

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

@@ -346,6 +346,21 @@ export const adminRoutes: Array<RouteRecordRaw> = [
                             perm: "platformFeeRate.list",
                             icon: 'ele-Wallet',
                         },
+                    },
+                    {
+                        path: '/platform/rechargeConfig',
+                        name: 'adminPlatformRechargeConfig',
+                        component: () => import('/@/views/admin/platform/rechargeConfig/index.vue'),
+                        meta: {
+                            title: '充值配置',
+                            isLink: '',
+                            isHide: false,
+                            isKeepAlive: true,
+                            isAffix: false,
+                            isIframe: false,
+                            perm: "rechargeConfig.list",
+                            icon: 'ele-Money',
+                        },
                     }
                 ]
             },

+ 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>

+ 73 - 0
admin-web/src/views/admin/platform/rechargeConfig/groupDialog.vue

@@ -0,0 +1,73 @@
+<template>
+  <div class="system-dialog-container">
+    <el-dialog
+        :title="state.dialog.title"
+        v-model="state.dialog.isShowDialog"
+        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="分组名称">
+          <el-input v-model="state.ruleForm.name" placeholder="如:VIP套餐(可选)" clearable />
+        </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">{{ state.dialog.submitTxt }}</el-button>
+      </template>
+    </el-dialog>
+  </div>
+</template>
+
+<script setup lang="ts" name="GroupDialog">
+import { reactive, ref } from 'vue';
+import { Msg } from "/@/utils/message";
+import { $body } from "/@/utils/request";
+import u from '/@/utils/u'
+
+const emit = defineEmits(['refresh']);
+const formRef = ref();
+
+const initState = () => ({
+  ruleForm: { id: null as number | null, name: '' },
+  btnLoading: false,
+  dialog: { isShowDialog: false, type: '', title: '', submitTxt: '' },
+})
+
+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;
+  if (action !== 'add' && row) {
+    state.ruleForm.id = row.id;
+    state.ruleForm.name = row.name || '';
+  }
+};
+
+const onClose = () => {
+  state.dialog.isShowDialog = false;
+  Object.assign(state, initState());
+};
+const onCancel = () => onClose();
+
+const onSubmit = () => {
+  state.btnLoading = true;
+  const url = !!state.ruleForm.id ? "/rechargeConfig/group/modify" : "/rechargeConfig/group/add";
+  $body(url, { id: state.ruleForm.id, name: state.ruleForm.name || null, stationIds: [] }).then(() => {
+    state.btnLoading = false;
+    Msg.message('操作成功');
+    onClose();
+    emit('refresh');
+  }).catch(() => {
+    state.btnLoading = false;
+    Msg.message('操作失败', 'error');
+  });
+};
+
+defineExpose({ open });
+</script>

+ 222 - 0
admin-web/src/views/admin/platform/rechargeConfig/index.vue

@@ -0,0 +1,222 @@
+<style scoped lang="scss">
+.group-card {
+  margin-bottom: 16px;
+
+  .group-header {
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    padding: 12px 16px;
+    background-color: var(--el-fill-color-light);
+    border-radius: 6px 6px 0 0;
+    border: 1px solid var(--el-border-color-light);
+    border-bottom: none;
+
+    .group-info {
+      display: flex;
+      align-items: center;
+      gap: 12px;
+
+      .group-title {
+        font-weight: 600;
+        font-size: 15px;
+      }
+    }
+
+    .group-actions {
+      display: flex;
+      gap: 8px;
+    }
+  }
+
+  .item-table-wrap {
+    border: 1px solid var(--el-border-color-light);
+    border-top: none;
+    border-radius: 0 0 6px 6px;
+    padding: 0;
+  }
+}
+
+.station-tags {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 4px;
+  margin-top: 4px;
+}
+</style>
+<template>
+  <div class="system-container layout-padding">
+    <el-card shadow="hover" class="layout-padding-auto">
+      <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>
+
+      <div v-loading="state.loading">
+        <el-empty v-if="!state.loading && state.groups.length === 0" description="暂无充值配置分组" />
+
+        <div v-for="g in state.groups" :key="g.group.id" class="group-card">
+          <div class="group-header">
+            <div class="group-info">
+              <span class="group-title">
+                <el-tag v-if="g.group.isDefault" type="danger" effect="light">默认</el-tag>
+                {{ g.group.name || (g.group.isDefault ? '平台默认配置' : '未命名分组') }}
+              </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-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">
+              <template #empty>
+                <el-empty :image-size="40" description="暂无配置项" />
+              </template>
+              <el-table-column label="充值金额" prop="rechargeAmount" width="140">
+                <template #default="{ row }">
+                  {{ u.fmt.fmtMoney(row.rechargeAmount) }}
+                </template>
+              </el-table-column>
+              <el-table-column label="赠款金额" prop="grantsAmount" width="140">
+                <template #default="{ row }">
+                  {{ u.fmt.fmtMoney(row.grantsAmount) }}
+                </template>
+              </el-table-column>
+              <el-table-column label="标签" prop="label" min-width="120">
+                <template #default="{ row }">
+                  {{ row.label || '-' }}
+                </template>
+              </el-table-column>
+              <el-table-column label="操作" prop="action" width="160" align="center" fixed="right">
+                <template #default="{ row }">
+                  <el-button v-auth="'rechargeConfig.modify'" type="warning" size="small" text @click="handleItemEdit(g.group.id, row)">编辑</el-button>
+                  <el-button v-auth="'rechargeConfig.remove'" type="danger" size="small" text @click="handleItemRemove(row)">删除</el-button>
+                </template>
+              </el-table-column>
+            </el-table>
+          </div>
+        </div>
+      </div>
+    </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 { $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;
+}
+
+const state = reactive({
+  loading: false,
+  groups: [] as GroupRow[],
+});
+
+onMounted(() => {
+  loadData();
+});
+
+const loadData = () => {
+  state.loading = true;
+  $get('/rechargeConfig/group/list').then((groups: any) => {
+    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(() => {
+    state.loading = false;
+  });
+};
+
+const loadItems = (g: GroupRow) => {
+  g.itemsLoading = true;
+  $get('/rechargeConfig/item/list', { groupId: g.group.id }).then((items: any) => {
+    g.items = items || [];
+    g.itemsLoading = false;
+  }).catch(() => {
+    g.itemsLoading = false;
+  });
+};
+
+const handleGroupAdd = () => {
+  groupDialogRef.value.open('add', null);
+};
+
+const handleGroupRemove = (group: any) => {
+  Msg.confirm('此操作将永久删除该分组及其所有配置项,是否继续?').then(() => {
+    $get(`/rechargeConfig/group/remove/${group.id}`).then(() => {
+      Msg.message("删除成功", 'success');
+      loadData();
+    }).catch(() => {
+      Msg.message("删除失败", 'error');
+    });
+  });
+};
+
+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 });
+};
+
+const handleItemEdit = (groupId: number, row: any) => {
+  itemDialogRef.value.open('edit', { ...row, groupId });
+};
+
+const handleItemRemove = (row: any) => {
+  Msg.confirm('此操作将永久删除该配置项,是否继续?').then(() => {
+    $get(`/rechargeConfig/item/remove/${row.id}`).then(() => {
+      Msg.message("删除成功", 'success');
+      loadData();
+    }).catch(() => {
+      Msg.message("删除失败", 'error');
+    });
+  });
+};
+</script>

+ 142 - 0
admin-web/src/views/admin/platform/rechargeConfig/itemDialog.vue

@@ -0,0 +1,142 @@
+<style scoped lang="scss">
+
+</style>
+<template>
+  <div class="system-dialog-container">
+    <el-dialog
+        :title="state.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"
+          :rules="state.rules"
+          label-position="top"
+          ref="formRef"
+          size="default">
+        <el-form-item label="充值金额(元)" prop="rechargeAmountYuan">
+          <el-input-number
+              v-model="state.ruleForm.rechargeAmountYuan"
+              placeholder="充值金额"
+              :min="0"
+              :step="10"
+              style="width: 100%">
+          </el-input-number>
+        </el-form-item>
+        <el-form-item label="赠款金额(元)" prop="grantsAmountYuan">
+          <el-input-number
+              v-model="state.ruleForm.grantsAmountYuan"
+              placeholder="赠款金额(0表示不赠送)"
+              :min="0"
+              :step="5"
+              style="width: 100%">
+          </el-input-number>
+        </el-form-item>
+        <el-form-item label="标签" prop="label">
+          <el-input
+              v-model="state.ruleForm.label"
+              placeholder="如:推荐、限时"
+              clearable>
+          </el-input>
+        </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>
+      </template>
+    </el-dialog>
+  </div>
+</template>
+
+<script setup lang="ts" name="ItemDialog">
+import { reactive, ref } from 'vue';
+import { Msg } from "/@/utils/message";
+import { $body } from "/@/utils/request";
+import u from '/@/utils/u'
+
+const emit = defineEmits(['refresh']);
+const formRef = ref();
+
+const initState = () => ({
+  ruleForm: {
+    id: null as number | null,
+    groupId: null as number | null,
+    rechargeAmountYuan: 0,
+    grantsAmountYuan: 0,
+    label: '',
+  },
+  btnLoading: false,
+  dialog: {
+    isShowDialog: false,
+    type: '',
+    title: '',
+    submitTxt: '',
+  },
+  rules: {
+    rechargeAmountYuan: [u.validator.required],
+  },
+})
+
+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;
+  if (action !== 'add' && row) {
+    state.ruleForm = {
+      id: row.id,
+      groupId: row.groupId,
+      rechargeAmountYuan: (row.rechargeAmount || 0) / 100,
+      grantsAmountYuan: (row.grantsAmount || 0) / 100,
+      label: row.label || '',
+    };
+  } else if (row) {
+    state.ruleForm.groupId = row.groupId;
+  }
+};
+
+const onClose = () => {
+  state.dialog.isShowDialog = false;
+  Object.assign(state, initState());
+};
+
+const onCancel = () => {
+  onClose();
+};
+
+const onSubmit = () => {
+  formRef.value.validate((v: boolean) => {
+    if (v) {
+      state.btnLoading = true;
+      const payload = {
+        id: state.ruleForm.id,
+        groupId: state.ruleForm.groupId,
+        rechargeAmount: Math.round(state.ruleForm.rechargeAmountYuan * 100),
+        grantsAmount: Math.round(state.ruleForm.grantsAmountYuan * 100),
+        label: state.ruleForm.label || null,
+      };
+      const url = !!state.ruleForm.id ? "/rechargeConfig/item/modify" : "/rechargeConfig/item/add";
+      $body(url, payload).then(() => {
+        state.btnLoading = false;
+        Msg.message('操作成功');
+        onClose();
+        emit('refresh');
+      }).catch(() => {
+        state.btnLoading = false;
+        Msg.message('操作失败', 'error');
+      });
+    } else {
+      Msg.message('请先完整填写表单', 'error');
+    }
+  });
+};
+
+defineExpose({ open });
+</script>

+ 171 - 0
car-wash-admin/src/main/java/com/kym/admin/controller/RechargeConfigController.java

@@ -0,0 +1,171 @@
+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.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
+ */
+@RestController
+@RequestMapping("/rechargeConfig")
+public class RechargeConfigController {
+
+    private final RechargeConfigService itemService;
+    private final RechargeConfigGroupService groupService;
+    private final RechargeConfigStationGroupService stationGroupService;
+
+    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() {
+        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("创建充值配置分组")
+    @PostMapping("/group/add")
+    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("修改充值配置分组")
+    @PostMapping("/group/modify")
+    R<?> groupModify(@RequestBody RechargeConfigGroup group) {
+        groupService.updateById(group);
+        return R.success();
+    }
+
+    /**
+     * 删除分组
+     */
+    @SaCheckPermission("rechargeConfig.remove")
+    @SysLog("删除充值配置分组")
+    @GetMapping("/group/remove/{id}")
+    R<?> groupRemove(@PathVariable long id) {
+        groupService.removeById(id);
+        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")
+    R<?> itemAdd(@RequestBody RechargeConfig config) {
+        itemService.save(config);
+        return R.success();
+    }
+
+    @SaCheckPermission("rechargeConfig.modify")
+    @SysLog("修改充值配置项")
+    @PostMapping("/item/modify")
+    R<?> itemModify(@RequestBody RechargeConfig config) {
+        itemService.updateById(config);
+        return R.success();
+    }
+
+    @SaCheckPermission("rechargeConfig.remove")
+    @SysLog("删除充值配置项")
+    @GetMapping("/item/remove/{id}")
+    R<?> itemRemove(@PathVariable long id) {
+        itemService.removeById(id);
+        return R.success();
+    }
+
+    // ==================== 兼容旧接口 ====================
+
+    @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() {
+        return R.success(itemService.list());
+    }
+}

+ 1 - 1
car-wash-admin/src/main/resources/application-dev.yml

@@ -71,4 +71,4 @@ spring:
 
 kym:
   notify-email: skyline@kuaiyuman.cn
-  domain: https://wash.kuaiyuman.cn
+  domain: https://dev-wash.kuaiyuman.cn

+ 6 - 4
car-wash-entity/src/main/java/com/kym/entity/RechargeConfig.java

@@ -1,15 +1,12 @@
 package com.kym.entity;
 
 import com.baomidou.mybatisplus.annotation.TableName;
-import com.kym.entity.BaseEntity;
 import java.io.Serializable;
 import lombok.Getter;
 import lombok.Setter;
 
 /**
- * <p>
- * 支付日志
- * </p>
+ * 充值配置项 — 属于某个配置组(RechargeConfigGroup),定义单个充值档位的金额和赠款
  *
  * @author skyline
  * @since 2024-11-15
@@ -21,6 +18,11 @@ public class RechargeConfig extends BaseEntity {
 
     private static final long serialVersionUID = 1L;
 
+    /**
+     * 所属分组ID
+     */
+    private Long groupId;
+
     /**
      * 充值金额(分)
      */

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

@@ -0,0 +1,29 @@
+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_group")
+public class RechargeConfigGroup extends BaseEntity {
+
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 分组名称(可选)
+     */
+    private String name;
+
+    /**
+     * 是否默认分组(未分配站点的自动使用)
+     */
+    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;
+}

+ 160 - 0
car-wash-entity/src/main/resources/sql/v2_settlement.sql

@@ -2,6 +2,7 @@
 -- 结算方案 V2 数据库变更脚本
 -- 按月结算模式:每月15日结算上月
 -- 发布日期:2026-05-14
+-- 修订日期:2026-05-26(V2.1 充值配置站点化 + 跨店分账含赠款)
 -- ====================================================
 
 -- ----------------------------
@@ -107,3 +108,162 @@ CREATE TABLE IF NOT EXISTS `t_platform_revenue_record` (
 -- 6. 清除虚拟站点(开发环境)
 -- ----------------------------
 -- DELETE FROM `t_station_account` WHERE `station_id` = '0';
+
+-- ----------------------------
+-- 7. V2.1 充值配置站点化(2026-05-26)
+-- ----------------------------
+ALTER TABLE `t_recharge_config`
+    ADD COLUMN `station_id` VARCHAR(64) DEFAULT NULL COMMENT '站点ID,NULL表示平台默认配置'
+    AFTER `label`;
+
+-- 为现有配置追加唯一索引(不包含NULL值)
+-- CREATE UNIQUE INDEX `uk_station_recharge_amount` ON `t_recharge_config` (`station_id`, `recharge_amount`);
+
+-- ----------------------------
+-- 8. V2.1 充值配置权限初始化(2026-05-26)
+-- ----------------------------
+INSERT INTO `t_permission` (`name`, `value`, `pid`, `weight`)
+SELECT '充值配置列表', 'rechargeConfig.list', 0, 1
+WHERE NOT EXISTS (SELECT 1 FROM `t_permission` WHERE `value` = 'rechargeConfig.list');
+
+INSERT INTO `t_permission` (`name`, `value`, `pid`, `weight`)
+SELECT '充值配置新增', 'rechargeConfig.add', 0, 2
+WHERE NOT EXISTS (SELECT 1 FROM `t_permission` WHERE `value` = 'rechargeConfig.add');
+
+INSERT INTO `t_permission` (`name`, `value`, `pid`, `weight`)
+SELECT '充值配置修改', 'rechargeConfig.modify', 0, 3
+WHERE NOT EXISTS (SELECT 1 FROM `t_permission` WHERE `value` = 'rechargeConfig.modify');
+
+INSERT INTO `t_permission` (`name`, `value`, `pid`, `weight`)
+SELECT '充值配置删除', 'rechargeConfig.remove', 0, 4
+WHERE NOT EXISTS (SELECT 1 FROM `t_permission` WHERE `value` = 'rechargeConfig.remove');
+
+-- 注意:执行完上述权限记录插入后,需在管理后台「角色管理」中为对应角色勾选充值配置权限
+-- 角色权限存储在 t_role.permissions 字段(JSON格式),无法直接通过SQL安全合并
+
+-- ====================================================
+-- 9. V2.2 充值配置分组化(2026-05-26)
+-- ====================================================
+
+-- 9a. 创建配置分组表
+CREATE TABLE IF NOT EXISTS `t_recharge_config_group` (
+    `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
+    `name` VARCHAR(64) DEFAULT NULL COMMENT '分组名称',
+    `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`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='充值配置分组表';
+
+-- 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()
+BEGIN
+    IF NOT EXISTS (
+        SELECT 1 FROM information_schema.COLUMNS
+        WHERE TABLE_SCHEMA = DATABASE()
+          AND TABLE_NAME = 't_recharge_config'
+          AND COLUMN_NAME = 'station_id'
+    ) THEN
+        ALTER TABLE `t_recharge_config`
+            ADD COLUMN `station_id` VARCHAR(64) DEFAULT NULL COMMENT '站点ID,NULL表示平台默认配置'
+            AFTER `label`;
+    END IF;
+END //
+DELIMITER ;
+CALL add_station_id_if_missing();
+DROP PROCEDURE IF EXISTS add_station_id_if_missing;
+
+-- 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`
+);
+
+-- 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
+);
+
+-- 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_station_group` sg WHERE sg.`station_id` = c.`station_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`;
+
+-- 9h. 回填 group_id:通过旧 station_id 匹配到关联表的 group_id
+UPDATE `t_recharge_config` c
+    INNER JOIN `t_recharge_config_station_group` sg ON c.`station_id` = sg.`station_id`
+SET c.`group_id` = sg.`group_id`;
+
+-- 9i. station_id 为 NULL 的行归入默认分组
+UPDATE `t_recharge_config`
+SET `group_id` = (SELECT id FROM `t_recharge_config_group` WHERE `is_default` = 1 LIMIT 1)
+WHERE `group_id` IS NULL;
+
+-- 9j. group_id 设为 NOT NULL
+ALTER TABLE `t_recharge_config`
+    MODIFY COLUMN `group_id` BIGINT NOT NULL COMMENT '所属分组ID';
+
+-- 9k. 删除旧 station_id 列
+DROP PROCEDURE IF EXISTS drop_station_id;
+DELIMITER //
+CREATE PROCEDURE drop_station_id()
+BEGIN
+    IF EXISTS (
+        SELECT 1 FROM information_schema.COLUMNS
+        WHERE TABLE_SCHEMA = DATABASE()
+          AND TABLE_NAME = 't_recharge_config'
+          AND COLUMN_NAME = 'station_id'
+    ) THEN
+        ALTER TABLE `t_recharge_config` DROP COLUMN `station_id`;
+    END IF;
+END //
+DELIMITER ;
+CALL drop_station_id();
+DROP PROCEDURE IF EXISTS drop_station_id;
+
+-- 9l. 组内金额唯一约束
+ALTER TABLE `t_recharge_config`
+    ADD INDEX `idx_group_id` (`group_id`),
+    ADD UNIQUE INDEX `uk_group_recharge_amount` (`group_id`, `recharge_amount`);

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

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

+ 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.mapper.mybatisplus.MyBaseMapper;
+
+/**
+ * 充值配置分组-站点关联 Mapper
+ *
+ * @author skyline
+ * @since 2026-05-26
+ */
+public interface RechargeConfigStationGroupMapper extends MyBaseMapper<RechargeConfigStationGroup> {
+}

+ 4 - 3
car-wash-miniapp/src/main/java/com/kym/miniapp/controller/CommonController.java

@@ -5,6 +5,7 @@ import com.kym.service.ContactService;
 import com.kym.service.RechargeConfigService;
 import org.springframework.web.bind.annotation.GetMapping;
 import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
 import org.springframework.web.bind.annotation.RestController;
 
 /**
@@ -37,12 +38,12 @@ public class CommonController {
 
 
     /**
-     * 充值金额配置项
+     * 充值金额配置项(按站点查询,优先返回站点专属配置,若无则返回默认配置)
      *
      * @return
      */
     @GetMapping("/rechargeConfig")
-    R<?> rechargeConfig() {
-        return R.success(rechargeConfigService.list());
+    R<?> rechargeConfig(@RequestParam(required = false) String stationId) {
+        return R.success(rechargeConfigService.listByStationId(stationId));
     }
 }

+ 8 - 1
car-wash-mp/src/pages-user/wallet/recharge.vue

@@ -87,7 +87,14 @@ const handleRechargeClick = (recharge: any, idx: number) => {
 };
 
 const loadRechargeConfig = () => {
-  get("/common/rechargeConfig")
+  // 优先使用归属站,无归属站时用当前消费站,都没有则不传(走默认分组)
+  const homeStationId = getApp<any>().globalData.user?.stationId;
+  const stationId = homeStationId || state.stationId || undefined;
+  const params: any = {};
+  if (stationId) {
+    params.stationId = stationId;
+  }
+  get("/common/rechargeConfig", params)
     .then((res: any) => {
       state.configList = res;
     })

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

@@ -0,0 +1,28 @@
+package com.kym.service;
+
+import com.kym.entity.RechargeConfigGroup;
+import com.kym.service.mybatisplus.MyBaseService;
+
+/**
+ * 充值配置分组服务
+ *
+ * @author skyline
+ * @since 2026-05-26
+ */
+public interface RechargeConfigGroupService extends MyBaseService<RechargeConfigGroup> {
+
+    /**
+     * 根据站点ID获取其关联的分组(通过关联表查询),无关联时返回默认分组
+     */
+    RechargeConfigGroup getByStationId(String stationId);
+
+    /**
+     * 获取默认分组(is_default = 1)
+     */
+    RechargeConfigGroup getDefaultGroup();
+
+    /**
+     * 创建分组。如果站点列表非空,同时建立关联
+     */
+    RechargeConfigGroup createGroup(RechargeConfigGroup group, java.util.List<String> stationIds);
+}

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

@@ -3,15 +3,31 @@ package com.kym.service;
 import com.kym.entity.RechargeConfig;
 import com.kym.service.mybatisplus.MyBaseService;
 
+import java.util.List;
+
 /**
- * <p>
- * 支付日志 服务类
- * </p>
+ * 充值配置项服务
  *
  * @author skyline
  * @since 2024-11-15
  */
 public interface RechargeConfigService extends MyBaseService<RechargeConfig> {
 
-    RechargeConfig getRechargeConfigByAmount(Integer amount);
+    /**
+     * 根据站点查询配置项(通过分组联查),优先返回站点专属配置,若无则返回默认配置。
+     * 返回扁平列表,API响应格式不变。
+     */
+    List<RechargeConfig> listByStationId(String stationId);
+
+    /**
+     * 根据用户归属站点+充值金额查找配置项。
+     * 先查站点专属分组 → 回退默认分组 → 组内匹配金额。
+     * 用于充值回调场景(wxNotify),确保同金额不同组时赠款金额正确。
+     */
+    RechargeConfig getByStationAndAmount(String stationId, Integer amount);
+
+    /**
+     * 根据分组ID查询该分组下的所有配置项
+     */
+    List<RechargeConfig> listByGroupId(Long groupId);
 }

+ 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);
+}

+ 9 - 8
car-wash-service/src/main/java/com/kym/service/awoara/event/handle/OrderCloseEventHandler.java

@@ -163,25 +163,26 @@ public class OrderCloseEventHandler implements AwoaraEventHandler<OrderInfoObjec
 
     /**
      * 跨店消费分账 — V2 结算方案
-     * 仅记录跨店支出(充值款实收 × 70%)和跨店收入(充值款实收 × 70%)
+     * 以订单实收总额(充值余额 + 赠款余额)的 70% 为跨店转账基数
      * 平台服务费由结算日统一计算,70:30 拉新奖励比例在此体现
-     * 注意:按充值款实收金额 (rechargePayment) 计算,赠款为平台促销金,不参与站点间转账
+     * 赠款为归属站点自行发起的营销成本,参与跨店分账,由归属站承担
      *
      * @param washOrder     洗车订单
      * @param userStationId 用户归属站点(充值方)
      */
     @Transactional
     protected void doCrossSplit(WashOrder washOrder, String userStationId) {
-        // 仅以充值款实收金额为跨店转账基数,赠款为平台促销金,不参与站点间转账
         int rechargePayment = washOrder.getRechargePayment();
-        if (rechargePayment == 0) {
-            log.info("订单:{},充值款支付为0(全部为赠款支付),不产生跨店转账", washOrder.getOrderId());
+        int grantsPayment = washOrder.getGrantsPayment();
+        int totalPayment = rechargePayment + grantsPayment;
+        if (totalPayment == 0) {
+            log.info("订单:{},支付金额为0,不产生跨店转账", washOrder.getOrderId());
             return;
         }
         BigDecimal crossRate = new BigDecimal("0.7");
-        int crossAmount = crossRate.multiply(BigDecimal.valueOf(rechargePayment)).setScale(0, RoundingMode.DOWN).intValue();
-        log.info("订单:{},执行跨店分账,归属站:{},消费站:{},充值款实收:{},转账金额:{}",
-                washOrder.getOrderId(), userStationId, washOrder.getStationId(), rechargePayment, crossAmount);
+        int crossAmount = crossRate.multiply(BigDecimal.valueOf(totalPayment)).setScale(0, RoundingMode.DOWN).intValue();
+        log.info("订单:{},执行跨店分账,归属站:{},消费站:{},充值款实收:{},赠款实收:{},转账金额:{}",
+                washOrder.getOrderId(), userStationId, washOrder.getStationId(), rechargePayment, grantsPayment, crossAmount);
 
         // 归属站点跨店支出
         var crossExpend = new SplitRecord()

+ 81 - 0
car-wash-service/src/main/java/com/kym/service/impl/RechargeConfigGroupServiceImpl.java

@@ -0,0 +1,81 @@
+package com.kym.service.impl;
+
+import com.kym.entity.RechargeConfig;
+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.context.annotation.Lazy;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.List;
+
+/**
+ * 充值配置分组服务实现
+ *
+ * @author skyline
+ * @since 2026-05-26
+ */
+@Service
+public class RechargeConfigGroupServiceImpl
+        extends MyBaseServiceImpl<RechargeConfigGroupMapper, RechargeConfigGroup>
+        implements RechargeConfigGroupService {
+
+    private final RechargeConfigService itemService;
+    private final RechargeConfigStationGroupService stationGroupService;
+
+    public RechargeConfigGroupServiceImpl(@Lazy RechargeConfigService itemService,
+                                           RechargeConfigStationGroupService stationGroupService) {
+        this.itemService = itemService;
+        this.stationGroupService = stationGroupService;
+    }
+
+    @Override
+    public RechargeConfigGroup getByStationId(String stationId) {
+        Long groupId = stationGroupService.getGroupIdByStationId(stationId);
+        if (groupId != null) {
+            return getById(groupId);
+        }
+        return null;
+    }
+
+    @Override
+    public RechargeConfigGroup getDefaultGroup() {
+        return lambdaQuery().eq(RechargeConfigGroup::getIsDefault, true).one();
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public RechargeConfigGroup createGroup(RechargeConfigGroup group, List<String> stationIds) {
+        save(group);
+
+        // 复制默认分组的配置项
+        RechargeConfigGroup defaultGroup = getDefaultGroup();
+        if (defaultGroup != null) {
+            List<RechargeConfig> defaultItems = itemService.listByGroupId(defaultGroup.getId());
+            if (!defaultItems.isEmpty()) {
+                var newItems = defaultItems.stream().map(c -> {
+                    var item = new RechargeConfig();
+                    item.setGroupId(group.getId());
+                    item.setRechargeAmount(c.getRechargeAmount());
+                    item.setGrantsAmount(c.getGrantsAmount());
+                    item.setLabel(c.getLabel());
+                    return item;
+                }).toList();
+                itemService.saveBatch(newItems);
+            }
+        }
+
+        // 关联站点
+        if (stationIds != null) {
+            for (String stationId : stationIds) {
+                stationGroupService.assignStationToGroup(stationId, group.getId());
+            }
+        }
+
+        return group;
+    }
+}

+ 64 - 5
car-wash-service/src/main/java/com/kym/service/impl/RechargeConfigServiceImpl.java

@@ -1,24 +1,83 @@
 package com.kym.service.impl;
 
 import com.kym.entity.RechargeConfig;
+import com.kym.entity.RechargeConfigGroup;
 import com.kym.mapper.RechargeConfigMapper;
+import com.kym.service.RechargeConfigGroupService;
 import com.kym.service.RechargeConfigService;
 import com.kym.service.mybatisplus.MyBaseServiceImpl;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.context.annotation.Lazy;
 import org.springframework.stereotype.Service;
 
+import java.util.List;
+
 /**
- * <p>
- * 支付日志 服务实现类
- * </p>
+ * 充值配置项服务实现
  *
  * @author skyline
  * @since 2024-11-15
  */
+@Slf4j
 @Service
 public class RechargeConfigServiceImpl extends MyBaseServiceImpl<RechargeConfigMapper, RechargeConfig> implements RechargeConfigService {
 
+    private final RechargeConfigGroupService groupService;
+
+    public RechargeConfigServiceImpl(@Lazy RechargeConfigGroupService groupService) {
+        this.groupService = groupService;
+    }
+
+    @Override
+    public List<RechargeConfig> listByStationId(String stationId) {
+        log.info("listByStationId: stationId={}", stationId);
+        // 优先站点专属分组
+        RechargeConfigGroup group = groupService.getByStationId(stationId);
+        log.info("listByStationId: stationGroup={}", group != null ? group.getId() : null);
+        if (group != null) {
+            var items = lambdaQuery().eq(RechargeConfig::getGroupId, group.getId()).list();
+            log.info("listByStationId: stationGroup items count={}", items.size());
+            if (!items.isEmpty()) {
+                return items;
+            }
+        }
+        // 回退默认分组
+        group = groupService.getDefaultGroup();
+        log.info("listByStationId: defaultGroup={}", group != null ? group.getId() : null);
+        if (group != null) {
+            var items = lambdaQuery().eq(RechargeConfig::getGroupId, group.getId()).list();
+            log.info("listByStationId: defaultGroup items count={}", items.size());
+            return items;
+        }
+        return List.of();
+    }
+
+    @Override
+    public RechargeConfig getByStationAndAmount(String stationId, Integer amount) {
+        // 优先站点专属分组
+        RechargeConfigGroup group = groupService.getByStationId(stationId);
+        if (group != null) {
+            var config = lambdaQuery()
+                    .eq(RechargeConfig::getGroupId, group.getId())
+                    .eq(RechargeConfig::getRechargeAmount, amount)
+                    .one();
+            if (config != null) {
+                return config;
+            }
+        }
+        // 回退默认分组
+        group = groupService.getDefaultGroup();
+        if (group != null) {
+            return lambdaQuery()
+                    .eq(RechargeConfig::getGroupId, group.getId())
+                    .eq(RechargeConfig::getRechargeAmount, amount)
+                    .one();
+        }
+        return null;
+    }
+
     @Override
-    public RechargeConfig getRechargeConfigByAmount(Integer amount) {
-        return lambdaQuery().eq(RechargeConfig::getRechargeAmount, amount).one();
+    public List<RechargeConfig> listByGroupId(Long groupId) {
+        return lambdaQuery().eq(RechargeConfig::getGroupId, groupId).list();
     }
 }

+ 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();
+    }
+}

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

@@ -8,6 +8,7 @@ import com.kym.entity.WashStation;
 import com.kym.entity.queryParams.StationQueryParams;
 import com.kym.entity.vo.WashStationVo;
 import com.kym.mapper.WashStationMapper;
+import com.kym.service.RechargeConfigGroupService;
 import com.kym.service.WashDeviceService;
 import com.kym.service.WashStationService;
 import com.kym.service.cache.KymCache;
@@ -37,9 +38,11 @@ public class WashStationServiceImpl extends MyBaseServiceImpl<WashStationMapper,
     }
 
     private final WashDeviceService washDeviceService;
+    private final RechargeConfigGroupService rechargeConfigGroupService;
 
-    public WashStationServiceImpl(WashDeviceService washDeviceService) {
+    public WashStationServiceImpl(WashDeviceService washDeviceService, RechargeConfigGroupService rechargeConfigGroupService) {
         this.washDeviceService = washDeviceService;
+        this.rechargeConfigGroupService = rechargeConfigGroupService;
     }
 
 
@@ -51,6 +54,8 @@ public class WashStationServiceImpl extends MyBaseServiceImpl<WashStationMapper,
         CommUtil.asserts(count == 0, "站点已存在");
         save(station);
 
+        // 新站点不自动创建分组,默认使用平台默认分组。管理员可在充值配置中为该站点分配专属分组。
+        // 如需自动创建:rechargeConfigGroupService.createGroup(new RechargeConfigGroup(), List.of(station.getStationId()));
     }
 
     @Override

+ 6 - 2
car-wash-service/src/main/java/com/kym/service/wechat/impl/WxPayServiceImpl.java

@@ -314,7 +314,12 @@ public class WxPayServiceImpl implements WxPayService {
             var walletDetail = walletDetailService.getWalletDetailByOrderNo(transaction.getOutTradeNo(), WalletDetail.TYPE_充值);
             if (walletDetail != null) {
 
-                var rechargeConfig = rechargeConfigService.getRechargeConfigByAmount(walletDetail.getAmount());
+                var stationId = transaction.getAttach();
+                var rechargeConfig = rechargeConfigService.getByStationAndAmount(stationId, walletDetail.getAmount());
+                if (rechargeConfig == null) {
+                    LOGGER.error("微信支付回调:未找到充值配置,stationId={}, amount={}", stationId, walletDetail.getAmount());
+                    return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
+                }
 
                 // 更新余额(赠款计入不可退优惠金额)
                 var account = accountService.getAccountByUserId(walletDetail.getUserId());
@@ -356,7 +361,6 @@ public class WxPayServiceImpl implements WxPayService {
                 payLogService.save(payLog);
 
                 // V2 结算方案:充值资金记录分账流水,结算日统一处理,不再即时转入站点账户
-                var stationId = transaction.getAttach();
 
                 // 分账记录(结算时按此汇总)
                 var splitRecord = new SplitRecord()

+ 16 - 2
docs/结算方案-业务说明.md

@@ -42,7 +42,8 @@
 **跨店转账比例**:70:30
 
 - 用户在 A 站充值、去 B 站消费 1,000 元时,A 站向 B 站转账 700 元(消费金额的 70%),A 站保留 300 元(30%)作为拉新奖励
-- 即跨店支出/收入以 70% 记账
+- 即跨店支出/收入以订单实收总额(充值余额 + 赠款余额)的 70% 记账
+- 赠款为归属站点自行发起的营销成本,参与跨店分账,由归属站承担
 
 **关键规则**:
 
@@ -118,11 +119,22 @@
   → 本站消费(A = B):
       记入 SplitRecord(type=消费, from=站点, to=站点),金额为消费全额
   → 跨店消费(A ≠ B):
-      记入 SplitRecord(type=跨店支出, from=A, to=B, amount=700)  ← 消费金额 × 70%
+      以订单实收总额(充值余额 + 赠款余额)的 70% 为跨店转账基数
+      记入 SplitRecord(type=跨店支出, from=A, to=B, amount=700)  ← 实收总额 × 70%
       记入 SplitRecord(type=跨店收入, from=A, to=B, amount=700)  ← 同上
       A 站保留 300 元(30%)作为拉新奖励
+      赠款为归属站点自行发起的营销成本,参与跨店分账,由归属站承担
 ```
 
+### 4.2.1 充值配置分组化(V2.2)
+
+充值配置采用「分组 + 配置项」两级模型:
+
+- **`t_recharge_config_group`**:配置分组,`station_id` 关联站点(NULL = 平台默认组),每站一个分组
+- **`t_recharge_config`**:配置项,`group_id` 关联分组,单条记录表示一个充值金额档位
+
+查询逻辑:先查站点专属分组 → 回退默认分组 → 返回组内配置项列表。充值回调(wxNotify)通过「归属站 + 金额」查找配置项,确保同金额不同组时赠款金额正确。新站点创建时自动创建专属分组并复制默认分组的配置项。
+
 ### 4.3 退款
 
 ```
@@ -232,4 +244,6 @@
 
 | 版本 | 日期 | 说明 |
 |---|---|---|
+| V2.2 | 2026-05-26 | 充值配置分组化(group + item 两级模型) |
+| V2.1 | 2026-05-26 | 跨店分账基数调整为充值余额+赠款余额;充值配置站点化(`RechargeConfig.stationId`)+ 默认配置回退 |
 | V2-draft | 2026-05-14 | 初稿,基于业务讨论整理 |