瀏覽代碼

补货员功能重构

skyline 1 月之前
父節點
當前提交
30064360d2
共有 33 個文件被更改,包括 2580 次插入43 次删除
  1. 14 0
      haha-admin-web/src/api/device.ts
  2. 88 0
      haha-admin-web/src/api/replenisher.ts
  3. 10 0
      haha-admin-web/src/api/shop.ts
  4. 9 0
      haha-admin-web/src/router/modules/operation.ts
  5. 120 2
      haha-admin-web/src/views/device/index.vue
  6. 147 2
      haha-admin-web/src/views/device/utils/hook.tsx
  7. 273 0
      haha-admin-web/src/views/replenisher/index.vue
  8. 497 0
      haha-admin-web/src/views/replenisher/utils/hook.tsx
  9. 36 0
      haha-admin-web/src/views/replenisher/utils/types.ts
  10. 130 38
      haha-admin-web/src/views/shop/utils/hook.tsx
  11. 49 0
      haha-admin/src/main/java/com/haha/admin/controller/DeviceController.java
  12. 14 0
      haha-admin/src/main/java/com/haha/admin/controller/InventoryController.java
  13. 186 0
      haha-admin/src/main/java/com/haha/admin/controller/ReplenisherController.java
  14. 31 0
      haha-admin/src/main/java/com/haha/admin/controller/ShopController.java
  15. 95 0
      haha-entity/src/main/java/com/haha/entity/Replenisher.java
  16. 46 0
      haha-entity/src/main/java/com/haha/entity/ReplenisherDevice.java
  17. 19 0
      haha-entity/src/main/java/com/haha/entity/dto/BatchBindDevicesDTO.java
  18. 16 0
      haha-entity/src/main/java/com/haha/entity/dto/BindDeviceDTO.java
  19. 14 0
      haha-entity/src/main/java/com/haha/entity/dto/ReplenisherBindDTO.java
  20. 22 0
      haha-entity/src/main/java/com/haha/entity/dto/ReplenisherCreateDTO.java
  21. 22 0
      haha-entity/src/main/java/com/haha/entity/dto/ReplenisherQueryDTO.java
  22. 25 0
      haha-entity/src/main/java/com/haha/entity/dto/ReplenisherUpdateDTO.java
  23. 45 0
      haha-entity/src/main/java/com/haha/entity/dto/StockRecordCreateV2DTO.java
  24. 53 0
      haha-mapper/src/main/java/com/haha/mapper/ReplenisherDeviceMapper.java
  25. 67 0
      haha-mapper/src/main/java/com/haha/mapper/ReplenisherMapper.java
  26. 23 0
      haha-mapper/src/main/resources/mapper/ReplenisherDeviceMapper.xml
  27. 38 0
      haha-mapper/src/main/resources/mapper/ReplenisherMapper.xml
  28. 122 0
      haha-service/src/main/java/com/haha/service/ReplenisherService.java
  29. 19 0
      haha-service/src/main/java/com/haha/service/ShopReplenisherService.java
  30. 14 0
      haha-service/src/main/java/com/haha/service/StockRecordService.java
  31. 274 0
      haha-service/src/main/java/com/haha/service/impl/ReplenisherServiceImpl.java
  32. 37 1
      haha-service/src/main/java/com/haha/service/impl/ShopReplenisherServiceImpl.java
  33. 25 0
      haha-service/src/main/java/com/haha/service/impl/StockRecordServiceImpl.java

+ 14 - 0
haha-admin-web/src/api/device.ts

@@ -68,3 +68,17 @@ export interface DoorRecordItem {
 export const getDoorRecords = (deviceId: string, params: { page?: number; pageSize?: number }) => {
   return http.request<ResultTable>("get", `/devices/${deviceId}/door-records`, { params });
 };
+
+// ==================== 补货员管理 ====================
+
+export const getDeviceReplenishers = (deviceId: string) => {
+  return http.request<Result>("get", `/devices/${deviceId}/replenishers`);
+};
+
+export const bindDeviceReplenisher = (deviceId: string, data: { replenisherId: number }) => {
+  return http.request<Result>("post", `/devices/${deviceId}/replenishers`, { data });
+};
+
+export const unbindDeviceReplenisher = (deviceId: string, replenisherId: number) => {
+  return http.request<Result>("delete", `/devices/${deviceId}/replenishers/${replenisherId}`);
+};

+ 88 - 0
haha-admin-web/src/api/replenisher.ts

@@ -0,0 +1,88 @@
+import { http } from "@/utils/http";
+
+type Result = {
+  code: number;
+  message: string;
+  data?: any;
+};
+
+type ResultTable = {
+  code: number;
+  message: string;
+  data?: {
+    list: Array<any>;
+    total: number;
+    pageSize: number;
+    currentPage: number;
+  };
+};
+
+export const getReplenisherList = (params: {
+  page?: number;
+  pageSize?: number;
+  keyword?: string;
+  status?: number;
+}) => {
+  return http.request<ResultTable>("get", "/replenishers/list", { params });
+};
+
+export const getReplenisherById = (id: number) => {
+  return http.request<Result>("get", `/replenishers/${id}`);
+};
+
+export const searchReplenishers = (keyword?: string) => {
+  return http.request<Result>(
+    "get",
+    `/replenishers/search${keyword ? `?keyword=${keyword}` : ""}`
+  );
+};
+
+export const createReplenisher = (data: {
+  name: string;
+  phone?: string;
+  employeeId?: string;
+}) => {
+  return http.request<Result>("post", "/replenishers", { data });
+};
+
+export const updateReplenisher = (
+  id: number,
+  data: {
+    name?: string;
+    phone?: string;
+    employeeId?: string;
+    status?: number;
+  }
+) => {
+  return http.request<Result>("put", `/replenishers/${id}`, { data });
+};
+
+export const updateReplenisherStatus = (id: number, data: { status: number }) => {
+  return http.request<Result>("put", `/replenishers/${id}/status`, { data });
+};
+
+export const deleteReplenisher = (id: number) => {
+  return http.request<Result>("delete", `/replenishers/${id}`);
+};
+
+export const getBoundDevices = (id: number) => {
+  return http.request<Result>("get", `/replenishers/${id}/devices`);
+};
+
+export const bindDevices = (id: number, data: { deviceIds: string[] }) => {
+  return http.request<Result>("post", `/replenishers/${id}/devices`, { data });
+};
+
+export const unbindDevice = (replenisherId: number, deviceId: string) => {
+  return http.request<Result>(
+    "delete",
+    `/replenishers/${replenisherId}/devices/${deviceId}`
+  );
+};
+
+export const batchBindDevices = (data: {
+  replenisherIds: number[];
+  deviceIds: string[];
+}) => {
+  return http.request<Result>("post", "/replenishers/batch-bind", { data });
+};

+ 10 - 0
haha-admin-web/src/api/shop.ts

@@ -130,3 +130,13 @@ export const getAvailableUsers = (keyword?: string) => {
     `/shops/available-users${keyword ? `?keyword=${keyword}` : ""}`
   );
 };
+
+// ==================== V2 补货员管理(独立补货员体系) ====================
+
+export const getShopReplenishersV2 = (id: number) => {
+  return http.request<Result>("get", `/shops/${id}/replenishers/v2`);
+};
+
+export const addReplenisherV2 = (id: number, data: { replenisherId: number }) => {
+  return http.request<Result>("post", `/shops/${id}/replenishers/v2`, { data });
+};

+ 9 - 0
haha-admin-web/src/router/modules/operation.ts

@@ -33,6 +33,15 @@ export default {
         icon: "ri:map-pin-line",
         title: "门店分布地图"
       }
+    },
+    {
+      path: "/operation/replenisher",
+      name: "ReplenisherList",
+      component: () => import("@/views/replenisher/index.vue"),
+      meta: {
+        icon: "ri:user-star-line",
+        title: "补货员管理"
+      }
     }
   ]
 } satisfies RouteConfigsTable;

+ 120 - 2
haha-admin-web/src/views/device/index.vue

@@ -1,5 +1,5 @@
 <script setup lang="ts">
-import { ref } from "vue";
+import { ref, computed } from "vue";
 import { useDevice } from "./utils/hook";
 import { PureTableBar } from "@/components/RePureTableBar";
 import { useRenderIcon } from "@/components/ReIcon/src/hooks";
@@ -8,6 +8,7 @@ import Unlock from "~icons/ep/unlock";
 import Temperature from "~icons/ri/temp-hot-line";
 import Volume from "~icons/ri/volume-up-line";
 import Document from "~icons/ep/document";
+import User from "~icons/ep/user";
 
 defineOptions({
   name: "DeviceManage"
@@ -38,7 +39,22 @@ const {
   recordColumns,
   recordPagination,
   handleRecordSizeChange,
-  handleRecordCurrentChange
+  handleRecordCurrentChange,
+  replenisherDialogVisible,
+  replenisherLoading,
+  currentDeviceForReplenisher,
+  boundReplenishers,
+  allReplenishers,
+  selectedReplenisherIds,
+  replenisherFilterKeyword,
+  filteredReplenishers,
+  replenisherAllSelected,
+  replenisherSelectPartial,
+  confirmReplenisherLoading,
+  handleBindReplenisher,
+  toggleReplenisherSelect,
+  toggleAllReplenishers,
+  handleReplenisherConfirm
 } = useDevice(tableRef);
 </script>
 
@@ -168,6 +184,16 @@ const {
             >
               开关门记录
             </el-button>
+            <el-button
+              class="reset-margin"
+              link
+              type="primary"
+              :size="size"
+              :icon="useRenderIcon(User)"
+              @click="handleBindReplenisher(row)"
+            >
+              补货员
+            </el-button>
           </template>
         </pure-table>
       </template>
@@ -191,6 +217,85 @@ const {
         @page-current-change="handleRecordCurrentChange"
       />
     </el-dialog>
+
+    <!-- 补货员绑定弹窗(勾选模式) -->
+    <el-dialog
+      v-model="replenisherDialogVisible"
+      :title="`补货员绑定 - ${currentDeviceForReplenisher?.deviceId || ''}`"
+      width="500px"
+      draggable
+      :close-on-click-modal="false"
+      destroy-on-close
+    >
+      <div v-loading="replenisherLoading" class="replenisher-dialog-container">
+        <!-- 绑定状态摘要 -->
+        <div class="mb-3 px-1 text-sm text-gray-500 flex items-center gap-1">
+          已选择
+          <span class="font-bold text-[var(--el-color-primary)]">{{ selectedReplenisherIds.size }}</span>
+          名补货员
+          <el-divider direction="vertical" />
+          共 {{ allReplenishers.length }} 名补货员
+        </div>
+
+        <!-- 搜索过滤 -->
+        <el-input
+          v-model="replenisherFilterKeyword"
+          placeholder="输入姓名/手机号/工号筛选补货员"
+          clearable
+          class="mb-2"
+          size="small"
+        />
+
+        <!-- 全选 -->
+        <div class="flex items-center px-1 pb-2 border-b border-gray-100 mb-2">
+          <el-checkbox
+            :model-value="replenisherAllSelected && filteredReplenishers.length > 0"
+            :indeterminate="replenisherSelectPartial"
+            size="small"
+            @change="toggleAllReplenishers"
+          />
+          <span class="ml-2 text-xs text-gray-400">全选当前列表({{ filteredReplenishers.length }}人)</span>
+        </div>
+
+        <!-- 补货员列表 -->
+        <div v-if="allReplenishers.length === 0 && !replenisherLoading" class="py-8">
+          <el-empty description="暂无可用补货员" />
+        </div>
+
+        <div v-if="filteredReplenishers.length > 0" class="divide-y divide-gray-100">
+          <label
+            v-for="r in filteredReplenishers"
+            :key="r.id"
+            class="replenisher-item flex items-center px-3 py-2.5 hover:bg-gray-50/50 cursor-pointer transition-colors rounded"
+          >
+            <el-checkbox
+              :model-value="selectedReplenisherIds.has(r.id)"
+              size="small"
+              @change="toggleReplenisherSelect(r.id)"
+            />
+            <div class="ml-2.5 flex items-center gap-2 min-w-0">
+              <span class="text-sm font-medium truncate">{{ r.name }}</span>
+              <span v-if="r.phone" class="text-xs text-gray-400 shrink-0">{{ r.phone }}</span>
+              <el-tag v-if="r.employeeId" size="small" type="info" class="shrink-0">{{ r.employeeId }}</el-tag>
+            </div>
+          </label>
+        </div>
+        <div v-else-if="!replenisherLoading && replenisherFilterKeyword && allReplenishers.length > 0" class="text-xs text-gray-400 text-center py-4">
+          未找到匹配的补货员
+        </div>
+      </div>
+
+      <template #footer>
+        <el-button @click="replenisherDialogVisible = false">取消</el-button>
+        <el-button
+          type="primary"
+          :loading="confirmReplenisherLoading"
+          @click="handleReplenisherConfirm"
+        >
+          确认绑定
+        </el-button>
+      </template>
+    </el-dialog>
   </div>
 </template>
 
@@ -204,4 +309,17 @@ const {
     margin-bottom: 12px;
   }
 }
+
+.replenisher-dialog-container {
+  min-height: 200px;
+  max-height: 450px;
+  overflow-y: auto;
+}
+
+.replenisher-item {
+  transition: background-color 0.2s;
+  &:hover {
+    background-color: var(--el-fill-color-darker);
+  }
+}
 </style>

+ 147 - 2
haha-admin-web/src/views/device/utils/hook.tsx

@@ -9,10 +9,14 @@ import {
   setTemperature,
   setVolume,
   getDoorRecords,
+  getDeviceReplenishers,
+  bindDeviceReplenisher,
+  unbindDeviceReplenisher,
   type DoorRecordItem
 } from "@/api/device";
 import { getEnabledShops } from "@/api/shop";
-import { type Ref, ref, toRaw, reactive, onMounted } from "vue";
+import { searchReplenishers } from "@/api/replenisher";
+import { type Ref, computed, ref, toRaw, reactive, onMounted } from "vue";
 import {
   ElForm,
   ElFormItem,
@@ -476,6 +480,131 @@ export function useDevice(tableRef: Ref) {
     recordPagination.currentPage = val;
     fetchDoorRecords();
   }
+
+  // ==================== 补货员绑定管理(勾选模式) ====================
+  const replenisherDialogVisible = ref(false);
+  const replenisherLoading = ref(false);
+  const currentDeviceForReplenisher = ref<DeviceItem>(null);
+  const boundReplenishers = ref<any[]>([]);
+  const allReplenishers = ref<any[]>([]);
+  const selectedReplenisherIds = ref<Set<number>>(new Set());
+  const origBoundReplenisherIds = ref<Set<number>>(new Set());
+  const replenisherFilterKeyword = ref("");
+  const confirmReplenisherLoading = ref(false);
+
+  const filteredReplenishers = computed(() => {
+    const keyword = replenisherFilterKeyword.value.trim().toLowerCase();
+    if (!keyword) return allReplenishers.value;
+    return allReplenishers.value.filter((r: any) => {
+      return (
+        (r.name && r.name.toLowerCase().includes(keyword)) ||
+        (r.phone && r.phone.includes(keyword)) ||
+        (r.employeeId && r.employeeId.toLowerCase().includes(keyword))
+      );
+    });
+  });
+
+  // 全选状态(当前过滤列表)
+  const replenisherAllSelected = computed(() => {
+    const filtered = filteredReplenishers.value;
+    return filtered.length > 0 && filtered.every((r: any) => selectedReplenisherIds.value.has(r.id));
+  });
+
+  const replenisherSelectPartial = computed(() => {
+    const filtered = filteredReplenishers.value;
+    return filtered.some((r: any) => selectedReplenisherIds.value.has(r.id)) && !replenisherAllSelected.value;
+  });
+
+  function toggleAllReplenishers() {
+    const newSet = new Set(selectedReplenisherIds.value);
+    if (replenisherAllSelected.value) {
+      filteredReplenishers.value.forEach((r: any) => newSet.delete(r.id));
+    } else {
+      filteredReplenishers.value.forEach((r: any) => newSet.add(r.id));
+    }
+    selectedReplenisherIds.value = newSet;
+  }
+
+  async function handleBindReplenisher(row: DeviceItem) {
+    currentDeviceForReplenisher.value = row;
+    replenisherDialogVisible.value = true;
+    replenisherFilterKeyword.value = "";
+    replenisherLoading.value = true;
+
+    try {
+      // 并行获取所有补货员 + 已绑定的补货员
+      const [allRes, boundRes] = await Promise.all([
+        searchReplenishers(""),
+        getDeviceReplenishers(row.deviceId)
+      ]);
+
+      allReplenishers.value = allRes.data || [];
+      const boundList: any[] = boundRes.data || [];
+      boundReplenishers.value = boundList;
+
+      const boundIds = new Set<number>(boundList.map((r: any) => r.id));
+      selectedReplenisherIds.value = new Set(boundIds);
+      origBoundReplenisherIds.value = new Set(boundIds);
+    } catch (error) {
+      console.error("获取数据失败:", error);
+      message("加载数据失败", { type: "error" });
+      allReplenishers.value = [];
+    } finally {
+      replenisherLoading.value = false;
+    }
+  }
+
+  function toggleReplenisherSelect(replenisherId: number) {
+    const newSet = new Set(selectedReplenisherIds.value);
+    if (newSet.has(replenisherId)) {
+      newSet.delete(replenisherId);
+    } else {
+      newSet.add(replenisherId);
+    }
+    selectedReplenisherIds.value = newSet;
+  }
+
+  async function handleReplenisherConfirm() {
+    if (!currentDeviceForReplenisher.value) return;
+
+    const deviceId = currentDeviceForReplenisher.value.deviceId;
+    const origSet = origBoundReplenisherIds.value;
+    const newSet = selectedReplenisherIds.value;
+
+    const toBind = [...newSet].filter(id => !origSet.has(id));
+    const toUnbind = [...origSet].filter(id => !newSet.has(id));
+
+    if (toBind.length === 0 && toUnbind.length === 0) {
+      message("未做任何变更", { type: "info" });
+      return;
+    }
+
+    confirmReplenisherLoading.value = true;
+    try {
+      // 批量绑定新选择的补货员
+      if (toBind.length > 0) {
+        await Promise.all(toBind.map(id =>
+          bindDeviceReplenisher(deviceId, { replenisherId: id })
+        ));
+      }
+
+      // 批量解绑被取消的补货员
+      if (toUnbind.length > 0) {
+        await Promise.all(toUnbind.map(id =>
+          unbindDeviceReplenisher(deviceId, id)
+        ));
+      }
+
+      message(`补货员更新完成(绑定${toBind.length}人,解绑${toUnbind.length}人)`, { type: "success" });
+      replenisherDialogVisible.value = false;
+      onSearch();
+    } catch (error) {
+      message("操作失败", { type: "error" });
+    } finally {
+      confirmReplenisherLoading.value = false;
+    }
+  }
+
   async function fetchShopOptions() {
     try {
       const { data } = await getEnabledShops();
@@ -513,6 +642,22 @@ export function useDevice(tableRef: Ref) {
     recordPagination,
     showDoorRecords,
     handleRecordSizeChange,
-    handleRecordCurrentChange
+    handleRecordCurrentChange,
+    // 补货员绑定相关(勾选模式)
+    replenisherDialogVisible,
+    replenisherLoading,
+    currentDeviceForReplenisher,
+    boundReplenishers,
+    allReplenishers,
+    selectedReplenisherIds,
+    replenisherFilterKeyword,
+    filteredReplenishers,
+    replenisherAllSelected,
+    replenisherSelectPartial,
+    confirmReplenisherLoading,
+    handleBindReplenisher,
+    toggleReplenisherSelect,
+    toggleAllReplenishers,
+    handleReplenisherConfirm
   };
 }

+ 273 - 0
haha-admin-web/src/views/replenisher/index.vue

@@ -0,0 +1,273 @@
+<script setup lang="ts">
+import { ref } from "vue";
+import { useReplenisher } from "./utils/hook";
+import { PureTableBar } from "@/components/RePureTableBar";
+import { useRenderIcon } from "@/components/ReIcon/src/hooks";
+
+import Delete from "~icons/ep/delete";
+import EditPen from "~icons/ep/edit-pen";
+import Refresh from "~icons/ep/refresh";
+import AddFill from "~icons/ri/add-circle-line";
+import Link from "~icons/ep/link";
+import ArrowRight from "~icons/ep/arrow-right";
+
+defineOptions({
+  name: "ReplenisherManage"
+});
+
+const formRef = ref();
+const tableRef = ref();
+
+const {
+  form,
+  loading,
+  columns,
+  dataList,
+  pagination,
+  onSearch,
+  resetForm,
+  openDialog,
+  handleDelete,
+  openBindDialog,
+  bindDialogVisible,
+  currentReplenisher,
+  deviceLoading,
+  shopDeviceTree,
+  loadingShopTree,
+  selectedDeviceIds,
+  toggleDeviceSelect,
+  toggleShopSelectAll,
+  getShopSelectState,
+  handleConfirmBind,
+  handleSizeChange,
+  handleCurrentChange
+} = useReplenisher(tableRef);
+</script>
+
+<template>
+  <div class="main">
+    <el-form
+      ref="formRef"
+      :inline="true"
+      :model="form"
+      class="search-form bg-bg_color w-full pl-8 pt-[12px] overflow-auto"
+    >
+      <el-form-item label="关键词:" prop="keyword">
+        <el-input
+          v-model="form.keyword"
+          placeholder="搜索姓名/手机号/工号"
+          clearable
+          class="w-[200px]!"
+        />
+      </el-form-item>
+      <el-form-item label="状态:" prop="status">
+        <el-select
+          v-model="form.status"
+          placeholder="请选择状态"
+          clearable
+          class="w-[140px]!"
+        >
+          <el-option label="正常" :value="1" />
+          <el-option label="禁用" :value="0" />
+        </el-select>
+      </el-form-item>
+      <el-form-item>
+        <el-button
+          type="primary"
+          :icon="useRenderIcon('ri/search-line')"
+          :loading="loading"
+          @click="onSearch"
+        >
+          搜索
+        </el-button>
+        <el-button :icon="useRenderIcon(Refresh)" @click="resetForm(formRef)">
+          重置
+        </el-button>
+      </el-form-item>
+    </el-form>
+
+    <PureTableBar
+      title="补货员管理"
+      :columns="columns"
+      @refresh="onSearch"
+    >
+      <template #buttons>
+        <el-button
+          type="primary"
+          :icon="useRenderIcon(AddFill)"
+          @click="openDialog()"
+        >
+          新增补货员
+        </el-button>
+      </template>
+      <template v-slot="{ size, dynamicColumns }">
+        <pure-table
+          ref="tableRef"
+          row-key="id"
+          adaptive
+          :adaptiveConfig="{ offsetBottom: 108 }"
+          align-whole="center"
+          table-layout="auto"
+          :loading="loading"
+          :size="size"
+          :data="dataList"
+          :columns="dynamicColumns"
+          :pagination="{ ...pagination, size }"
+          :header-cell-style="{
+            background: 'var(--el-fill-color-light)',
+            color: 'var(--el-text-color-primary)'
+          }"
+          @page-size-change="handleSizeChange"
+          @page-current-change="handleCurrentChange"
+        >
+          <template #operation="{ row }">
+            <el-button
+              class="reset-margin"
+              link
+              type="primary"
+              :size="size"
+              :icon="useRenderIcon(Link)"
+              @click="openBindDialog(row)"
+            >
+              设备
+            </el-button>
+            <el-button
+              class="reset-margin"
+              link
+              type="primary"
+              :size="size"
+              :icon="useRenderIcon(EditPen)"
+              @click="openDialog('修改', row)"
+            >
+              修改
+            </el-button>
+            <el-popconfirm
+              :title="`是否确认删除补货员 ${row.name}?`"
+              @confirm="handleDelete(row)"
+            >
+              <template #reference>
+                <el-button
+                  class="reset-margin"
+                  link
+                  type="primary"
+                  :size="size"
+                  :icon="useRenderIcon(Delete)"
+                >
+                  删除
+                </el-button>
+              </template>
+            </el-popconfirm>
+          </template>
+        </pure-table>
+      </template>
+    </PureTableBar>
+
+    <!-- 设备绑定弹窗(门店设备树) -->
+    <el-dialog
+      v-model="bindDialogVisible"
+      :title="`设备绑定 - ${currentReplenisher?.name || ''}`"
+      width="600px"
+      draggable
+      :close-on-click-modal="false"
+    >
+      <div v-loading="loadingShopTree" class="bind-device-container">
+        <!-- 绑定状态摘要 -->
+        <div class="mb-3 px-1 text-sm text-gray-500 flex items-center gap-1">
+          已选择
+          <span class="font-bold text-[var(--el-color-primary)]">{{ selectedDeviceIds.size }}</span>
+          台设备
+          <el-divider direction="vertical" />
+          共 {{ shopDeviceTree.length }} 个门店
+        </div>
+
+        <!-- 门店设备列表 -->
+        <div v-if="shopDeviceTree.length === 0 && !loadingShopTree" class="py-8">
+          <el-empty description="暂无可用门店" />
+        </div>
+
+        <div v-for="shop in shopDeviceTree" :key="shop.id" class="mb-3 border rounded-lg overflow-hidden">
+          <!-- 门店头部 -->
+          <div
+            class="flex items-center px-3 py-2.5 cursor-pointer select-none transition-colors hover:bg-gray-50"
+            :class="{ 'border-b': shop.expanded }"
+            @click="shop.expanded = !shop.expanded"
+          >
+            <el-icon
+              class="mr-1.5 transition-transform duration-200 text-gray-400"
+              :class="{ 'rotate-90': shop.expanded }"
+            >
+              <ArrowRight />
+            </el-icon>
+            <span class="flex-1 text-sm font-medium truncate">{{ shop.name }}</span>
+            <span class="text-xs text-gray-400 mr-2">{{ shop.devices.length }}台设备</span>
+            <el-checkbox
+              :indeterminate="getShopSelectState(shop) === 'partial'"
+              :model-value="getShopSelectState(shop) === 'all'"
+              size="small"
+              @click.stop
+              @change="toggleShopSelectAll(shop)"
+            />
+          </div>
+          <!-- 门店下设备列表 -->
+          <div v-if="shop.expanded" class="divide-y divide-gray-50">
+            <label
+              v-for="device in shop.devices"
+              :key="device.deviceId"
+              class="flex items-center px-5 py-2.5 hover:bg-gray-50/50 cursor-pointer transition-colors"
+            >
+              <el-checkbox
+                :model-value="selectedDeviceIds.has(device.deviceId)"
+                size="small"
+                @change="toggleDeviceSelect(device.deviceId)"
+              />
+              <span class="ml-2.5 text-sm">{{ device.name || device.deviceId }}</span>
+              <span class="ml-1.5 text-xs text-gray-400">({{ device.deviceId }})</span>
+            </label>
+            <el-empty
+              v-if="shop.devices.length === 0"
+              description="该门店暂无设备"
+              :image-size="50"
+            />
+          </div>
+        </div>
+      </div>
+
+      <template #footer>
+        <el-button @click="bindDialogVisible = false">取消</el-button>
+        <el-button
+          type="primary"
+          :loading="deviceLoading"
+          @click="handleConfirmBind"
+        >
+          确认绑定
+        </el-button>
+      </template>
+    </el-dialog>
+  </div>
+</template>
+
+<style lang="scss" scoped>
+.main-content {
+  margin: 24px 24px 0 !important;
+}
+
+.search-form {
+  :deep(.el-form-item) {
+    margin-bottom: 12px;
+  }
+}
+
+.bind-device-container {
+  min-height: 200px;
+  max-height: 400px;
+  overflow-y: auto;
+}
+
+.device-item {
+  transition: background-color 0.2s;
+
+  &:hover {
+    background-color: var(--el-fill-color-darker);
+  }
+}
+</style>

+ 497 - 0
haha-admin-web/src/views/replenisher/utils/hook.tsx

@@ -0,0 +1,497 @@
+import dayjs from "dayjs";
+import { message } from "@/utils/message";
+import { addDialog } from "@/components/ReDialog";
+import { usePublicHooks } from "@/views/system/hooks";
+import type { PaginationProps } from "@pureadmin/table";
+import { deviceDetection } from "@pureadmin/utils";
+import {
+  getReplenisherList,
+  createReplenisher,
+  updateReplenisher,
+  updateReplenisherStatus,
+  deleteReplenisher,
+  getBoundDevices,
+  bindDevices,
+  unbindDevice
+} from "@/api/replenisher";
+import { getEnabledShops, getShopDevices } from "@/api/shop";
+import {
+  type Ref,
+  h,
+  ref,
+  toRaw,
+  reactive,
+  onMounted
+} from "vue";
+import {
+  ElForm,
+  ElInput,
+  ElFormItem,
+  ElSelect,
+  ElOption,
+  ElRadioGroup,
+  ElRadioButton,
+  ElButton,
+  ElTag,
+  ElMessageBox,
+  ElPopconfirm
+} from "element-plus";
+import type { ReplenisherFormItem, ReplenisherSearchForm, ShopWithDevices } from "./types";
+
+export function useReplenisher(tableRef: Ref) {
+  const form = reactive<ReplenisherSearchForm>({
+    keyword: "",
+    status: ""
+  });
+  const dialogForm = reactive<any>({
+    id: undefined,
+    name: "",
+    phone: "",
+    employeeId: "",
+    status: 1
+  });
+  const formRef = ref();
+  const ruleFormRef = ref();
+  const dataList = ref([]);
+  const loading = ref(true);
+  const switchLoadMap = ref({});
+  const { switchStyle } = usePublicHooks();
+  const pagination = reactive<PaginationProps>({
+    total: 0,
+    pageSize: 10,
+    currentPage: 1,
+    background: true
+  });
+
+  const columns: TableColumnList = [
+    {
+      label: "补货员ID",
+      prop: "id",
+      width: 90
+    },
+    {
+      label: "姓名",
+      prop: "name",
+      minWidth: 100
+    },
+    {
+      label: "手机号",
+      prop: "phone",
+      minWidth: 130
+    },
+    {
+      label: "工号",
+      prop: "employeeId",
+      minWidth: 100
+    },
+    {
+      label: "绑定设备数",
+      prop: "boundDeviceCount",
+      width: 110
+    },
+    {
+      label: "状态",
+      prop: "status",
+      minWidth: 100,
+      cellRenderer: scope => (
+        <el-switch
+          size={scope.props.size === "small" ? "small" : "default"}
+          loading={switchLoadMap.value[scope.index]?.loading}
+          v-model={scope.row.status}
+          active-value={1}
+          inactive-value={0}
+          active-text="正常"
+          inactive-text="禁用"
+          inline-prompt
+          style={switchStyle.value}
+          onChange={() => onChange(scope as any)}
+        />
+      )
+    },
+    {
+      label: "累计任务",
+      prop: "totalTasks",
+      width: 100
+    },
+    {
+      label: "最后任务时间",
+      prop: "lastTaskTime",
+      minWidth: 160,
+      formatter: ({ lastTaskTime }) =>
+        lastTaskTime ? dayjs(lastTaskTime).format("YYYY-MM-DD HH:mm:ss") : "-"
+    },
+    {
+      label: "创建时间",
+      prop: "createTime",
+      minWidth: 160,
+      formatter: ({ createTime }) =>
+        createTime ? dayjs(createTime).format("YYYY-MM-DD HH:mm:ss") : "-"
+    },
+    {
+      label: "操作",
+      fixed: "right",
+      width: 220,
+      slot: "operation"
+    }
+  ];
+
+  // 状态切换
+  function onChange({ row, index }) {
+    ElMessageBox.confirm(
+      `确认要<strong>${row.status === 0 ? "禁用" : "启用"}</strong>补货员<strong style='color:var(--el-color-primary)'>${row.name}</strong>吗?`,
+      "系统提示",
+      {
+        confirmButtonText: "确定",
+        cancelButtonText: "取消",
+        type: "warning",
+        dangerouslyUseHTMLString: true,
+        draggable: true
+      }
+    )
+      .then(async () => {
+        switchLoadMap.value[index] = Object.assign({}, switchLoadMap.value[index], {
+          loading: true
+        });
+        try {
+          const res = await updateReplenisherStatus(row.id, { status: row.status });
+          if (res.code === 200) {
+            message(`已${row.status === 0 ? "禁用" : "启用"}补货员 ${row.name}`, {
+              type: "success"
+            });
+          } else {
+            row.status = row.status === 0 ? 1 : 0;
+            message(res.message || "操作失败", { type: "error" });
+          }
+        } catch {
+          row.status = row.status === 0 ? 1 : 0;
+          message("操作失败", { type: "error" });
+        } finally {
+          setTimeout(() => {
+            switchLoadMap.value[index] = Object.assign({}, switchLoadMap.value[index], {
+              loading: false
+            });
+          }, 300);
+        }
+      })
+      .catch(() => {
+        row.status = row.status === 0 ? 1 : 0;
+      });
+  }
+
+  // 删除
+  async function handleDelete(row: ReplenisherFormItem) {
+    try {
+      const res = await deleteReplenisher(row.id);
+      if (res.code === 200) {
+        message(`已成功删除补货员 ${row.name}`, { type: "success" });
+        onSearch();
+      } else {
+        message(res.message || "删除失败", { type: "error" });
+      }
+    } catch (error) {
+      message("删除失败", { type: "error" });
+    }
+  }
+
+  // 设备绑定管理(门店设备树)
+  const bindDialogVisible = ref(false);
+  const currentReplenisher = ref<ReplenisherFormItem>(null);
+  const deviceLoading = ref(false);
+  const shopDeviceTree = ref<ShopWithDevices[]>([]);
+  const loadingShopTree = ref(false);
+  const selectedDeviceIds = ref<Set<string>>(new Set());
+  const origBoundDeviceIds = ref<Set<string>>(new Set());
+
+  async function openBindDialog(row: ReplenisherFormItem) {
+    currentReplenisher.value = row;
+    bindDialogVisible.value = true;
+    loadingShopTree.value = true;
+
+    try {
+      // 并行获取所有启用的门店 + 已绑定的设备
+      const [shopsRes, boundRes] = await Promise.all([
+        getEnabledShops(),
+        getBoundDevices(row.id)
+      ]);
+
+      const shops: any[] = shopsRes.data || [];
+      const boundDevices: string[] = boundRes.data || [];
+      selectedDeviceIds.value = new Set(boundDevices);
+      origBoundDeviceIds.value = new Set(boundDevices);
+
+      // 并行获取每个门店的设备
+      const shopPromises = shops.map(async (shop: any) => {
+        try {
+          const devRes = await getShopDevices(shop.id);
+          const devices: any[] = devRes.data || [];
+          return {
+            id: shop.id,
+            name: shop.name,
+            address: shop.address || "",
+            devices: devices.map((d: any) => ({
+              deviceId: d.deviceId,
+              name: d.name
+            })),
+            expanded: false,
+            loading: false
+          } as ShopWithDevices;
+        } catch {
+          return {
+            id: shop.id,
+            name: shop.name,
+            address: shop.address || "",
+            devices: [],
+            expanded: false,
+            loading: false
+          } as ShopWithDevices;
+        }
+      });
+
+      shopDeviceTree.value = await Promise.all(shopPromises);
+    } catch (error) {
+      console.error("加载门店设备数据失败:", error);
+      message("加载数据失败", { type: "error" });
+    } finally {
+      loadingShopTree.value = false;
+    }
+  }
+
+  function toggleDeviceSelect(deviceId: string) {
+    const newSet = new Set(selectedDeviceIds.value);
+    if (newSet.has(deviceId)) {
+      newSet.delete(deviceId);
+    } else {
+      newSet.add(deviceId);
+    }
+    selectedDeviceIds.value = newSet;
+  }
+
+  function toggleShopSelectAll(shop: ShopWithDevices) {
+    const newSet = new Set(selectedDeviceIds.value);
+    const allSelected = shop.devices.every(d => newSet.has(d.deviceId));
+    if (allSelected) {
+      shop.devices.forEach(d => newSet.delete(d.deviceId));
+    } else {
+      shop.devices.forEach(d => newSet.add(d.deviceId));
+    }
+    selectedDeviceIds.value = newSet;
+  }
+
+  function getShopSelectState(shop: ShopWithDevices): "all" | "partial" | "none" {
+    if (shop.devices.length === 0) return "none";
+    const selected = shop.devices.filter(d => selectedDeviceIds.value.has(d.deviceId)).length;
+    if (selected === shop.devices.length) return "all";
+    if (selected > 0) return "partial";
+    return "none";
+  }
+
+  async function handleConfirmBind() {
+    if (!currentReplenisher.value) return;
+
+    const origSet = origBoundDeviceIds.value;
+    const newSet = selectedDeviceIds.value;
+
+    const toBind = [...newSet].filter(id => !origSet.has(id));
+    const toUnbind = [...origSet].filter(id => !newSet.has(id));
+
+    if (toBind.length === 0 && toUnbind.length === 0) {
+      message("未做任何变更", { type: "info" });
+      return;
+    }
+
+    deviceLoading.value = true;
+    try {
+      if (toBind.length > 0) {
+        const res = await bindDevices(currentReplenisher.value.id, { deviceIds: toBind });
+        if (res.code !== 200) {
+          message("绑定设备失败: " + (res.message || ""), { type: "error" });
+          return;
+        }
+      }
+
+      if (toUnbind.length > 0) {
+        await Promise.all(toUnbind.map(deviceId =>
+          unbindDevice(currentReplenisher.value.id, deviceId)
+        ));
+      }
+
+      message(`设备绑定更新完成(绑定${toBind.length}台,解绑${toUnbind.length}台)`, { type: "success" });
+      bindDialogVisible.value = false;
+      onSearch();
+    } catch (error) {
+      message("操作失败", { type: "error" });
+    } finally {
+      deviceLoading.value = false;
+    }
+  }
+
+  // 分页
+  function handleSizeChange(val: number) {
+    pagination.pageSize = val;
+    onSearch();
+  }
+
+  function handleCurrentChange(val: number) {
+    pagination.currentPage = val;
+    onSearch();
+  }
+
+  // 搜索
+  async function onSearch() {
+    loading.value = true;
+    try {
+      const searchParams: any = {
+        page: pagination.currentPage,
+        pageSize: pagination.pageSize
+      };
+      if (form.keyword) searchParams.keyword = form.keyword;
+      if (form.status) searchParams.status = form.status;
+
+      const { data } = await getReplenisherList(searchParams);
+      if (data) {
+        dataList.value = data.list || [];
+        pagination.total = Number(data.total) || 0;
+      }
+    } catch (error) {
+      console.error("获取补货员列表失败:", error);
+    } finally {
+      setTimeout(() => {
+        loading.value = false;
+      }, 300);
+    }
+  }
+
+  // 重置表单
+  const resetForm = formEl => {
+    if (!formEl) return;
+    formEl.resetFields();
+    pagination.currentPage = 1;
+    onSearch();
+  };
+
+  // 打开弹窗
+  function openDialog(title = "新增", row?: ReplenisherFormItem) {
+    // 初始化对话框表单数据
+    dialogForm.id = row?.id;
+    dialogForm.name = row?.name ?? "";
+    dialogForm.phone = row?.phone ?? "";
+    dialogForm.employeeId = row?.employeeId ?? "";
+    dialogForm.status = row?.status ?? 1;
+    addDialog({
+      title: `${title}补货员`,
+      props: {
+        formInline: {
+          id: row?.id,
+          name: row?.name ?? "",
+          phone: row?.phone ?? "",
+          employeeId: row?.employeeId ?? "",
+          status: row?.status ?? 1
+        }
+      },
+      width: "40%",
+      draggable: true,
+      fullscreen: deviceDetection(),
+      fullscreenIcon: true,
+      closeOnClickModal: false,
+      contentRenderer: () => (
+        <ElForm ref={ruleFormRef} model={dialogForm} label-width="100px">
+          <ElFormItem
+            label="姓名"
+            prop="name"
+            rules={[{ required: true, message: "请输入补货员姓名", trigger: "blur" }]}
+          >
+            <ElInput
+              v-model={dialogForm.name}
+              placeholder="请输入补货员姓名"
+              clearable
+            />
+          </ElFormItem>
+          <ElFormItem
+            label="手机号"
+            prop="phone"
+          >
+            <ElInput v-model={dialogForm.phone} placeholder="请输入手机号" clearable />
+          </ElFormItem>
+          <ElFormItem
+            label="工号"
+            prop="employeeId"
+          >
+            <ElInput v-model={dialogForm.employeeId} placeholder="请输入工号" clearable />
+          </ElFormItem>
+          <ElFormItem label="状态" prop="status">
+            <ElRadioGroup v-model={dialogForm.status}>
+              <ElRadioButton value={1}>正常</ElRadioButton>
+              <ElRadioButton value={0}>禁用</ElRadioButton>
+            </ElRadioGroup>
+          </ElFormItem>
+        </ElForm>
+      ),
+      beforeSure: async (done) => {
+        const valid = await ruleFormRef.value.validate().catch(() => false);
+        if (!valid) return;
+
+        try {
+          if (title === "新增") {
+            const res = await createReplenisher({
+              name: dialogForm.name,
+              phone: dialogForm.phone || undefined,
+              employeeId: dialogForm.employeeId || undefined
+            });
+            if (res.code === 200) {
+              message(`新增补货员 ${dialogForm.name} 成功`, { type: "success" });
+              done();
+              onSearch();
+            } else {
+              message(res.message || "新增失败", { type: "error" });
+            }
+          } else {
+            const res = await updateReplenisher(row.id, {
+              name: dialogForm.name,
+              phone: dialogForm.phone || undefined,
+              employeeId: dialogForm.employeeId || undefined,
+              status: dialogForm.status
+            });
+            if (res.code === 200) {
+              message(`修改补货员 ${dialogForm.name} 成功`, { type: "success" });
+              done();
+              onSearch();
+            } else {
+              message(res.message || "修改失败", { type: "error" });
+            }
+          }
+        } catch (error) {
+          message("操作失败", { type: "error" });
+        }
+      }
+    });
+  }
+
+  onMounted(() => {
+    onSearch();
+  });
+
+  return {
+    form,
+    loading,
+    columns,
+    dataList,
+    pagination,
+    onSearch,
+    resetForm,
+    openDialog,
+    handleDelete,
+    openBindDialog,
+    bindDialogVisible,
+    currentReplenisher,
+    deviceLoading,
+    shopDeviceTree,
+    loadingShopTree,
+    selectedDeviceIds,
+    toggleDeviceSelect,
+    toggleShopSelectAll,
+    getShopSelectState,
+    handleConfirmBind,
+    handleSizeChange,
+    handleCurrentChange
+  };
+}

+ 36 - 0
haha-admin-web/src/views/replenisher/utils/types.ts

@@ -0,0 +1,36 @@
+// 补货员表单项类型
+export interface ReplenisherFormItem {
+  id?: number;
+  name: string;
+  phone?: string;
+  employeeId?: string;
+  status: number;
+  totalTasks?: number;
+  boundDeviceCount?: number;
+  lastTaskTime?: string;
+  createTime?: string;
+  statusLabel?: string;
+  statusColor?: string;
+}
+
+// 搜索表单类型
+export interface ReplenisherSearchForm {
+  keyword: string;
+  status: number | string;
+}
+
+// 门店下设备项(含绑定状态)
+export interface ShopDeviceItem {
+  deviceId: string;
+  name: string;
+}
+
+// 门店设备树节点
+export interface ShopWithDevices {
+  id: number;
+  name: string;
+  address: string;
+  devices: ShopDeviceItem[];
+  expanded: boolean;
+  loading: boolean;
+}

+ 130 - 38
haha-admin-web/src/views/shop/utils/hook.tsx

@@ -14,6 +14,8 @@ import {
   toggleShopStatus,
   getShopDevices,
   getShopReplenishers,
+  getShopReplenishersV2,
+  addReplenisherV2,
   linkDevice,
   unlinkDevice,
   addReplenisher,
@@ -22,6 +24,7 @@ import {
 } from "@/api/shop";
 import { getAllRoles } from "@/api/role";
 import { getAdminList } from "@/api/admin";
+import { searchReplenishers, unbindDevice } from "@/api/replenisher";
 import { type Ref, ref, toRaw, reactive, onMounted, computed } from "vue";
 import {
   ElForm,
@@ -141,7 +144,6 @@ export function useShop(tableRef: Ref) {
       
       // 只添加非空的搜索条件
       if (form.name) searchParams.name = form.name;
-      if (form.code) searchParams.code = form.code;
       if (form.status !== undefined) searchParams.status = form.status;
       
       const { data } = await getShopList(searchParams);
@@ -648,52 +650,142 @@ export function useShop(tableRef: Ref) {
     });
   }
 
-  // 补货员管理
+  // 补货员管理 V2
   async function handleReplenishers(row: ShopItem) {
-    const replenishersRes = await getShopReplenishers(row.id);
-    const currentReplenishers = replenishersRes.data || [];
+    const replenishersRes = await getShopReplenishersV2(row.id);
+    const currentReplenishers = ref<any[]>(replenishersRes.data || []);
+    const searchKeyword = ref("");
+    const searchResults = ref<any[]>([]);
+    const searchLoading = ref(false);
+
+    async function doSearch() {
+      const keyword = searchKeyword.value.trim();
+      if (!keyword) {
+        message("请输入搜索关键词", { type: "warning" });
+        return;
+      }
+      searchLoading.value = true;
+      try {
+        const res = await searchReplenishers(keyword);
+        if (res.code === 0 || res.code === 200) {
+          searchResults.value = res.data || [];
+        }
+      } catch {
+        searchResults.value = [];
+      } finally {
+        searchLoading.value = false;
+      }
+    }
 
-    const adminsRes = await getAdminList({ page: 1, pageSize: 1000 });
-    const allAdmins = adminsRes.data?.list || [];
+    async function doAdd(replenisher: any) {
+      try {
+        const res = await addReplenisherV2(row.id, { replenisherId: replenisher.id });
+        if (res.code === 0 || res.code === 200) {
+          message(`已添加补货员 ${replenisher.name}`, { type: "success" });
+          const updateRes = await getShopReplenishersV2(row.id);
+          currentReplenishers.value = updateRes.data || [];
+          searchKeyword.value = "";
+          searchResults.value = [];
+        } else {
+          message(res.message || "操作失败", { type: "error" });
+        }
+      } catch {
+        message("操作失败", { type: "error" });
+      }
+    }
 
-    const selectedAdminIds = ref(currentReplenishers.map(r => r.adminId));
+    async function doRemove(replenisher: any) {
+      try {
+        // 获取门店所有设备并逐个解绑
+        const devicesRes = await getShopDevices(row.id);
+        const devices = devicesRes.data || [];
+        if (devices.length === 0) {
+          message("该门店下无设备", { type: "warning" });
+          return;
+        }
+        for (const device of devices) {
+          await unbindDevice(replenisher.id, device.deviceId);
+        }
+        message(`已移除补货员 ${replenisher.name}`, { type: "success" });
+        const updateRes = await getShopReplenishersV2(row.id);
+        currentReplenishers.value = updateRes.data || [];
+      } catch {
+        message("操作失败", { type: "error" });
+      }
+    }
 
     addDialog({
-      title: `管理 ${row.name} 的补货员`,
-      width: "60%",
+      title: `管理 ${row.name} 的补货员(V2)`,
+      width: "500px",
       draggable: true,
       closeOnClickModal: false,
+      hideFooter: true,
       contentRenderer: () => (
-        <div>
-          <el-transfer
-            v-model={selectedAdminIds.value}
-            data={allAdmins}
-            titles={["可选补货员", "已分配补货员"]}
-            props={{
-              key: "id",
-              label: "nickname"
-            }}
-          />
+        <div class="p-2">
+          <div class="text-sm font-medium mb-2 text-gray-600">当前绑定的补货员:</div>
+          {currentReplenishers.value.length === 0 ? (
+            <el-empty description="暂无绑定补货员" />
+          ) : (
+            <div class="space-y-2 mb-4">
+              {currentReplenishers.value.map((r: any) => (
+                <div class="flex items-center justify-between px-3 py-2 rounded bg-[var(--el-fill-color-light)]">
+                  <div class="flex items-center gap-2">
+                    <span class="text-sm font-medium">{r.name}</span>
+                    {r.phone ? <span class="text-xs text-gray-400">{r.phone}</span> : null}
+                    {r.employeeId ? <el-tag size="small" type="info">{r.employeeId}</el-tag> : null}
+                  </div>
+                  <el-button
+                    type="danger"
+                    link
+                    size="small"
+                    loading={false}
+                    onClick={() => doRemove(r)}
+                  >
+                    移除
+                  </el-button>
+                </div>
+              ))}
+            </div>
+          )}
+          <el-divider />
+          <div class="text-sm font-medium mb-2 text-gray-600">搜索并添加补货员:</div>
+          <div class="flex gap-2 mb-2">
+            <el-input
+              v-model={searchKeyword.value}
+              placeholder="输入姓名/手机号/工号搜索"
+              clearable
+              class="flex-1"
+              onKeyupEnter={() => doSearch()}
+            />
+            <el-button type="primary" loading={searchLoading.value} onClick={() => doSearch()}>
+              搜索
+            </el-button>
+          </div>
+          {searchResults.value.length > 0 ? (
+            <div>
+              {searchResults.value.map((r: any) => (
+                <div class="flex items-center justify-between px-3 py-2 mb-1 rounded bg-[var(--el-color-primary-light-9)]">
+                  <div class="flex items-center gap-2">
+                    <span class="text-sm">{r.name}</span>
+                    {r.phone ? <span class="text-xs text-gray-400">{r.phone}</span> : null}
+                  </div>
+                  <el-button
+                    type="primary"
+                    link
+                    size="small"
+                    disabled={currentReplenishers.value.some(b => b.id === r.id)}
+                    onClick={() => doAdd(r)}
+                  >
+                    {currentReplenishers.value.some(b => b.id === r.id) ? "已绑定" : "添加"}
+                  </el-button>
+                </div>
+              ))}
+            </div>
+          ) : searchKeyword.value && !searchLoading.value ? (
+            <div class="text-xs text-gray-400 text-center py-2">请输入关键词搜索补货员</div>
+          ) : null}
         </div>
-      ),
-      beforeSure: async (done, { closeLoading }) => {
-        try {
-          // 先移除所有补货员
-          for (const r of currentReplenishers) {
-            await removeReplenisher(row.id, r.adminId);
-          }
-          // 再添加新补货员
-          for (const adminId of selectedAdminIds.value) {
-            await addReplenisher(row.id, { adminId });
-          }
-          message("补货员分配更新成功", { type: "success" });
-          done();
-          onSearch();
-        } catch (error) {
-          closeLoading();
-          message("操作失败", { type: "error" });
-        }
-      }
+      )
     });
   }
 

+ 49 - 0
haha-admin/src/main/java/com/haha/admin/controller/DeviceController.java

@@ -4,6 +4,7 @@ import com.baomidou.mybatisplus.core.metadata.IPage;
 import com.haha.admin.annotation.RequirePermission;
 import com.haha.entity.dto.DeviceQueryDTO;
 import com.haha.entity.dto.DoorOpenDTO;
+import com.haha.entity.dto.ReplenisherBindDTO;
 import com.haha.entity.dto.TemperatureDTO;
 import com.haha.entity.dto.VolumeDTO;
 import com.haha.common.annotation.Log;
@@ -11,9 +12,11 @@ import com.haha.common.enums.OperationType;
 import com.haha.common.vo.PageResult;
 import com.haha.common.vo.Result;
 import com.haha.entity.Device;
+import com.haha.entity.Replenisher;
 import com.haha.service.DeviceService;
 import com.haha.service.DoorRecordService;
 import com.haha.service.LayerTemplateService;
+import com.haha.service.ReplenisherService;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.web.bind.annotation.*;
@@ -33,6 +36,7 @@ public class DeviceController {
     private final DeviceService deviceService;
     private final DoorRecordService doorRecordService;
     private final LayerTemplateService layerTemplateService;
+    private final ReplenisherService replenisherService;
 
     /**
      * 分页查询设备列表
@@ -166,4 +170,49 @@ public class DeviceController {
         layerTemplateService.saveProductConfig(deviceId, floors);
         return Result.success("保存成功", null);
     }
+
+    // ==================== 补货员管理 ====================
+
+    /**
+     * 获取设备绑定的补货员列表
+     *
+     * @param deviceId 设备SN号
+     * @return 补货员列表
+     */
+    @RequirePermission("device:read")
+    @GetMapping("/{deviceId}/replenishers")
+    public Result<List<Replenisher>> getReplenishers(@PathVariable String deviceId) {
+        List<Replenisher> replenishers = replenisherService.getReplenishersByDeviceId(deviceId);
+        return Result.success("查询成功", replenishers);
+    }
+
+    /**
+     * 绑定补货员到设备
+     *
+     * @param deviceId 设备SN号
+     * @param dto      补货员ID
+     * @return 操作结果
+     */
+    @RequirePermission("device:update")
+    @Log(module = "设备管理", operation = OperationType.UPDATE, summary = "绑定补货员到设备")
+    @PostMapping("/{deviceId}/replenishers")
+    public Result<Void> bindReplenisher(@PathVariable String deviceId, @RequestBody ReplenisherBindDTO dto) {
+        replenisherService.bindDevices(dto.getReplenisherId(), List.of(deviceId));
+        return Result.success("绑定成功");
+    }
+
+    /**
+     * 解绑设备的补货员
+     *
+     * @param deviceId      设备SN号
+     * @param replenisherId 补货员ID
+     * @return 操作结果
+     */
+    @RequirePermission("device:update")
+    @Log(module = "设备管理", operation = OperationType.UPDATE, summary = "解绑设备补货员")
+    @DeleteMapping("/{deviceId}/replenishers/{replenisherId}")
+    public Result<Void> unbindReplenisher(@PathVariable String deviceId, @PathVariable Long replenisherId) {
+        replenisherService.unbindDevice(replenisherId, deviceId);
+        return Result.success("解绑成功");
+    }
 }

+ 14 - 0
haha-admin/src/main/java/com/haha/admin/controller/InventoryController.java

@@ -208,6 +208,20 @@ public class InventoryController {
         return Result.success("上货记录创建成功", record);
     }
 
+    /**
+     * 创建上货记录(V2:支持独立补货员体系)
+     */
+    @RequirePermission("inventory:create")
+    @Log(module = "库存管理", operation = OperationType.INSERT, summary = "V2创建上货记录")
+    @PostMapping("/records/v2")
+    public Result<StockRecord> createRecordV2(@RequestBody @Validated StockRecordCreateV2DTO dto) {
+        StockRecord record = stockRecordService.createRecordV2(
+                dto.getDeviceId(), dto.getStockType(), dto.getReplenisherId(),
+                dto.getReplenisherName(), dto.getReplenisherPhone(), dto.getRemark());
+
+        return Result.success("上货记录创建成功", record);
+    }
+
     /**
      * 完成上货记录
      */

+ 186 - 0
haha-admin/src/main/java/com/haha/admin/controller/ReplenisherController.java

@@ -0,0 +1,186 @@
+package com.haha.admin.controller;
+
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.haha.admin.annotation.RequirePermission;
+import com.haha.common.annotation.Log;
+import com.haha.common.enums.OperationType;
+import com.haha.common.vo.PageResult;
+import com.haha.common.vo.Result;
+import com.haha.entity.Replenisher;
+import com.haha.entity.dto.*;
+import com.haha.service.ReplenisherService;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+/**
+ * 补货员管理控制器
+ */
+@Slf4j
+@RestController
+@RequestMapping("/replenishers")
+@RequiredArgsConstructor
+public class ReplenisherController {
+
+    private final ReplenisherService replenisherService;
+
+    /**
+     * 分页查询补货员列表
+     *
+     * @param queryDTO 查询参数(支持关键词模糊搜索和状态筛选)
+     * @return 补货员分页列表
+     */
+    @RequirePermission("replenisher:read")
+    @GetMapping("/list")
+    public Result<PageResult<Replenisher>> list(ReplenisherQueryDTO queryDTO) {
+        IPage<Replenisher> page = replenisherService.getPage(queryDTO);
+        return Result.success("查询成功", PageResult.of(page));
+    }
+
+    /**
+     * 获取补货员详情(含绑定设备数)
+     *
+     * @param id 补货员ID
+     * @return 补货员详情
+     */
+    @RequirePermission("replenisher:read")
+    @GetMapping("/{id}")
+    public Result<Replenisher> getById(@PathVariable Long id) {
+        Replenisher replenisher = replenisherService.getReplenisherDetail(id);
+        if (replenisher == null) {
+            return Result.error(404, "补货员不存在");
+        }
+        return Result.success("查询成功", replenisher);
+    }
+
+    /**
+     * 关键词搜索启用的补货员(用于下拉选择)
+     *
+     * @param keyword 搜索关键词(可选,搜索姓名/手机号/工号)
+     * @return 启用的补货员列表
+     */
+    @RequirePermission("replenisher:read")
+    @GetMapping("/search")
+    public Result<List<Replenisher>> search(@RequestParam(required = false) String keyword) {
+        List<Replenisher> list = replenisherService.searchByKeyword(keyword);
+        return Result.success("查询成功", list);
+    }
+
+    /**
+     * 创建补货员
+     *
+     * @param dto 创建参数
+     * @return 创建的补货员
+     */
+    @RequirePermission("replenisher:create")
+    @Log(module = "补货员管理", operation = OperationType.INSERT, summary = "创建补货员")
+    @PostMapping
+    public Result<Replenisher> create(@RequestBody ReplenisherCreateDTO dto) {
+        Replenisher replenisher = replenisherService.createReplenisher(dto);
+        return Result.success("创建成功", replenisher);
+    }
+
+    /**
+     * 更新补货员信息
+     *
+     * @param id  补货员ID
+     * @param dto 更新参数
+     * @return 更新后的补货员
+     */
+    @RequirePermission("replenisher:update")
+    @Log(module = "补货员管理", operation = OperationType.UPDATE, summary = "更新补货员")
+    @PutMapping("/{id}")
+    public Result<Replenisher> update(@PathVariable Long id, @RequestBody ReplenisherUpdateDTO dto) {
+        Replenisher replenisher = replenisherService.updateReplenisher(id, dto);
+        return Result.success("更新成功", replenisher);
+    }
+
+    /**
+     * 启用/禁用补货员
+     *
+     * @param id  补货员ID
+     * @param dto 状态参数
+     * @return 操作结果
+     */
+    @RequirePermission("replenisher:update")
+    @Log(module = "补货员管理", operation = OperationType.UPDATE, summary = "更新补货员状态")
+    @PutMapping("/{id}/status")
+    public Result<Void> updateStatus(@PathVariable Long id, @RequestBody StatusDTO dto) {
+        replenisherService.updateStatus(id, dto.getStatus());
+        return Result.success("状态更新成功");
+    }
+
+    /**
+     * 删除补货员(级联解绑所有设备)
+     *
+     * @param id 补货员ID
+     * @return 操作结果
+     */
+    @RequirePermission("replenisher:delete")
+    @Log(module = "补货员管理", operation = OperationType.DELETE, summary = "删除补货员")
+    @DeleteMapping("/{id}")
+    public Result<Void> delete(@PathVariable Long id) {
+        replenisherService.deleteReplenisher(id);
+        return Result.success("删除成功");
+    }
+
+    /**
+     * 获取补货员已绑定的设备ID列表
+     *
+     * @param id 补货员ID
+     * @return 设备SN号列表
+     */
+    @RequirePermission("replenisher:read")
+    @GetMapping("/{id}/devices")
+    public Result<List<String>> getBoundDevices(@PathVariable Long id) {
+        List<String> deviceIds = replenisherService.getBoundDeviceIds(id);
+        return Result.success("查询成功", deviceIds);
+    }
+
+    /**
+     * 绑定设备到补货员
+     *
+     * @param id  补货员ID
+     * @param dto 设备ID列表
+     * @return 绑定结果
+     */
+    @RequirePermission("replenisher:update")
+    @Log(module = "补货员管理", operation = OperationType.UPDATE, summary = "绑定设备到补货员")
+    @PostMapping("/{id}/devices")
+    public Result<Object> bindDevices(@PathVariable Long id, @RequestBody BindDeviceDTO dto) {
+        int count = replenisherService.bindDevices(id, dto.getDeviceIds());
+        return Result.success("绑定完成", java.util.Map.of("success", count, "total", dto.getDeviceIds().size()));
+    }
+
+    /**
+     * 解绑补货员的某个设备
+     *
+     * @param id       补货员ID
+     * @param deviceId 设备SN号
+     * @return 操作结果
+     */
+    @RequirePermission("replenisher:update")
+    @Log(module = "补货员管理", operation = OperationType.UPDATE, summary = "解绑补货员设备")
+    @DeleteMapping("/{id}/devices/{deviceId}")
+    public Result<Void> unbindDevice(@PathVariable Long id, @PathVariable String deviceId) {
+        replenisherService.unbindDevice(id, deviceId);
+        return Result.success("解绑成功");
+    }
+
+    /**
+     * 批量绑定(多补货员到多设备)
+     *
+     * @param dto 补货员ID列表和设备ID列表
+     * @return 绑定结果
+     */
+    @RequirePermission("replenisher:update")
+    @Log(module = "补货员管理", operation = OperationType.UPDATE, summary = "批量绑定补货员到设备")
+    @PostMapping("/batch-bind")
+    public Result<Object> batchBind(@RequestBody BatchBindDevicesDTO dto) {
+        int count = replenisherService.batchBindDevices(dto.getReplenisherIds(), dto.getDeviceIds());
+        int total = dto.getReplenisherIds().size() * dto.getDeviceIds().size();
+        return Result.success("批量绑定完成", java.util.Map.of("success", count, "total", total));
+    }
+}

+ 31 - 0
haha-admin/src/main/java/com/haha/admin/controller/ShopController.java

@@ -10,6 +10,7 @@ import com.haha.common.vo.PageResult;
 import com.haha.common.vo.Result;
 import com.haha.entity.Shop;
 import com.haha.entity.Device;
+import com.haha.entity.Replenisher;
 import com.haha.entity.ShopReplenisher;
 import com.haha.entity.Admin;
 import com.haha.service.ShopService;
@@ -363,4 +364,34 @@ public class ShopController {
         List<Admin> admins = adminService.getAvailableAdmins(keyword);
         return Result.success("查询成功", admins);
     }
+
+    // ==================== V2 补货员管理(独立补货员体系) ====================
+
+    /**
+     * V2:获取门店关联的补货员列表(通过设备绑定反向推导)
+     *
+     * @param id 门店ID
+     * @return 补货员列表(来自独立补货员表)
+     */
+    @RequirePermission("shop:read")
+    @GetMapping("/{id}/replenishers/v2")
+    public Result<List<Replenisher>> getReplenishersV2(@PathVariable Long id) {
+        List<Replenisher> replenishers = shopReplenisherService.getReplenishersByShopIdV2(id);
+        return Result.success("查询成功", replenishers);
+    }
+
+    /**
+     * V2:将独立补货员绑定到门店下的所有设备
+     *
+     * @param id  门店ID
+     * @param dto 补货员ID
+     * @return 操作结果
+     */
+    @RequirePermission("shop:update")
+    @Log(module = "门店管理", operation = OperationType.INSERT, summary = "V2绑定补货员到门店")
+    @PostMapping("/{id}/replenishers/v2")
+    public Result<Void> addReplenisherV2(@PathVariable Long id, @RequestBody ReplenisherBindDTO dto) {
+        shopReplenisherService.addReplenisherToShop(id, dto.getReplenisherId());
+        return Result.success("绑定成功");
+    }
 }

+ 95 - 0
haha-entity/src/main/java/com/haha/entity/Replenisher.java

@@ -0,0 +1,95 @@
+package com.haha.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fasterxml.jackson.databind.annotation.JsonSerialize;
+import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.time.LocalDateTime;
+
+/**
+ * 补货员实体
+ * 独立于运营管理员(t_admin)的补货员账号体系
+ */
+@Data
+@TableName("t_replenisher")
+public class Replenisher implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    @TableId(type = IdType.ASSIGN_ID)
+    @JsonSerialize(using = ToStringSerializer.class)
+    private Long id;
+
+    /**
+     * 姓名
+     */
+    private String name;
+
+    /**
+     * 手机号
+     */
+    private String phone;
+
+    /**
+     * 工号
+     */
+    private String employeeId;
+
+    /**
+     * 微信OpenID(扫码绑定时回填)
+     */
+    private String wechatOpenid;
+
+    /**
+     * 头像
+     */
+    private String avatar;
+
+    /**
+     * 状态:1-启用,0-禁用
+     */
+    private Integer status;
+
+    /**
+     * 累计任务数
+     */
+    private Integer totalTasks;
+
+    /**
+     * 最后任务时间
+     */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai")
+    private LocalDateTime lastTaskTime;
+
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai")
+    private LocalDateTime createTime;
+
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai")
+    private LocalDateTime updateTime;
+
+    // ========== 以下为非数据库字段,用于前端展示 ==========
+
+    /**
+     * 绑定的设备数量
+     */
+    @TableField(exist = false)
+    private Integer boundDeviceCount;
+
+    /**
+     * 状态标签
+     */
+    @TableField(exist = false)
+    private String statusLabel;
+
+    /**
+     * 状态颜色
+     */
+    @TableField(exist = false)
+    private String statusColor;
+}

+ 46 - 0
haha-entity/src/main/java/com/haha/entity/ReplenisherDevice.java

@@ -0,0 +1,46 @@
+package com.haha.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fasterxml.jackson.databind.annotation.JsonSerialize;
+import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.time.LocalDateTime;
+
+/**
+ * 补货员-设备关联实体
+ * 支持设备级绑定粒度
+ */
+@Data
+@TableName("t_replenisher_device")
+public class ReplenisherDevice implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    @TableId(type = IdType.ASSIGN_ID)
+    @JsonSerialize(using = ToStringSerializer.class)
+    private Long id;
+
+    /**
+     * 补货员ID
+     */
+    @JsonSerialize(using = ToStringSerializer.class)
+    private Long replenisherId;
+
+    /**
+     * 设备SN号
+     */
+    private String deviceId;
+
+    /**
+     * 绑定来源:MANUAL-手动,QR-扫码绑定,SHOP_INHERIT-门店继承
+     */
+    private String source;
+
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai")
+    private LocalDateTime createTime;
+}

+ 19 - 0
haha-entity/src/main/java/com/haha/entity/dto/BatchBindDevicesDTO.java

@@ -0,0 +1,19 @@
+package com.haha.entity.dto;
+
+import jakarta.validation.constraints.NotEmpty;
+import lombok.Data;
+
+import java.util.List;
+
+/**
+ * 批量绑定DTO(多补货员到多设备)
+ */
+@Data
+public class BatchBindDevicesDTO {
+
+    @NotEmpty(message = "补货员ID列表不能为空")
+    private List<Long> replenisherIds;
+
+    @NotEmpty(message = "设备ID列表不能为空")
+    private List<String> deviceIds;
+}

+ 16 - 0
haha-entity/src/main/java/com/haha/entity/dto/BindDeviceDTO.java

@@ -0,0 +1,16 @@
+package com.haha.entity.dto;
+
+import jakarta.validation.constraints.NotEmpty;
+import lombok.Data;
+
+import java.util.List;
+
+/**
+ * 绑定设备到补货员DTO
+ */
+@Data
+public class BindDeviceDTO {
+
+    @NotEmpty(message = "设备ID列表不能为空")
+    private List<String> deviceIds;
+}

+ 14 - 0
haha-entity/src/main/java/com/haha/entity/dto/ReplenisherBindDTO.java

@@ -0,0 +1,14 @@
+package com.haha.entity.dto;
+
+import jakarta.validation.constraints.NotNull;
+import lombok.Data;
+
+/**
+ * 补货员绑定DTO(通用,用于门店/设备绑定补货员)
+ */
+@Data
+public class ReplenisherBindDTO {
+
+    @NotNull(message = "补货员ID不能为空")
+    private Long replenisherId;
+}

+ 22 - 0
haha-entity/src/main/java/com/haha/entity/dto/ReplenisherCreateDTO.java

@@ -0,0 +1,22 @@
+package com.haha.entity.dto;
+
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.Size;
+import lombok.Data;
+
+/**
+ * 创建补货员DTO
+ */
+@Data
+public class ReplenisherCreateDTO {
+
+    @NotBlank(message = "补货员姓名不能为空")
+    @Size(max = 50, message = "姓名长度不能超过50")
+    private String name;
+
+    @Size(max = 20, message = "手机号长度不能超过20")
+    private String phone;
+
+    @Size(max = 50, message = "工号长度不能超过50")
+    private String employeeId;
+}

+ 22 - 0
haha-entity/src/main/java/com/haha/entity/dto/ReplenisherQueryDTO.java

@@ -0,0 +1,22 @@
+package com.haha.entity.dto;
+
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+/**
+ * 补货员分页查询DTO
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class ReplenisherQueryDTO extends PageQueryDTO {
+
+    /**
+     * 关键字(姓名/手机号/工号模糊搜索)
+     */
+    private String keyword;
+
+    /**
+     * 状态:1-启用,0-禁用
+     */
+    private Integer status;
+}

+ 25 - 0
haha-entity/src/main/java/com/haha/entity/dto/ReplenisherUpdateDTO.java

@@ -0,0 +1,25 @@
+package com.haha.entity.dto;
+
+import jakarta.validation.constraints.Size;
+import lombok.Data;
+
+/**
+ * 更新补货员DTO
+ */
+@Data
+public class ReplenisherUpdateDTO {
+
+    @Size(max = 50, message = "姓名长度不能超过50")
+    private String name;
+
+    @Size(max = 20, message = "手机号长度不能超过20")
+    private String phone;
+
+    @Size(max = 50, message = "工号长度不能超过50")
+    private String employeeId;
+
+    /**
+     * 状态:1-启用,0-禁用
+     */
+    private Integer status;
+}

+ 45 - 0
haha-entity/src/main/java/com/haha/entity/dto/StockRecordCreateV2DTO.java

@@ -0,0 +1,45 @@
+package com.haha.entity.dto;
+
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotNull;
+import lombok.Data;
+
+/**
+ * 创建上货记录DTO(V2,支持独立补货员体系)
+ */
+@Data
+public class StockRecordCreateV2DTO {
+
+    /**
+     * 设备ID
+     */
+    @NotBlank(message = "设备ID不能为空")
+    private String deviceId;
+
+    /**
+     * 类型:1上货,2补货
+     */
+    private Integer stockType;
+
+    /**
+     * 补货员ID
+     */
+    @NotNull(message = "补货员ID不能为空")
+    private Long replenisherId;
+
+    /**
+     * 补货员名称
+     */
+    @NotBlank(message = "补货员名称不能为空")
+    private String replenisherName;
+
+    /**
+     * 补货员电话
+     */
+    private String replenisherPhone;
+
+    /**
+     * 备注
+     */
+    private String remark;
+}

+ 53 - 0
haha-mapper/src/main/java/com/haha/mapper/ReplenisherDeviceMapper.java

@@ -0,0 +1,53 @@
+package com.haha.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.haha.entity.ReplenisherDevice;
+import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
+import org.apache.ibatis.annotations.Delete;
+import org.apache.ibatis.annotations.Select;
+
+import java.util.List;
+
+/**
+ * 补货员-设备关联Mapper
+ */
+@Mapper
+public interface ReplenisherDeviceMapper extends BaseMapper<ReplenisherDevice> {
+
+    /**
+     * 获取补货员绑定的所有设备ID列表
+     */
+    @Select("SELECT device_id FROM t_replenisher_device WHERE replenisher_id = #{replenisherId}")
+    List<String> selectDeviceIdsByReplenisherId(@Param("replenisherId") Long replenisherId);
+
+    /**
+     * 获取设备绑定的所有关联记录
+     */
+    @Select("SELECT * FROM t_replenisher_device WHERE device_id = #{deviceId} ORDER BY create_time DESC")
+    List<ReplenisherDevice> selectByDeviceId(@Param("deviceId") String deviceId);
+
+    /**
+     * 批量删除某补货员的设备绑定
+     */
+    @Delete("DELETE FROM t_replenisher_device WHERE replenisher_id = #{replenisherId}")
+    int deleteByReplenisherId(@Param("replenisherId") Long replenisherId);
+
+    /**
+     * 批量删除某设备的补货员绑定
+     */
+    @Delete("DELETE FROM t_replenisher_device WHERE device_id = #{deviceId}")
+    int deleteByDeviceId(@Param("deviceId") String deviceId);
+
+    /**
+     * 检查绑定是否已存在
+     */
+    @Select("SELECT COUNT(*) FROM t_replenisher_device WHERE replenisher_id = #{replenisherId} AND device_id = #{deviceId}")
+    int countByReplenisherIdAndDeviceId(@Param("replenisherId") Long replenisherId,
+                                        @Param("deviceId") String deviceId);
+
+    /**
+     * 批量插入关联(忽略已存在)
+     */
+    int batchInsertIgnore(@Param("list") List<ReplenisherDevice> list);
+}

+ 67 - 0
haha-mapper/src/main/java/com/haha/mapper/ReplenisherMapper.java

@@ -0,0 +1,67 @@
+package com.haha.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.haha.entity.Replenisher;
+import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
+import org.apache.ibatis.annotations.Select;
+
+import java.util.List;
+
+/**
+ * 补货员Mapper
+ */
+@Mapper
+public interface ReplenisherMapper extends BaseMapper<Replenisher> {
+
+    /**
+     * 关键词搜索补货员
+     */
+    @Select("<script>" +
+            "SELECT * FROM t_replenisher " +
+            "<where>" +
+            "  <if test='keyword != null and keyword != \"\"'>" +
+            "    AND (name LIKE CONCAT('%', #{keyword}, '%') " +
+            "         OR phone LIKE CONCAT('%', #{keyword}, '%') " +
+            "         OR employee_id LIKE CONCAT('%', #{keyword}, '%'))" +
+            "  </if>" +
+            "  AND status = 1" +
+            "</where>" +
+            "ORDER BY create_time DESC" +
+            "</script>")
+    List<Replenisher> searchByKeyword(@Param("keyword") String keyword);
+
+    /**
+     * 统计补货员的绑定设备数
+     */
+    @Select("SELECT COUNT(*) FROM t_replenisher_device WHERE replenisher_id = #{replenisherId}")
+    int countBoundDevices(@Param("replenisherId") Long replenisherId);
+
+    /**
+     * 分页查询补货员列表(带设备绑定数量和搜索条件)
+     */
+    IPage<Replenisher> selectWithDeviceCount(Page<Replenisher> page,
+                                              @Param("keyword") String keyword,
+                                              @Param("status") Integer status);
+
+    /**
+     * 通过门店ID查询补货员列表(通过设备关联反向推导)
+     */
+    @Select("SELECT DISTINCT r.* FROM t_replenisher r " +
+            "INNER JOIN t_replenisher_device rd ON r.id = rd.replenisher_id " +
+            "INNER JOIN t_device d ON rd.device_id = d.device_id " +
+            "WHERE d.shop_id = #{shopId} " +
+            "ORDER BY r.create_time DESC")
+    List<Replenisher> selectByShopId(@Param("shopId") Long shopId);
+
+    /**
+     * 通过设备ID查询绑定的补货员列表
+     */
+    @Select("SELECT r.* FROM t_replenisher r " +
+            "INNER JOIN t_replenisher_device rd ON r.id = rd.replenisher_id " +
+            "WHERE rd.device_id = #{deviceId} " +
+            "ORDER BY r.create_time DESC")
+    List<Replenisher> selectByDeviceId(@Param("deviceId") String deviceId);
+}

+ 23 - 0
haha-mapper/src/main/resources/mapper/ReplenisherDeviceMapper.xml

@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.haha.mapper.ReplenisherDeviceMapper">
+
+    <!-- 补货员-设备关联结果映射 -->
+    <resultMap id="BaseResultMap" type="com.haha.entity.ReplenisherDevice">
+        <id column="id" property="id"/>
+        <result column="replenisher_id" property="replenisherId"/>
+        <result column="device_id" property="deviceId"/>
+        <result column="source" property="source"/>
+        <result column="create_time" property="createTime"/>
+    </resultMap>
+
+    <!-- 批量插入关联(忽略已存在) -->
+    <insert id="batchInsertIgnore">
+        INSERT IGNORE INTO t_replenisher_device (id, replenisher_id, device_id, source, create_time)
+        VALUES
+        <foreach collection="list" item="item" separator=",">
+            (#{item.id}, #{item.replenisherId}, #{item.deviceId}, #{item.source}, #{item.createTime})
+        </foreach>
+    </insert>
+
+</mapper>

+ 38 - 0
haha-mapper/src/main/resources/mapper/ReplenisherMapper.xml

@@ -0,0 +1,38 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.haha.mapper.ReplenisherMapper">
+
+    <!-- 补货员基础结果映射 -->
+    <resultMap id="BaseResultMap" type="com.haha.entity.Replenisher">
+        <id column="id" property="id"/>
+        <result column="name" property="name"/>
+        <result column="phone" property="phone"/>
+        <result column="employee_id" property="employeeId"/>
+        <result column="wechat_openid" property="wechatOpenid"/>
+        <result column="avatar" property="avatar"/>
+        <result column="status" property="status"/>
+        <result column="total_tasks" property="totalTasks"/>
+        <result column="last_task_time" property="lastTaskTime"/>
+        <result column="create_time" property="createTime"/>
+        <result column="update_time" property="updateTime"/>
+    </resultMap>
+
+    <!-- 带设备绑定数量的补货员列表查询 -->
+    <select id="selectWithDeviceCount" resultMap="BaseResultMap">
+        SELECT r.*,
+               (SELECT COUNT(*) FROM t_replenisher_device rd WHERE rd.replenisher_id = r.id) AS bound_device_count
+        FROM t_replenisher r
+        <where>
+            <if test="keyword != null and keyword != ''">
+                AND (r.name LIKE CONCAT('%', #{keyword}, '%')
+                     OR r.phone LIKE CONCAT('%', #{keyword}, '%')
+                     OR r.employee_id LIKE CONCAT('%', #{keyword}, '%'))
+            </if>
+            <if test="status != null">
+                AND r.status = #{status}
+            </if>
+        </where>
+        ORDER BY r.create_time DESC
+    </select>
+
+</mapper>

+ 122 - 0
haha-service/src/main/java/com/haha/service/ReplenisherService.java

@@ -0,0 +1,122 @@
+package com.haha.service;
+
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.haha.entity.Replenisher;
+import com.haha.entity.dto.ReplenisherCreateDTO;
+import com.haha.entity.dto.ReplenisherQueryDTO;
+import com.haha.entity.dto.ReplenisherUpdateDTO;
+
+import java.util.List;
+
+/**
+ * 补货员服务接口
+ */
+public interface ReplenisherService extends IService<Replenisher> {
+
+    /**
+     * 分页查询补货员
+     *
+     * @param queryDTO 查询参数
+     * @return 分页结果
+     */
+    IPage<Replenisher> getPage(ReplenisherQueryDTO queryDTO);
+
+    /**
+     * 创建补货员
+     *
+     * @param dto 创建参数
+     * @return 创建的补货员
+     */
+    Replenisher createReplenisher(ReplenisherCreateDTO dto);
+
+    /**
+     * 更新补货员信息
+     *
+     * @param id  补货员ID
+     * @param dto 更新参数
+     * @return 更新后的补货员
+     */
+    Replenisher updateReplenisher(Long id, ReplenisherUpdateDTO dto);
+
+    /**
+     * 启用/禁用补货员
+     *
+     * @param id     补货员ID
+     * @param status 状态:1-启用,0-禁用
+     */
+    void updateStatus(Long id, Integer status);
+
+    /**
+     * 删除补货员(级联删除设备绑定)
+     *
+     * @param id 补货员ID
+     */
+    void deleteReplenisher(Long id);
+
+    /**
+     * 获取补货员详情(含绑定设备数)
+     *
+     * @param id 补货员ID
+     * @return 补货员详情
+     */
+    Replenisher getReplenisherDetail(Long id);
+
+    /**
+     * 绑定设备到补货员
+     *
+     * @param replenisherId 补货员ID
+     * @param deviceIds     设备SN号列表
+     * @return 成功绑定的数量
+     */
+    int bindDevices(Long replenisherId, List<String> deviceIds);
+
+    /**
+     * 解绑补货员的某设备
+     *
+     * @param replenisherId 补货员ID
+     * @param deviceId      设备SN号
+     */
+    void unbindDevice(Long replenisherId, String deviceId);
+
+    /**
+     * 批量绑定(多补货员到多设备)
+     *
+     * @param replenisherIds 补货员ID列表
+     * @param deviceIds      设备SN号列表
+     * @return 成功绑定的数量
+     */
+    int batchBindDevices(List<Long> replenisherIds, List<String> deviceIds);
+
+    /**
+     * 获取补货员绑定的设备ID列表
+     *
+     * @param replenisherId 补货员ID
+     * @return 设备SN号列表
+     */
+    List<String> getBoundDeviceIds(Long replenisherId);
+
+    /**
+     * 获取设备绑定的补货员列表
+     *
+     * @param deviceId 设备SN号
+     * @return 补货员列表
+     */
+    List<Replenisher> getReplenishersByDeviceId(String deviceId);
+
+    /**
+     * 通过门店查询补货员列表(通过设备关联反向推导)
+     *
+     * @param shopId 门店ID
+     * @return 补货员列表
+     */
+    List<Replenisher> getReplenishersByShopId(Long shopId);
+
+    /**
+     * 关键词搜索启用的补货员
+     *
+     * @param keyword 搜索关键词
+     * @return 补货员列表
+     */
+    List<Replenisher> searchByKeyword(String keyword);
+}

+ 19 - 0
haha-service/src/main/java/com/haha/service/ShopReplenisherService.java

@@ -2,6 +2,7 @@ package com.haha.service;
 
 import com.baomidou.mybatisplus.extension.service.IService;
 import com.haha.common.vo.Result;
+import com.haha.entity.Replenisher;
 import com.haha.entity.ShopReplenisher;
 
 import java.util.List;
@@ -65,4 +66,22 @@ public interface ShopReplenisherService extends IService<ShopReplenisher> {
      * @return 是否是补货员
      */
     boolean isReplenisher(Long shopId, Long adminId);
+
+    // ========== V2 方法:使用独立补货员体系(t_replenisher + t_replenisher_device) ==========
+
+    /**
+     * V2:获取门店关联的补货员列表(通过设备绑定反向推导)
+     *
+     * @param shopId 门店ID
+     * @return 补货员列表
+     */
+    List<Replenisher> getReplenishersByShopIdV2(Long shopId);
+
+    /**
+     * V2:将补货员绑定到门店的所有设备
+     *
+     * @param shopId        门店ID
+     * @param replenisherId 补货员ID
+     */
+    void addReplenisherToShop(Long shopId, Long replenisherId);
 }

+ 14 - 0
haha-service/src/main/java/com/haha/service/StockRecordService.java

@@ -77,4 +77,18 @@ public interface StockRecordService extends IService<StockRecord> {
      * @return 记录详情
      */
     Map<String, Object> getDetailWithDeviceName(Long id);
+
+    /**
+     * V2:创建上货记录(使用独立补货员体系)
+     *
+     * @param deviceId       设备ID
+     * @param stockType      类型:1上货,2补货
+     * @param replenisherId  补货员ID
+     * @param replenisherName 补货员名称
+     * @param replenisherPhone 补货员电话
+     * @param remark         备注
+     * @return 上货记录
+     */
+    StockRecord createRecordV2(String deviceId, Integer stockType, Long replenisherId,
+                               String replenisherName, String replenisherPhone, String remark);
 }

+ 274 - 0
haha-service/src/main/java/com/haha/service/impl/ReplenisherServiceImpl.java

@@ -0,0 +1,274 @@
+package com.haha.service.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.haha.common.exception.BusinessException;
+import com.haha.entity.Replenisher;
+import com.haha.entity.ReplenisherDevice;
+import com.haha.entity.dto.ReplenisherCreateDTO;
+import com.haha.entity.dto.ReplenisherQueryDTO;
+import com.haha.entity.dto.ReplenisherUpdateDTO;
+import com.haha.mapper.ReplenisherDeviceMapper;
+import com.haha.mapper.ReplenisherMapper;
+import com.haha.service.ReplenisherService;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.util.StringUtils;
+
+import java.time.LocalDateTime;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * 补货员服务实现类
+ */
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class ReplenisherServiceImpl extends ServiceImpl<ReplenisherMapper, Replenisher> implements ReplenisherService {
+
+    private final ReplenisherMapper replenisherMapper;
+    private final ReplenisherDeviceMapper replenisherDeviceMapper;
+
+    @Override
+    public IPage<Replenisher> getPage(ReplenisherQueryDTO queryDTO) {
+        queryDTO.validate();
+
+        Page<Replenisher> page = new Page<>(queryDTO.getPage(), queryDTO.getPageSize());
+        IPage<Replenisher> result = replenisherMapper.selectWithDeviceCount(page, queryDTO.getKeyword(), queryDTO.getStatus());
+
+        // 填充状态标签
+        for (Replenisher r : result.getRecords()) {
+            fillStatusLabel(r);
+        }
+
+        return result;
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public Replenisher createReplenisher(ReplenisherCreateDTO dto) {
+        // 检查手机号是否已存在
+        if (StringUtils.hasText(dto.getPhone())) {
+            LambdaQueryWrapper<Replenisher> wrapper = new LambdaQueryWrapper<>();
+            wrapper.eq(Replenisher::getPhone, dto.getPhone());
+            if (this.count(wrapper) > 0) {
+                throw new BusinessException(400, "该手机号已被其他补货员使用");
+            }
+        }
+
+        Replenisher replenisher = new Replenisher();
+        replenisher.setName(dto.getName());
+        replenisher.setPhone(dto.getPhone());
+        replenisher.setEmployeeId(dto.getEmployeeId());
+        replenisher.setStatus(1);
+        replenisher.setTotalTasks(0);
+        replenisher.setCreateTime(LocalDateTime.now());
+        replenisher.setUpdateTime(LocalDateTime.now());
+
+        this.save(replenisher);
+
+        log.info("创建补货员成功: id={}, name={}", replenisher.getId(), dto.getName());
+
+        fillStatusLabel(replenisher);
+        return replenisher;
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public Replenisher updateReplenisher(Long id, ReplenisherUpdateDTO dto) {
+        Replenisher replenisher = this.getById(id);
+        if (replenisher == null) {
+            throw new BusinessException(404, "补货员不存在");
+        }
+
+        // 检查手机号是否与其他补货员重复
+        if (StringUtils.hasText(dto.getPhone())) {
+            LambdaQueryWrapper<Replenisher> wrapper = new LambdaQueryWrapper<>();
+            wrapper.eq(Replenisher::getPhone, dto.getPhone())
+                    .ne(Replenisher::getId, id);
+            if (this.count(wrapper) > 0) {
+                throw new BusinessException(400, "该手机号已被其他补货员使用");
+            }
+        }
+
+        if (StringUtils.hasText(dto.getName())) {
+            replenisher.setName(dto.getName());
+        }
+        if (StringUtils.hasText(dto.getPhone())) {
+            replenisher.setPhone(dto.getPhone());
+        }
+        if (StringUtils.hasText(dto.getEmployeeId())) {
+            replenisher.setEmployeeId(dto.getEmployeeId());
+        }
+        if (dto.getStatus() != null) {
+            replenisher.setStatus(dto.getStatus());
+        }
+        replenisher.setUpdateTime(LocalDateTime.now());
+
+        this.updateById(replenisher);
+
+        log.info("更新补货员成功: id={}, name={}", id, replenisher.getName());
+
+        fillStatusLabel(replenisher);
+        return replenisher;
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void updateStatus(Long id, Integer status) {
+        Replenisher replenisher = this.getById(id);
+        if (replenisher == null) {
+            throw new BusinessException(404, "补货员不存在");
+        }
+
+        replenisher.setStatus(status);
+        replenisher.setUpdateTime(LocalDateTime.now());
+        this.updateById(replenisher);
+
+        log.info("更新补货员状态: id={}, status={}", id, status);
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void deleteReplenisher(Long id) {
+        Replenisher replenisher = this.getById(id);
+        if (replenisher == null) {
+            throw new BusinessException(404, "补货员不存在");
+        }
+
+        // 级联删除设备绑定
+        replenisherDeviceMapper.deleteByReplenisherId(id);
+
+        // 删除补货员
+        this.removeById(id);
+
+        log.info("删除补货员成功: id={}, name={}", id, replenisher.getName());
+    }
+
+    @Override
+    public Replenisher getReplenisherDetail(Long id) {
+        Replenisher replenisher = this.getById(id);
+        if (replenisher == null) {
+            return null;
+        }
+
+        // 填充设备绑定数
+        replenisher.setBoundDeviceCount(replenisherMapper.countBoundDevices(id));
+        fillStatusLabel(replenisher);
+
+        return replenisher;
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public int bindDevices(Long replenisherId, List<String> deviceIds) {
+        Replenisher replenisher = this.getById(replenisherId);
+        if (replenisher == null) {
+            throw new BusinessException(404, "补货员不存在");
+        }
+
+        if (deviceIds == null || deviceIds.isEmpty()) {
+            return 0;
+        }
+
+        List<ReplenisherDevice> toAdd = new ArrayList<>();
+        LocalDateTime now = LocalDateTime.now();
+
+        for (String deviceId : deviceIds) {
+            int count = replenisherDeviceMapper.countByReplenisherIdAndDeviceId(replenisherId, deviceId);
+            if (count > 0) {
+                log.warn("绑定已存在,跳过: replenisherId={}, deviceId={}", replenisherId, deviceId);
+                continue;
+            }
+
+            ReplenisherDevice rd = new ReplenisherDevice();
+            rd.setReplenisherId(replenisherId);
+            rd.setDeviceId(deviceId);
+            rd.setSource("MANUAL");
+            rd.setCreateTime(now);
+            toAdd.add(rd);
+        }
+
+        if (!toAdd.isEmpty()) {
+            replenisherDeviceMapper.batchInsertIgnore(toAdd);
+        }
+
+        log.info("绑定设备到补货员: replenisherId={}, 请求={}, 成功={}", replenisherId, deviceIds.size(), toAdd.size());
+        return toAdd.size();
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void unbindDevice(Long replenisherId, String deviceId) {
+        LambdaQueryWrapper<ReplenisherDevice> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(ReplenisherDevice::getReplenisherId, replenisherId)
+                .eq(ReplenisherDevice::getDeviceId, deviceId);
+
+        replenisherDeviceMapper.delete(wrapper);
+
+        log.info("解绑补货员设备: replenisherId={}, deviceId={}", replenisherId, deviceId);
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public int batchBindDevices(List<Long> replenisherIds, List<String> deviceIds) {
+        if (replenisherIds == null || replenisherIds.isEmpty() || deviceIds == null || deviceIds.isEmpty()) {
+            return 0;
+        }
+
+        int total = 0;
+        for (Long replenisherId : replenisherIds) {
+            total += bindDevices(replenisherId, deviceIds);
+        }
+
+        return total;
+    }
+
+    @Override
+    public List<String> getBoundDeviceIds(Long replenisherId) {
+        return replenisherDeviceMapper.selectDeviceIdsByReplenisherId(replenisherId);
+    }
+
+    @Override
+    public List<Replenisher> getReplenishersByDeviceId(String deviceId) {
+        List<Replenisher> list = replenisherMapper.selectByDeviceId(deviceId);
+        for (Replenisher r : list) {
+            fillStatusLabel(r);
+        }
+        return list;
+    }
+
+    @Override
+    public List<Replenisher> getReplenishersByShopId(Long shopId) {
+        List<Replenisher> list = replenisherMapper.selectByShopId(shopId);
+        for (Replenisher r : list) {
+            fillStatusLabel(r);
+        }
+        return list;
+    }
+
+    @Override
+    public List<Replenisher> searchByKeyword(String keyword) {
+        return replenisherMapper.searchByKeyword(keyword);
+    }
+
+    /**
+     * 填充状态标签
+     */
+    private void fillStatusLabel(Replenisher r) {
+        if (r.getStatus() != null) {
+            if (r.getStatus() == 1) {
+                r.setStatusLabel("正常");
+                r.setStatusColor("success");
+            } else {
+                r.setStatusLabel("禁用");
+                r.setStatusColor("danger");
+            }
+        }
+    }
+}

+ 37 - 1
haha-service/src/main/java/com/haha/service/impl/ShopReplenisherServiceImpl.java

@@ -6,9 +6,13 @@ import com.haha.common.exception.BusinessException;
 import com.haha.common.vo.Result;
 import com.haha.entity.ShopReplenisher;
 import com.haha.entity.Admin;
+import com.haha.entity.Device;
+import com.haha.entity.Replenisher;
 import com.haha.mapper.ShopReplenisherMapper;
-import com.haha.service.ShopReplenisherService;
 import com.haha.service.AdminService;
+import com.haha.service.DeviceService;
+import com.haha.service.ReplenisherService;
+import com.haha.service.ShopReplenisherService;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.stereotype.Service;
@@ -17,6 +21,7 @@ import org.springframework.transaction.annotation.Transactional;
 import java.time.LocalDateTime;
 import java.util.ArrayList;
 import java.util.List;
+import java.util.stream.Collectors;
 
 /**
  * 门店补货员服务实现类
@@ -28,6 +33,8 @@ public class ShopReplenisherServiceImpl extends ServiceImpl<ShopReplenisherMappe
 
     private final ShopReplenisherMapper shopReplenisherMapper;
     private final AdminService adminService;
+    private final ReplenisherService replenisherService;
+    private final DeviceService deviceService;
 
     @Override
     public List<ShopReplenisher> getReplenishersByShopId(Long shopId) {
@@ -164,4 +171,33 @@ public class ShopReplenisherServiceImpl extends ServiceImpl<ShopReplenisherMappe
     public boolean isReplenisher(Long shopId, Long adminId) {
         return shopReplenisherMapper.countByShopIdAndAdminId(shopId, adminId) > 0;
     }
+
+    // ========== V2 方法实现 ==========
+
+    @Override
+    public List<Replenisher> getReplenishersByShopIdV2(Long shopId) {
+        return replenisherService.getReplenishersByShopId(shopId);
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void addReplenisherToShop(Long shopId, Long replenisherId) {
+        // 检查补货员是否存在
+        Replenisher replenisher = replenisherService.getById(replenisherId);
+        if (replenisher == null) {
+            throw new BusinessException(404, "补货员不存在");
+        }
+
+        // 查询门店下的所有设备
+        List<Device> devices = deviceService.lambdaQuery().eq(Device::getShopId, shopId).list();
+        if (devices.isEmpty()) {
+            throw new BusinessException(400, "该门店下没有设备,无法绑定补货员");
+        }
+
+        List<String> deviceIds = devices.stream().map(Device::getDeviceId).collect(Collectors.toList());
+        int count = replenisherService.bindDevices(replenisherId, deviceIds);
+
+        log.info("将补货员绑定到门店所有设备: replenisherId={}, shopId={}, deviceCount={}, boundCount={}",
+                replenisherId, shopId, deviceIds.size(), count);
+    }
 }

+ 25 - 0
haha-service/src/main/java/com/haha/service/impl/StockRecordServiceImpl.java

@@ -92,6 +92,31 @@ public class StockRecordServiceImpl extends ServiceImpl<StockRecordMapper, Stock
         return record;
     }
 
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public StockRecord createRecordV2(String deviceId, Integer stockType, Long replenisherId,
+                                       String replenisherName, String replenisherPhone, String remark) {
+        StockRecord record = new StockRecord();
+        record.setDeviceId(deviceId);
+        record.setStockType(stockType);
+        // 复用 stockerId/stockerName/stockerPhone 字段存储补货员信息
+        record.setStockerId(replenisherId);
+        record.setStockerName(replenisherName);
+        record.setStockerPhone(replenisherPhone);
+        record.setStatus(1); // 进行中
+        record.setTotalItems(0);
+        record.setTotalQuantity(0);
+        record.setRemark(remark);
+        record.setCreateTime(LocalDateTime.now());
+        record.setUpdateTime(LocalDateTime.now());
+
+        save(record);
+
+        log.info("创建上货记录(V2): id={}, deviceId={}, replenisherName={}", record.getId(), deviceId, replenisherName);
+
+        return record;
+    }
+
     @Override
     @Transactional(rollbackFor = Exception.class)
     public void startActivity(Long recordId) {