Procházet zdrojové kódy

feat: 运营/补货员数据分离 + 门状态轮询 + 首页补货管理入口

【后端】
- ReplenisherOperationController: isAdmin()/getAccessibleDeviceIds()/canAccessDevice()
- 查询接口角色分流: admin返回全部设备+补货单, replenisher仅绑定设备
- 写操作(replenishStock/openDoor)保留仅补货员调用
- GET /replenisher/device/{id}/status 门状态查询(轻量轮询)

【门状态轮询】
- operation.vue: 每3秒轮询门状态, 检测到关门弹窗确认(继续补货/提交结果)

【首页】
- index.vue: 商户运营区新增「补货管理」入口→/pages/replenish/index

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
skyline před 12 hodinami
rodič
revize
6a58c6d944

+ 7 - 0
haha-admin-mp/src/api/replenish.ts

@@ -98,6 +98,13 @@ export async function openDeviceDoor(deviceId: string): Promise<void> {
   return post(`/replenisher/device/${deviceId}/open`);
 }
 
+/**
+ * 查询设备门状态(轻量轮询)
+ */
+export async function getDeviceDoorStatus(deviceId: string): Promise<{ status: number; statusLabel: string }> {
+  return get(`/replenisher/device/${deviceId}/status`);
+}
+
 /**
  * 获取补货员的补货单列表
  */

+ 12 - 0
haha-admin-mp/src/pages/index/index.vue

@@ -139,6 +139,12 @@
             </view>
             <text class="quick-name">新品申请</text>
           </view>
+          <view class="quick-item" @click="navigateTo('/pages/replenish/index')">
+            <view class="quick-icon">
+              <view class="icon-replenish"></view>
+            </view>
+            <text class="quick-name">补货管理</text>
+          </view>
           <view class="quick-item" @click="navigateTo('/pages/replenisher/list')">
             <view class="quick-icon">
               <view class="icon-replenisher"></view>
@@ -691,6 +697,12 @@ onMounted(() => {
   }
 }
 
+.icon-replenish {
+  width: 36rpx; height: 36rpx; position: relative;
+  &::before { content: ''; position: absolute; top: 2rpx; left: 2rpx; right: 2rpx; bottom: 2rpx; border: 2.5rpx solid $primary-color; border-radius: 5rpx; }
+  &::after { content: ''; position: absolute; top: 12rpx; left: 8rpx; right: 8rpx; height: 2.5rpx; background: $primary-color; border-radius: 1rpx; box-shadow: 0 8rpx 0 $primary-color, 0 16rpx 0 $primary-color; }
+}
+
 .icon-staff {
   width: 32rpx;
   height: 36rpx;

+ 38 - 3
haha-admin-mp/src/pages/replenish/operation.vue

@@ -97,9 +97,9 @@
 </template>
 
 <script setup lang="ts">
-import { ref, computed, onMounted } from 'vue';
+import { ref, computed, onMounted, onUnmounted } from 'vue';
 import NavBar from '@/components/NavBar.vue';
-import { getDeviceInventory, replenishStock, getDevicePendingOrders, getReplenisherOrderDetail, openDeviceDoor } from '@/api/replenish';
+import { getDeviceInventory, replenishStock, getDevicePendingOrders, getReplenisherOrderDetail, openDeviceDoor, getDeviceDoorStatus } from '@/api/replenish';
 import { isReplenisher } from '@/utils/auth';
 
 interface ReplenishInput {
@@ -124,6 +124,8 @@ const showOrderOnly = ref(false);
 const orderItemCodes = ref<Set<string>>(new Set());
 const pendingOrders = ref<any[]>([]);
 const selectedOrderIndex = ref(0);
+const doorClosedAlertShown = ref(false);
+let doorPollTimer: any = null;
 
 const isInOrder = (item: any) => {
   const code = (item.product_code || item.productCode || '').trim().toUpperCase();
@@ -238,6 +240,32 @@ const handleReopen = () => {
   });
 };
 
+const startDoorPolling = () => {
+  if (!deviceId.value) return;
+  doorPollTimer = setInterval(async () => {
+    if (doorClosedAlertShown.value) return;
+    try {
+      const status = await getDeviceDoorStatus(deviceId.value);
+      if (status.status !== 1) {
+        doorClosedAlertShown.value = true;
+        uni.showModal({
+          title: '柜门已关闭',
+          content: '检测到柜门已关闭,是否继续补货?\n\n选择「继续补货」留在当前页面\n选择「提交结果」直接提交补货',
+          cancelText: '提交结果',
+          confirmText: '继续补货',
+          success: (res) => {
+            if (res.confirm) {
+              doorClosedAlertShown.value = false;
+            } else {
+              handleSubmit();
+            }
+          }
+        });
+      }
+    } catch { /* ignore poll errors */ }
+  }, 3000);
+};
+
 onMounted(async () => {
   const pages = getCurrentPages();
   const opts = (pages[pages.length - 1] as any)?.$page?.options || (pages[pages.length - 1] as any)?.options || {};
@@ -306,7 +334,14 @@ onMounted(async () => {
     });
   } catch (e: any) {
     uni.showToast({ title: e.message || '加载失败', icon: 'none' });
-  } finally { loading.value = false; }
+  } finally {
+    loading.value = false;
+    startDoorPolling();
+  }
+});
+
+onUnmounted(() => {
+  if (doorPollTimer) { clearInterval(doorPollTimer); doorPollTimer = null; }
 });
 </script>
 

+ 70 - 35
haha-admin/src/main/java/com/haha/admin/controller/ReplenisherOperationController.java

@@ -66,17 +66,20 @@ public class ReplenisherOperationController {
      */
     @GetMapping("/device/list")
     public Result<List<Map<String, Object>>> getDeviceList() {
-        Replenisher replenisher = getCurrentReplenisher();
-
-        // 获取补货员绑定的设备ID列表
-        List<String> deviceIds = replenisherService.getBoundDeviceIds(replenisher.getId());
+        List<String> deviceIds = getAccessibleDeviceIds();
         if (deviceIds.isEmpty()) {
             return Result.success("查询成功", new ArrayList<>());
         }
 
+        boolean isAdmin = isAdmin();
+        Long operatorId = null;
+        if (!isAdmin) {
+            operatorId = getCurrentReplenisher().getId();
+        }
+
         // 批量统计各设备补货单数量
-        Map<String, Long> pendingOrderCountMap = new java.util.HashMap<>();   // 草稿+已提交+已同步
-        Map<String, Long> submittedOrderCountMap = new java.util.HashMap<>(); // 已提交+已同步(不含草稿)
+        Map<String, Long> pendingOrderCountMap = new java.util.HashMap<>();
+        Map<String, Long> submittedOrderCountMap = new java.util.HashMap<>();
         if (!deviceIds.isEmpty()) {
             var qwAll = new com.baomidou.mybatisplus.core.conditions.query.QueryWrapper<ReplenishmentOrder>()
                 .select("device_id", "status", "COUNT(*) as cnt")
@@ -123,15 +126,15 @@ public class ReplenisherOperationController {
             }
         }
 
-        // 批量统计今日已补货设备(上货类型、当前补货员、今天)
+        // 批量统计今日已补货设备
         java.time.LocalDate today = java.time.LocalDate.now();
         Map<String, Boolean> todayRestockedMap = new java.util.HashMap<>();
-        if (!deviceIds.isEmpty()) {
+        if (!deviceIds.isEmpty() && !isAdmin) {
             var logQw = new com.baomidou.mybatisplus.core.conditions.query.QueryWrapper<InventoryLog>()
                 .select("DISTINCT device_id")
                 .in("device_id", deviceIds)
                 .eq("change_type", InventoryLog.TYPE_RESTOCK)
-                .eq("operator_id", replenisher.getId())
+                .eq("operator_id", operatorId)
                 .ge("create_time", today.atStartOfDay())
                 .lt("create_time", today.plusDays(1).atStartOfDay());
             List<Map<String, Object>> logRows = inventoryLogService.listMaps(logQw);
@@ -204,9 +207,7 @@ public class ReplenisherOperationController {
      */
     @GetMapping("/device/{deviceId}/pending-orders")
     public Result<List<Map<String, Object>>> getDevicePendingOrders(@PathVariable String deviceId) {
-        Replenisher replenisher = getCurrentReplenisher();
-        List<String> boundIds = replenisherService.getBoundDeviceIds(replenisher.getId());
-        if (!boundIds.contains(deviceId)) {
+        if (!canAccessDevice(deviceId)) {
             return Result.error(403, "您无权查看此设备");
         }
 
@@ -247,6 +248,24 @@ public class ReplenisherOperationController {
         return success ? Result.success("开门指令已发送", null) : Result.error(500, "开门失败");
     }
 
+    /**
+     * 查询设备门状态(轻量轮询用)
+     */
+    @GetMapping("/device/{deviceId}/status")
+    public Result<Map<String, Object>> getDeviceStatus(@PathVariable String deviceId) {
+        if (!canAccessDevice(deviceId)) {
+            return Result.error(403, "您无权查看此设备");
+        }
+        Device device = deviceService.getDeviceBySn(deviceId);
+        if (device == null) {
+            return Result.error(404, "设备不存在");
+        }
+        Map<String, Object> result = new HashMap<>();
+        result.put("status", device.getStatus());
+        result.put("statusLabel", getDeviceStatusLabel(device.getStatus()));
+        return Result.success("查询成功", result);
+    }
+
     /**
      * 获取设备库存详情
      *
@@ -254,12 +273,7 @@ public class ReplenisherOperationController {
      */
     @GetMapping("/device/inventory/{deviceId}")
     public Result<Map<String, Object>> getDeviceInventory(@PathVariable String deviceId) {
-        // 验证当前登录用户是补货员
-        Replenisher replenisher = getCurrentReplenisher();
-
-        // 验证该设备是否绑定到此补货员
-        List<String> boundDeviceIds = replenisherService.getBoundDeviceIds(replenisher.getId());
-        if (!boundDeviceIds.contains(deviceId)) {
+        if (!canAccessDevice(deviceId)) {
             return Result.error(403, "您无权查看此设备的库存");
         }
 
@@ -369,15 +383,13 @@ public class ReplenisherOperationController {
             @RequestParam(defaultValue = "1") int page,
             @RequestParam(defaultValue = "10") int pageSize,
             @RequestParam(required = false) Integer status) {
-        Replenisher replenisher = getCurrentReplenisher();
-        List<String> deviceIds = replenisherService.getBoundDeviceIds(replenisher.getId());
-
-        if (deviceIds.isEmpty()) {
-            return Result.success("查询成功", new PageResult<>());
-        }
-
         LambdaQueryWrapper<ReplenishmentOrder> wrapper = new LambdaQueryWrapper<>();
-        wrapper.in(ReplenishmentOrder::getDeviceId, deviceIds);
+        if (!isAdmin()) {
+            Replenisher replenisher = getCurrentReplenisher();
+            List<String> deviceIds = replenisherService.getBoundDeviceIds(replenisher.getId());
+            if (deviceIds.isEmpty()) return Result.success("查询成功", new PageResult<>());
+            wrapper.in(ReplenishmentOrder::getDeviceId, deviceIds);
+        }
         if (status != null) {
             wrapper.eq(ReplenishmentOrder::getStatus, status);
         }
@@ -406,15 +418,12 @@ public class ReplenisherOperationController {
      */
     @GetMapping("/orders/{id}")
     public Result<Map<String, Object>> getOrderDetail(@PathVariable Long id) {
-        Replenisher replenisher = getCurrentReplenisher();
         ReplenishmentOrder order = replenishmentOrderService.getDetailWithItems(id);
         if (order == null) {
             return Result.error(404, "补货单不存在");
         }
 
-        // 验证该补货单关联的设备是否绑定到当前补货员
-        List<String> deviceIds = replenisherService.getBoundDeviceIds(replenisher.getId());
-        if (!deviceIds.contains(order.getDeviceId())) {
+        if (!isAdmin() && !canAccessDevice(order.getDeviceId())) {
             return Result.error(403, "您无权查看此补货单");
         }
 
@@ -426,17 +435,20 @@ public class ReplenisherOperationController {
         return Result.success("查询成功", result);
     }
 
+    private boolean isAdmin() {
+        String userType = StpUtil.getTokenSession().getString("userType");
+        return !"REPLENISHER".equals(userType);
+    }
+
     /**
-     * 获取当前登录的补货员
+     * 获取当前登录的补货员(仅补货员可调用,运营人员返回null)
      */
     private Replenisher getCurrentReplenisher() {
-        // 验证当前登录用户是否为补货员
         String userType = StpUtil.getTokenSession().getString("userType");
         if (!"REPLENISHER".equals(userType)) {
-            log.warn("非补货员用户尝试访问补货员接口");
-            throw new BusinessException(403, "无访问权限");
+            log.warn("非补货员用户尝试访问补货员专用接口");
+            throw new BusinessException(403, "仅补货员可执行此操作");
         }
-
         String loginId = StpUtil.getLoginIdAsString();
         Replenisher replenisher = replenisherService.getById(loginId);
         if (replenisher == null) {
@@ -445,6 +457,29 @@ public class ReplenisherOperationController {
         return replenisher;
     }
 
+    /**
+     * 获取补货员绑定的设备ID列表,运营人员返回全部设备ID
+     */
+    private List<String> getAccessibleDeviceIds() {
+        if (isAdmin()) {
+            return deviceService.list().stream()
+                .map(Device::getDeviceId)
+                .filter(id -> id != null && !id.isEmpty())
+                .collect(Collectors.toList());
+        }
+        Replenisher replenisher = getCurrentReplenisher();
+        return replenisherService.getBoundDeviceIds(replenisher.getId());
+    }
+
+    /**
+     * 验证设备访问权限(运营人员可访问全部,补货员仅可访问已绑定设备)
+     */
+    private boolean canAccessDevice(String deviceId) {
+        if (isAdmin()) return true;
+        Replenisher replenisher = getCurrentReplenisher();
+        return replenisherService.getBoundDeviceIds(replenisher.getId()).contains(deviceId);
+    }
+
     /**
      * 获取设备状态标签
      */