Forráskód Böngészése

refactor: 补货员绑定流程优化,统一到运营端小程序

- 移除 haha-mp 用户端小程序中的死胡同绑定页面和相关代码
- haha-admin-mp 补货首页新增「扫码开柜」按钮,扫描设备二维码直接进入补货操作
- Web 后台绑定码弹窗移除无效的普通二维码,改为清晰的操作步骤指引
- 移除 qrcode 依赖,简化 hook.tsx
- 补货员完整操作链路:后台创建账号 → 运营端小程序绑定微信 → 扫码开柜补货
- 新增补货系统文档

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
skyline 1 hete
szülő
commit
d20f17048e

+ 424 - 0
docs/补货系统文档.md

@@ -0,0 +1,424 @@
+# 补货系统文档
+
+## 系统概览
+
+补货系统负责管理智能售货机的商品补货流程,包含两套子体系:
+
+- **补货员体系**:独立的补货员账号管理、微信登录认证、设备绑定,以及小程序端执行补货操作
+- **补货单体系**:运营后台创建补货计划单,管理补货单的完整生命周期
+
+系统已从 V1(`t_stocker` 上货员)迁移到 V2(`t_replenisher` 补货员),V1 表已废弃。
+
+---
+
+## 一、补货员管理
+
+### 1.1 数据模型
+
+#### t_replenisher(补货员表)
+
+| 字段 | 类型 | 说明 |
+|------|------|------|
+| id | BIGINT | 主键,雪花算法生成 |
+| name | VARCHAR | 姓名 |
+| phone | VARCHAR | 手机号 |
+| employee_id | VARCHAR | 工号 |
+| wechat_openid | VARCHAR | 微信 OpenID(绑定后写入) |
+| avatar | VARCHAR | 头像 URL |
+| status | INT | 状态:1=启用,0=禁用 |
+| total_tasks | INT | 累计任务数 |
+| last_task_time | DATETIME | 最后任务时间 |
+| create_time | DATETIME | 创建时间 |
+| update_time | DATETIME | 更新时间 |
+
+#### t_replenisher_device(设备绑定表)
+
+| 字段 | 类型 | 说明 |
+|------|------|------|
+| id | BIGINT | 主键 |
+| replenisher_id | BIGINT | 补货员 ID |
+| device_id | VARCHAR | 设备 SN 号 |
+| source | VARCHAR | 绑定来源:MANUAL(手动)/ QR(扫码)/ SHOP_INHERIT(门店继承) |
+| create_time | DATETIME | 绑定时间 |
+
+### 1.2 API 接口
+
+所有接口路由前缀为 `/replenishers`,需要管理员权限。
+
+#### 补货员 CRUD
+
+| 方法 | 路径 | 权限 | 说明 |
+|------|------|------|------|
+| GET | `/replenishers/list` | `replenisher:read` | 分页查询补货员列表,支持 keyword/status 筛选 |
+| GET | `/replenishers/{id}` | `replenisher:read` | 获取补货员详情(含绑定设备数) |
+| GET | `/replenishers/search` | `replenisher:read` | 按姓名/手机/工号模糊搜索 |
+| POST | `/replenishers` | `replenisher:create` | 创建补货员(name 必填) |
+| PUT | `/replenishers/{id}` | `replenisher:update` | 更新补货员信息 |
+| PUT | `/replenishers/{id}/status` | `replenisher:update` | 启用/禁用补货员 |
+| DELETE | `/replenishers/{id}` | `replenisher:delete` | 删除补货员(级联解绑所有设备) |
+
+#### 设备绑定
+
+| 方法 | 路径 | 权限 | 说明 |
+|------|------|------|------|
+| GET | `/replenishers/{id}/devices` | `replenisher:read` | 获取补货员绑定的设备 ID 列表 |
+| POST | `/replenishers/{id}/devices` | `replenisher:update` | 绑定设备到补货员(INSERT IGNORE,幂等) |
+| DELETE | `/replenishers/{id}/devices/{deviceId}` | `replenisher:update` | 解绑单个设备 |
+| POST | `/replenishers/batch-bind` | `replenisher:update` | 批量绑定多个补货员到多个设备 |
+
+#### 微信绑定
+
+| 方法 | 路径 | 权限 | 说明 |
+|------|------|------|------|
+| POST | `/replenishers/{id}/binding-code` | `replenisher:update` | 生成 24 位绑定码(Redis 存储,24小时有效) |
+
+### 1.3 认证流程
+
+补货员的绑定和登录全部在**运营端小程序(haha-admin-mp)**中完成。
+
+#### 创建与绑定(首次使用)
+
+```
+管理员在 Web 后台创建补货员 → 生成 24 位绑定码(Redis 存储,24h 有效)
+→ 将绑定码告知补货员
+→ 补货员打开"哈哈运营平台"小程序 → 登录页点击"已有绑定码?点击绑定微信"
+→ 输入绑定码 → 微信授权 → POST /replenisher/login/bind
+→ 后端校验绑定码、获取 openid、写入 wechat_openid、激活账号
+→ 自动登录 → 进入补货首页
+```
+
+#### 静默登录(已绑定后)
+
+```
+补货员打开运营端小程序 → 点击"补货员微信一键登录"
+→ wx.login() 获取 code → POST /replenisher/login/wechat
+→ 后端通过 code 换 openid → 查找已绑定的补货员
+→ 生成 SaToken → 返回 token + 用户信息 → 进入补货首页
+```
+
+### 1.4 关联管理入口
+
+补货员还可以通过以下入口绑定到门店/设备:
+
+| 来源 | 接口 | 说明 |
+|------|------|------|
+| 门店管理 | `POST /shops/{id}/replenishers` | 将补货员绑定到门店下所有设备,source=SHOP_INHERIT |
+| 门店管理 | `GET /shops/{id}/replenishers` | 查询门店关联的补货员列表 |
+| 设备管理 | `POST /devices/{deviceId}/replenishers` | 直接绑定补货员到设备,source=MANUAL |
+| 设备管理 | `GET /devices/{deviceId}/replenishers` | 查询设备绑定的补货员列表 |
+| 设备管理 | `DELETE /devices/{deviceId}/replenishers/{replenisherId}` | 解绑设备的补货员 |
+
+---
+
+## 二、补货操作
+
+### 2.1 权限控制
+
+补货员只能操作自己绑定的设备。操作前系统会:
+
+1. 从当前登录的 SaToken session 中获取补货员 ID
+2. 查询补货员绑定的设备 ID 列表
+3. 校验目标设备是否在绑定列表中,不在则拒绝访问
+
+### 2.2 API 接口
+
+所有接口路由前缀为 `/replenisher`,需要补货员角色(`REPLENISHER`)。
+
+| 方法 | 路径 | 说明 |
+|------|------|------|
+| GET | `/replenisher/my-info` | 获取当前补货员个人信息 |
+| GET | `/replenisher/device/list` | 获取绑定设备列表(含各设备库存概览、在线状态) |
+| GET | `/replenisher/device/inventory/{deviceId}` | 获取设备库存详情(含商品信息、层模板分组) |
+| POST | `/replenisher/stock/replenish` | 执行补货操作,逐商品增加库存 |
+
+### 2.3 补货操作流程
+
+```
+补货员选择设备 → GET /replenisher/device/inventory/{deviceId} → 查看当前库存
+→ 输入各商品补货数量 → POST /replenisher/stock/replenish
+→ 后端逐商品调用 deviceInventoryService.increaseStock()
+→ 库存增加成功 → 返回执行结果(成功/失败明细)
+```
+
+补货请求体(`ReplenishDTO`):
+
+```json
+{
+  "deviceId": "SN20250101001",
+  "items": [
+    {
+      "productId": 1001,
+      "productCode": "P001",
+      "productName": "可口可乐",
+      "shelfNum": 1,
+      "position": "left",
+      "quantity": 10
+    }
+  ]
+}
+```
+
+### 2.4 库存记录
+
+补货操作后系统自动记录上货日志:
+
+- 日志类型:`InventoryLog.TYPE_RESTOCK`(补货上货)
+- 数据库表:`t_inventory_log` 记录 beforeStock → afterStock 变动
+- 可通过 `POST /inventory/records/v2` 创建上货记录时关联补货员信息(`replenisherId`、`replenisherName`、`replenisherPhone`)
+
+---
+
+## 三、补货单管理
+
+补货单用于运营人员制定补货计划,管理补货任务从创建到完成的全流程。
+
+### 3.1 数据模型
+
+#### t_replenishment_order(补货单表)
+
+| 字段 | 类型 | 说明 |
+|------|------|------|
+| id | BIGINT | 主键 |
+| order_no | VARCHAR | 补货单号,规则:`RO` + yyyyMMdd + 4位序号 |
+| device_id | VARCHAR | 目标设备 SN |
+| shop_id | BIGINT | 目标门店 ID |
+| status | INT | 状态:0 草稿 / 1 已提交 / 2 已同步ERP / 3 已完成 / 4 已取消 |
+| erp_order_no | VARCHAR | ERP 采购单号(同步后回填) |
+| erp_sync_time | DATETIME | ERP 同步时间 |
+| supplier_name | VARCHAR | 供应商名称 |
+| warehouse_name | VARCHAR | 发货仓库 |
+| expected_arrival_time | DATETIME | 预计到货时间 |
+| total_quantity | INT | 补货总数量 |
+| total_amount | DECIMAL | 补货总金额 |
+| remark | VARCHAR | 备注 |
+| creator_id | BIGINT | 创建人 ID |
+| creator_name | VARCHAR | 创建人姓名 |
+
+#### t_replenishment_order_item(补货单明细表)
+
+| 字段 | 类型 | 说明 |
+|------|------|------|
+| id | BIGINT | 主键 |
+| order_id | BIGINT | 关联补货单 ID |
+| device_id | VARCHAR | 设备 SN(冗余) |
+| product_id | BIGINT | 商品 ID |
+| product_code | VARCHAR | 商品编码 |
+| product_name | VARCHAR | 商品名称 |
+| planned_quantity | INT | 计划补货数量(≥1) |
+| actual_quantity | INT | 实际补货数量 |
+| before_stock | INT | 补货前库存 |
+| after_stock | INT | 补货后库存 |
+| unit_price | DECIMAL | 单价 |
+| total_price | DECIMAL | 小计金额 |
+| shelf_num | INT | 货架层号 |
+| position | VARCHAR | 货道位置(left/right) |
+
+### 3.2 状态流转
+
+```
+草稿(0) ──提交──→ 已提交(1) ──同步ERP──→ 已同步ERP(2) ──完成──→ 已完成(3)
+  │                  │                                    │
+  └──取消──→ 已取消(4) ←──取消──┘                        │
+       ↑                                                   │
+       └───────────────────────────────────────────────────┘
+                          (已完成也可以取消)
+```
+
+状态说明:
+
+| 状态码 | 状态名 | 标签色 | 说明 |
+|--------|--------|--------|------|
+| 0 | 草稿 | info | 可编辑、可删除、可提交、可取消 |
+| 1 | 已提交 | warning | 可同步ERP、可取消 |
+| 2 | 已同步ERP | primary | 已推送至采购系统,可完成、可取消 |
+| 3 | 已完成 | success | 终态(也可取消) |
+| 4 | 已取消 | danger | 终态 |
+
+### 3.3 API 接口
+
+路由前缀为 `/replenishment-orders`,需要相应权限。
+
+| 方法 | 路径 | 权限 | 说明 |
+|------|------|------|------|
+| GET | `/replenishment-orders/list` | `replenishment:order:read` | 分页查询,支持 orderNo/deviceId/shopId/status/时间范围/创建人 筛选 |
+| GET | `/replenishment-orders/{id}` | `replenishment:order:read` | 获取补货单详情 |
+| GET | `/replenishment-orders/{id}/items` | `replenishment:order:read` | 获取补货单商品明细列表 |
+| POST | `/replenishment-orders` | `replenishment:order:create` | 创建补货单(含商品明细) |
+| PUT | `/replenishment-orders` | `replenishment:order:update` | 更新补货单(仅草稿状态可更新) |
+| DELETE | `/replenishment-orders/{id}` | `replenishment:order:delete` | 删除补货单(仅草稿状态可删除) |
+| POST | `/replenishment-orders/{id}/submit` | `replenishment:order:submit` | 提交补货单(草稿 → 已提交) |
+| POST | `/replenishment-orders/{id}/sync-erp` | `replenishment:order:sync` | 同步到 ERP 系统(已提交 → 已同步ERP) |
+| POST | `/replenishment-orders/{id}/complete` | `replenishment:order:complete` | 完成补货单(已同步ERP → 已完成) |
+| POST | `/replenishment-orders/{id}/cancel` | `replenishment:order:update` | 取消补货单 |
+
+### 3.4 操作约束
+
+- **创建**:需传入 deviceId(必填)、商品明细列表(plannedQuantity ≥ 1),系统自动生成单号
+- **更新**:仅草稿状态可更新,更新时重新计算 totalQuantity 和 totalAmount
+- **删除**:仅草稿状态可删除,删除时级联删除明细
+- **提交**:草稿 → 已提交,此后不可再编辑
+- **同步 ERP**:目前为桩代码,预留企得宝 SDK 集成接口
+- **完成**:已同步ERP → 已完成
+- **取消**:草稿、已提交、已同步ERP、已完成 均可取消(取消是终态)
+
+---
+
+## 四、前端页面
+
+### 4.1 Web 管理后台(haha-admin-web)
+
+| 页面 | 路由 | 说明 |
+|------|------|------|
+| 补货员列表 | `/operation/replenisher` | 补货员 CRUD、设备绑定/解绑、绑定码生成 |
+| 补货单列表 | 侧边栏「库存管理 → 补货单列表」 | 补货单查询、新建、状态操作 |
+| 补货单详情 | 侧边栏「库存管理 → 补货单详情」 | 补货单完整信息 + 商品明细 |
+| 设备管理 | 设备列表页内嵌弹窗 | 为指定设备绑定/解绑补货员 |
+| 门店管理 | 门店列表页内嵌弹窗 | 为门店批量绑定补货员到其下所有设备 |
+| 库存记录 | 库存管理 → 上货记录 | 上货记录中可关联补货员信息(V2 补货员体系) |
+
+### 4.2 运营端小程序(haha-admin-mp)
+
+| 页面 | 路径 | 说明 |
+|------|------|------|
+| 补货首页 | `pages/replenish/index` | 补货员信息卡片、绑定设备列表、待补货统计、**扫码开柜**按钮 |
+| 补货操作 | `pages/replenish/operation` | 设备商品清单、补货数量输入、提交补货 |
+| 绑定微信 | `pages/replenish/bind` | 通过小程序码/绑定码参数进入,完成微信授权绑定 |
+| 补货员管理 | `pages/replenisher/list` | 管理员查看补货员列表 |
+| 编辑补货员 | `pages/replenisher/form` | 管理员新增/编辑补货员 |
+| 设备绑定 | `pages/replenisher/bind-device` | 管理员为补货员绑定设备(门店设备树) |
+| 配置补货员 | `pages/device/replenisher-config` | 从设备端配置其绑定的补货员 |
+
+### 4.3 扫码开柜
+
+补货首页提供"扫码开柜"按钮,补货员可扫描设备机身二维码直接进入补货操作:
+
+1. 点击「扫码开柜」→ 调用 `uni.scanCode` 扫描设备二维码
+2. 机身二维码格式:`https://dev-haha.kuaiyuman.cn/{deviceId}`
+3. 解析提取 deviceId → 校验是否在绑定设备列表中
+4. 跳转 `/pages/replenish/operation?deviceId=xxx` 执行补货
+
+---
+
+## 五、数据库变更记录
+
+| 文件 | 说明 |
+|------|------|
+| `docs/database/migrate_replenisher.sql` | V1 `t_stocker` → V2 `t_replenisher` 数据迁移 |
+| `docs/database/migrate_replenishment_to_inventory.sql` | 补货单菜单注册到库存管理模块下 |
+| `haha-admin/src/main/resources/sql/replenishment_order.sql` | `t_replenishment_order` 和 `t_replenishment_order_item` 建表 DDL |
+
+---
+
+## 六、技术要点
+
+### 6.1 库存操作
+
+补货操作调用 `DeviceInventoryService.increaseStock()` 增加库存,该方法:
+
+- 若库存记录不存在则创建(stock = quantity)
+- 若已存在则累加(stock = beforeStock + quantity)
+- 无上限限制
+- 写入 `t_inventory_log` 记录变动
+- 使用乐观锁(version 字段)防止并发冲突
+
+### 6.2 绑定码机制
+
+- 管理员在 Web 后台补货员列表点击「绑定码」按钮调用 `POST /replenishers/{id}/binding-code`
+- 后端生成 24 位随机字符串(UUID 截取)存入 Redis,Key 格式为 `replenisher:bind:code:{code}`,TTL 24 小时
+- 将绑定码告知补货员,补货员在运营端小程序登录页输入绑定码完成微信绑定
+- 绑定码一次性使用,验证后立即从 Redis 删除
+
+### 6.3 补货单号规则
+
+- 格式:`RO` + `yyyyMMdd` + 4位自增序号(如 `RO202507060001`)
+- 通过查询当天最大单号 + 1 生成
+- 数据库 `order_no` 字段有 UNIQUE 索引保证唯一性
+- 日订单量超过 9999 会报错
+
+### 6.4 ERP 集成状态
+
+`syncToErp` 接口目前为桩代码,日志记录"模拟同步ERP成功"。企得宝 SDK 已在 `haha-sdk` 模块中预留,后续接入时需实现真实推送逻辑。
+
+---
+
+## 七、相关文件索引
+
+### 后端核心文件
+
+```
+haha-entity/src/main/java/com/haha/entity/
+├── Replenisher.java                    # 补货员实体
+├── ReplenisherDevice.java              # 设备绑定实体
+├── ReplenishmentOrder.java             # 补货单实体
+├── ReplenishmentOrderItem.java         # 补货单明细实体
+└── dto/
+    ├── ReplenishDTO.java               # 补货操作请求 DTO
+    ├── ReplenisherCreateDTO.java       # 创建补货员 DTO
+    ├── ReplenisherUpdateDTO.java       # 更新补货员 DTO
+    ├── ReplenisherQueryDTO.java        # 补货员查询 DTO
+    ├── ReplenisherBindDTO.java         # 微信绑定 DTO
+    ├── BindReplenisherDTO.java         # 门店绑定补货员 DTO
+    ├── ReplenishmentOrderCreateDTO.java    # 创建补货单 DTO
+    ├── ReplenishmentOrderUpdateDTO.java    # 更新补货单 DTO
+    └── ReplenishmentOrderQueryDTO.java     # 补货单查询 DTO
+
+haha-admin/src/main/java/com/haha/admin/controller/
+├── ReplenisherController.java          # 补货员管理接口
+├── ReplenisherLoginController.java     # 补货员认证接口
+├── ReplenisherOperationController.java # 补货员操作接口
+└── ReplenishmentOrderController.java   # 补货单管理接口
+
+haha-service/src/main/java/com/haha/service/
+├── ReplenisherService.java             # 补货员服务接口
+├── ReplenishmentOrderService.java      # 补货单服务接口
+└── impl/
+    ├── ReplenisherServiceImpl.java     # 补货员服务实现
+    └── ReplenishmentOrderServiceImpl.java  # 补货单服务实现
+
+haha-mapper/src/main/java/com/haha/mapper/
+├── ReplenisherMapper.java
+├── ReplenisherDeviceMapper.java
+├── ReplenishmentOrderMapper.java
+└── ReplenishmentOrderItemMapper.java
+
+haha-common/src/main/java/com/haha/common/enums/
+└── ReplenishmentOrderStatusEnum.java   # 补货单状态枚举
+```
+
+### 前端核心文件
+
+```
+haha-admin-web/src/
+├── views/replenisher/                  # 补货员管理页面
+│   ├── index.vue
+│   └── utils/
+│       ├── hook.tsx
+│       └── types.ts
+├── views/replenishment-order/          # 补货单管理页面
+│   ├── index.vue
+│   ├── detail.vue
+│   └── components/
+│       └── CreateDialog.vue
+└── api/
+    ├── replenisher.ts
+    └── replenishmentOrder.ts
+
+haha-admin-mp/src/
+├── pages/replenish/                    # 补货员小程序页面
+│   ├── index.vue
+│   ├── operation.vue
+│   └── bind.vue
+├── pages/replenisher/                  # 补货员管理页面
+│   ├── list.vue
+│   ├── form.vue
+│   └── bind-device.vue
+├── pages/device/
+│   └── replenisher-config.vue
+└── api/
+    ├── replenish.ts
+    └── replenisher.ts
+
+haha-mp/src/
+└── pages/replenish/
+    └── bind.vue                       # 用户端绑定页面
+```

+ 65 - 1
haha-admin-mp/src/pages/replenish/index.vue

@@ -44,7 +44,13 @@
     <view class="device-section" v-else>
       <view class="section-header">
         <text class="section-title">我的设备</text>
-        <text class="section-count" v-if="deviceList.length">共 {{ deviceList.length }} 台</text>
+        <view class="section-actions">
+          <view class="scan-btn" @click="handleScanDevice">
+            <text class="scan-icon">📷</text>
+            <text class="scan-text">扫码开柜</text>
+          </view>
+          <text class="section-count" v-if="deviceList.length">共 {{ deviceList.length }} 台</text>
+        </view>
       </view>
 
       <!-- 空状态 -->
@@ -150,6 +156,35 @@ const getStockRatio = (device: any): number => {
   return Math.min(ratio, 100);
 };
 
+const handleScanDevice = () => {
+  uni.scanCode({
+    onlyFromCamera: false,
+    scanType: ['qrCode'],
+    success: (res: any) => {
+      const result = res.result || '';
+      // 机身二维码格式: https://dev-haha.kuaiyuman.cn/{deviceId}
+      const deviceIdMatch = result.match(/\/([^/]+)$/);
+      if (!deviceIdMatch) {
+        uni.showToast({ title: '无效的设备二维码', icon: 'none' });
+        return;
+      }
+      const scannedDeviceId = deviceIdMatch[1];
+      // 校验设备是否在补货员绑定列表中
+      const found = deviceList.value.find((d: any) => d.deviceId === scannedDeviceId);
+      if (!found) {
+        uni.showToast({ title: '该设备未绑定,请联系管理员', icon: 'none' });
+        return;
+      }
+      uni.navigateTo({
+        url: `/pages/replenish/operation?deviceId=${scannedDeviceId}`
+      });
+    },
+    fail: () => {
+      // 用户取消扫码,不做提示
+    }
+  });
+};
+
 onMounted(async () => {
   try {
     // 获取补货员信息
@@ -307,6 +342,35 @@ onMounted(async () => {
     color: $text-color-primary;
   }
 
+  .section-actions {
+    display: flex;
+    align-items: center;
+    gap: 16rpx;
+
+    .scan-btn {
+      display: flex;
+      align-items: center;
+      gap: 6rpx;
+      padding: 10rpx 20rpx;
+      background: $primary-color;
+      border-radius: 10rpx;
+
+      .scan-icon {
+        font-size: 24rpx;
+      }
+
+      .scan-text {
+        font-size: 24rpx;
+        font-weight: 600;
+        color: #fff;
+      }
+
+      &:active {
+        opacity: 0.85;
+      }
+    }
+  }
+
   .section-count {
     font-size: 24rpx;
     color: $text-color-muted;

+ 18 - 50
haha-admin-web/src/views/replenisher/index.vue

@@ -303,22 +303,14 @@ const {
             <div class="binding-code-hint">点击绑定码可复制</div>
           </div>
 
-          <!-- 二维码展示 -->
-          <div v-if="bindingCodeData.qrCodeDataUrl" class="qr-section">
-            <div class="section-label">绑定二维码</div>
-            <div class="qr-wrapper">
-              <img :src="bindingCodeData.qrCodeDataUrl" alt="绑定二维码" class="qr-image" />
-            </div>
-            <div class="qr-hint">微信扫码后自动打开小程序绑定页</div>
-          </div>
-
-          <!-- 手动绑定指引 -->
+          <!-- 操作指引 -->
           <div class="manual-section">
-            <div class="section-label">手动绑定方式</div>
+            <div class="section-label">补货员操作步骤</div>
             <div class="manual-content">
-              <div class="manual-step">1. 补货员打开"哈哈运营平台"小程序</div>
-              <div class="manual-step">2. 登录页点击"已有绑定码?点击绑定微信"</div>
-              <div class="manual-step">3. 输入上方24位绑定码完成绑定</div>
+              <div class="manual-step">1. 打开"哈哈运营平台"小程序</div>
+              <div class="manual-step">2. 在登录页点击"已有绑定码?点击绑定微信"</div>
+              <div class="manual-step">3. 输入上方 24 位绑定码,授权微信完成绑定</div>
+              <div class="manual-step">4. 绑定成功后自动进入补货首页,即可开始补货</div>
             </div>
           </div>
         </template>
@@ -485,50 +477,26 @@ const {
     }
   }
 
-  .qr-section {
-    margin-bottom: 24px;
-
-    .qr-wrapper {
-      display: flex;
-      justify-content: center;
-      background: #fff;
-      border: 1px solid var(--el-border-color-lighter);
-      border-radius: 8px;
-      padding: 16px;
-      width: fit-content;
-      margin: 0 auto;
-
-      .qr-image {
-        width: 200px;
-        height: 200px;
-        display: block;
-      }
-    }
-
-    .qr-hint {
-      font-size: 12px;
-      color: var(--el-text-color-placeholder);
-      margin-top: 6px;
-      text-align: center;
-    }
-  }
-
   .manual-section {
     margin-bottom: 8px;
 
+    .section-label {
+      font-size: 14px;
+      font-weight: 600;
+      color: var(--el-text-color-primary);
+      margin-bottom: 10px;
+    }
+
     .manual-content {
-      background: var(--el-fill-color-lighter);
-      border-radius: 6px;
-      padding: 12px 16px;
+      background: var(--el-color-primary-light-9);
+      border: 1px solid var(--el-color-primary-light-7);
+      border-radius: 8px;
+      padding: 14px 18px;
 
       .manual-step {
         font-size: 13px;
         color: var(--el-text-color-secondary);
-        line-height: 1.8;
-
-        &:hover {
-          color: var(--el-text-color-primary);
-        }
+        line-height: 2;
       }
     }
   }

+ 1 - 22
haha-admin-web/src/views/replenisher/utils/hook.tsx

@@ -2,7 +2,6 @@ import dayjs from "dayjs";
 import { message } from "@/utils/message";
 import { addDialog } from "@/components/ReDialog";
 import { usePublicHooks } from "@/views/system/hooks";
-import QRCode from "qrcode";
 import { deviceDetection } from "@pureadmin/utils";
 import type { PaginationProps } from "@pureadmin/table";
 import { getReplenisherList,
@@ -331,18 +330,15 @@ export function useReplenisher(tableRef: Ref) {
   const bindingCodeData = reactive<{
     replenisherName: string;
     bindingCode: string;
-    qrCodeDataUrl: string;
   }>({
     replenisherName: "",
-    bindingCode: "",
-    qrCodeDataUrl: ""
+    bindingCode: ""
   });
   const generatingCode = ref(false);
 
   function openBindingCodeDialog(row: ReplenisherFormItem) {
     bindingCodeData.replenisherName = row.name;
     bindingCodeData.bindingCode = "";
-    bindingCodeData.qrCodeDataUrl = "";
     bindingDialogVisible.value = true;
     handleGenerateAndShowBindingCode(row);
   }
@@ -365,23 +361,6 @@ export function useReplenisher(tableRef: Ref) {
         const code = res.data.bindingCode;
         bindingCodeData.bindingCode = code;
 
-        // 生成二维码(将绑定码编码到URL中,扫码后打开小程序)
-        try {
-          // 使用完整小程序路径:/pages/replenish/bind?code=xxx
-          const miniProgramPath = `/pages/replenish/bind?code=${code}`;
-          bindingCodeData.qrCodeDataUrl = await QRCode.toDataURL(miniProgramPath, {
-            width: 280,
-            margin: 2,
-            color: {
-              dark: "#1e293b",
-              light: "#ffffff"
-            }
-          });
-        } catch (qrErr) {
-          console.error("生成二维码失败:", qrErr);
-          bindingCodeData.qrCodeDataUrl = "";
-        }
-
         // 刷新列表更新 wechatOpenid 状态
         onSearch();
       } else {

+ 0 - 32
haha-mp/src/App.vue

@@ -10,40 +10,9 @@ import { logger } from './utils/logger';
  */
 onLaunch((options: any) => {
   logger.log("App Launch, options:", JSON.stringify(options));
-  checkBindingCode(options);
   checkLoginStatus(options);
 });
 
-/**
- * 检测启动参数中的补货员绑定码
- * 场景:通过管理后台生成的二维码进入小程序时,会携带 scene/bindingCode 参数
- */
-const checkBindingCode = (options: any) => {
-  if (!options) return;
-
-  // 检查 scene 场景值(小程序码场景)
-  if (options.scene) {
-    const scene = decodeURIComponent(options.scene);
-    logger.log('[App] 检测到场景值:', scene);
-    // 24位大写字母数字组合为绑定码格式
-    if (/^[A-Z0-9]{24}$/.test(scene)) {
-      logger.log('[App] 场景值为补货员绑定码');
-      uni.navigateTo({
-        url: '/pages/replenish/bind?code=' + scene
-      });
-      return;
-    }
-  }
-
-  // 检查 query 参数中的绑定码
-  if (options.query && options.query.bindingCode) {
-    logger.log('[App] 检测到补货员绑定码:', options.query.bindingCode);
-    uni.navigateTo({
-      url: '/pages/replenish/bind?code=' + encodeURIComponent(options.query.bindingCode)
-    });
-  }
-};
-
 /**
  * 应用显示
  */
@@ -72,7 +41,6 @@ onHide(() => {
 const LOGIN_WHITE_LIST = [
   'pages/login/login',
   'pages/products/products',
-  'pages/replenish/bind',
 ];
 
 /**

+ 0 - 32
haha-mp/src/api/replenish.ts

@@ -1,32 +0,0 @@
-/**
- * 补货员相关API
- */
-import { post, get } from '../utils/request';
-
-export interface LoginResponse {
-  token: string;
-  userInfo: any;
-}
-
-/**
- * 补货员微信静默登录
- * @param params { code: 微信登录凭证 }
- */
-export const loginByWechat = (params: { code: string }): Promise<LoginResponse> => {
-  return post<LoginResponse>('/replenisher/login/wechat', params);
-};
-
-/**
- * 补货员扫码绑定微信
- * @param params { bindingCode: 绑定码, code: 微信登录凭证 }
- */
-export const bindWechat = (params: { bindingCode: string; code: string }): Promise<LoginResponse> => {
-  return post<LoginResponse>('/replenisher/login/bind', params);
-};
-
-/**
- * 获取当前补货员信息
- */
-export const getMyInfo = (): Promise<any> => {
-  return get('/replenisher/my-info');
-};

+ 0 - 8
haha-mp/src/pages.json

@@ -120,14 +120,6 @@
 				"navigationBarBackgroundColor": "#FFD700",
 				"navigationBarTextStyle": "black"
 			}
-		},
-		{
-			"path": "pages/replenish/bind",
-			"style": {
-				"navigationBarTitleText": "上货员绑定",
-				"navigationBarBackgroundColor": "#FFD700",
-				"navigationBarTextStyle": "black"
-			}
 		}
 	],
 	"easycom": {

+ 0 - 566
haha-mp/src/pages/replenish/bind.vue

@@ -1,566 +0,0 @@
-<template>
-  <view class="container">
-    <!-- 顶部品牌区 -->
-    <view class="brand-section">
-      <view class="brand-icon">
-        <view class="brand-icon-inner">
-          <text class="brand-icon-text">HH</text>
-        </view>
-      </view>
-      <text class="brand-title">哈哈零售</text>
-      <text class="brand-subtitle">设备上货员·微信绑定</text>
-    </view>
-
-    <!-- 绑定卡片 -->
-    <view class="bind-card" v-if="!bound">
-      <!-- 绑定码卡片 -->
-      <view class="code-card">
-        <view class="code-header">
-          <text class="code-label">绑定码</text>
-          <text class="code-expire">24小时内有效</text>
-        </view>
-        <text class="code-value">{{ displayCode }}</text>
-      </view>
-
-      <!-- 绑定流程步骤 -->
-      <view class="steps">
-        <view class="step-item" :class="{ active: step === 1, done: step > 1 }">
-          <view class="step-circle">
-            <text class="step-number" v-if="step === 0">1</text>
-            <text class="step-check" v-else-if="step > 0">✓</text>
-          </view>
-          <view class="step-content">
-            <text class="step-title">获取绑定码</text>
-            <text class="step-desc">由管理员在后台生成</text>
-          </view>
-        </view>
-        <view class="step-line" :class="{ active: step >= 1 }"></view>
-        <view class="step-item" :class="{ active: step === 2, done: step > 2 }">
-          <view class="step-circle">
-            <text class="step-number" v-if="step <= 1">2</text>
-            <text class="step-check" v-else>✓</text>
-          </view>
-          <view class="step-content">
-            <text class="step-title">微信授权绑定</text>
-            <text class="step-desc">授权微信账号完成绑定</text>
-          </view>
-        </view>
-        <view class="step-line" :class="{ active: step >= 2 }"></view>
-        <view class="step-item" :class="{ active: step === 3 }">
-          <view class="step-circle">
-            <text class="step-number">3</text>
-          </view>
-          <view class="step-content">
-            <text class="step-title">绑定成功</text>
-            <text class="step-desc">进入上货员工作台</text>
-          </view>
-        </view>
-      </view>
-
-      <!-- 绑定按钮 -->
-      <button
-        class="bind-btn"
-        :disabled="bindLoading || !bindingCode"
-        :loading="bindLoading"
-        hover-class="bind-btn-hover"
-        @tap="handleBind"
-      >
-        <text v-if="!bindLoading && hasValidCode">绑定微信</text>
-        <text v-else-if="!hasValidCode">绑定码无效</text>
-        <text v-else>绑定中...</text>
-      </button>
-
-      <text class="back-link" @tap="goBack">返回首页</text>
-    </view>
-
-    <!-- 成功状态 -->
-    <view class="success-section" v-else>
-      <view class="success-icon">
-        <text class="success-check">✓</text>
-      </view>
-      <text class="success-title">绑定成功</text>
-      <text class="success-desc">即将进入上货员工作台...</text>
-      <view class="success-dots">
-        <view class="dot" v-for="i in 3" :key="i"></view>
-      </view>
-    </view>
-  </view>
-</template>
-
-<script setup lang="ts">
-import { ref, computed, onMounted } from 'vue';
-import { onLoad } from '@dcloudio/uni-app';
-import { bindWechat, loginByWechat } from '@/api/replenish';
-import { logger } from '@/utils/logger';
-
-const bindingCode = ref('');
-const bindLoading = ref(false);
-const bound = ref(false);
-const step = ref(0);
-
-const hasValidCode = computed(() => {
-  return bindingCode.value && bindingCode.value.length >= 4;
-});
-
-const displayCode = computed(() => {
-  const code = bindingCode.value;
-  if (!code) return '---';
-  return code.replace(/(.{4})/g, '$1 ').trim();
-});
-
-onLoad((options: any) => {
-  logger.log('[补货员绑定] onLoad options:', options);
-
-  // 从 scene 场景值或 query 参数获取绑定码
-  if (options.scene) {
-    const scene = decodeURIComponent(options.scene);
-    if (/^[A-Z0-9]{24}$/.test(scene)) {
-      bindingCode.value = scene;
-      step.value = 1;
-      logger.log('[补货员绑定] 从场景值获取绑定码:', scene);
-      return;
-    }
-  }
-
-  if (options.code) {
-    bindingCode.value = options.code;
-    step.value = 1;
-    logger.log('[补货员绑定] 从参数获取绑定码:', options.code);
-    return;
-  }
-
-  if (options.bindingCode) {
-    bindingCode.value = options.bindingCode;
-    step.value = 1;
-    logger.log('[补货员绑定] 从参数获取绑定码:', options.bindingCode);
-    return;
-  }
-
-  // 无绑定码参数时提示
-  uni.showToast({ title: '缺少绑定码参数', icon: 'none' });
-});
-
-const goBack = () => {
-  uni.reLaunch({ url: '/pages/index/index' });
-};
-
-const handleBind = async () => {
-  if (!hasValidCode.value) {
-    uni.showToast({ title: '绑定码无效', icon: 'none' });
-    return;
-  }
-
-  bindLoading.value = true;
-  step.value = 2;
-
-  try {
-    // 1. 微信授权获取code
-    logger.log('[补货员绑定] 开始微信授权...');
-    const loginRes = await new Promise<any>((resolve, reject) => {
-      uni.login({
-        provider: 'weixin',
-        success: (res) => {
-          logger.log('[补货员绑定] 微信授权成功:', res);
-          resolve(res);
-        },
-        fail: (err) => {
-          logger.error('[补货员绑定] 微信授权失败:', err);
-          reject(err);
-        }
-      });
-    });
-
-    if (!loginRes.code) {
-      uni.showToast({ title: '微信授权失败', icon: 'none' });
-      step.value = 1;
-      return;
-    }
-
-    // 2. 调用绑定接口
-    logger.log('[补货员绑定] 开始调用绑定接口...');
-    const response = await bindWechat({
-      bindingCode: bindingCode.value.trim().toUpperCase(),
-      code: loginRes.code
-    });
-
-    // 3. 保存登录信息
-    if (response.token) {
-      uni.setStorageSync('accessToken', response.token);
-      uni.setStorageSync('haha-user-info', response.userInfo);
-    }
-
-    // 4. 显示成功状态
-    bound.value = true;
-    step.value = 3;
-
-    setTimeout(() => {
-      // 跳转到首页,补货员登录后在首页可能会有对应的入口
-      uni.reLaunch({ url: '/pages/index/index' });
-    }, 2000);
-  } catch (error: any) {
-    logger.error('[补货员绑定] 绑定失败:', error);
-    step.value = 1;
-    // 错误已在 request.ts 中统一 toast
-  } finally {
-    bindLoading.value = false;
-  }
-};
-</script>
-
-<style lang="scss">
-.container {
-  min-height: 100vh;
-  background: $color-bg-secondary;
-  display: flex;
-  flex-direction: column;
-  align-items: center;
-  padding-bottom: calc(80rpx + env(safe-area-inset-bottom));
-}
-
-/* ===== 品牌区域 ===== */
-.brand-section {
-  display: flex;
-  flex-direction: column;
-  align-items: center;
-  padding: 80rpx 0 48rpx;
-  animation: slideDown 0.3s $ease-out;
-
-  .brand-icon {
-    width: 120rpx;
-    height: 120rpx;
-    background: linear-gradient(135deg, $color-primary 0%, $color-primary-dark 100%);
-    border-radius: $radius-xl;
-    display: flex;
-    align-items: center;
-    justify-content: center;
-    margin-bottom: 24rpx;
-    box-shadow: $shadow-primary;
-
-    .brand-icon-inner {
-      .brand-icon-text {
-        font-size: 36rpx;
-        font-weight: 700;
-        color: #fff;
-        letter-spacing: 4rpx;
-      }
-    }
-  }
-
-  .brand-title {
-    font-size: 36rpx;
-    font-weight: 700;
-    color: $color-text-primary;
-    margin-bottom: 8rpx;
-  }
-
-  .brand-subtitle {
-    font-size: 26rpx;
-    color: $color-text-secondary;
-  }
-}
-
-/* ===== 绑定卡片 ===== */
-.bind-card {
-  width: 100%;
-  max-width: 600rpx;
-  padding: 0 32rpx;
-  animation: slideUp 0.3s $ease-out 0.1s both;
-}
-
-/* 绑定码展示 */
-.code-card {
-  background: $color-bg-primary;
-  border: 2rpx dashed $color-primary;
-  border-radius: $radius-lg;
-  padding: 32rpx;
-  text-align: center;
-  margin-bottom: 40rpx;
-  box-shadow: $shadow-sm;
-
-  .code-header {
-    display: flex;
-    align-items: center;
-    justify-content: center;
-    gap: 16rpx;
-    margin-bottom: 20rpx;
-
-    .code-label {
-      font-size: 24rpx;
-      color: $color-text-secondary;
-    }
-
-    .code-expire {
-      font-size: 22rpx;
-      color: $color-text-tertiary;
-      padding: 4rpx 12rpx;
-      background: $color-primary-bg;
-      border-radius: $radius-sm;
-    }
-  }
-
-  .code-value {
-    display: block;
-    font-size: 38rpx;
-    font-weight: 700;
-    color: $color-text-primary;
-    letter-spacing: 8rpx;
-    font-family: 'Courier New', Courier, monospace;
-  }
-}
-
-/* ===== 步骤流程 ===== */
-.steps {
-  padding: 0 16rpx;
-  margin-bottom: 48rpx;
-
-  .step-item {
-    display: flex;
-    align-items: flex-start;
-    padding: 16rpx 0;
-    opacity: 0.4;
-    transition: opacity $duration-normal $ease-out;
-
-    &.active {
-      opacity: 1;
-
-      .step-circle {
-        background: $color-primary;
-        border-color: $color-primary;
-      }
-
-      .step-title {
-        color: $color-text-primary;
-        font-weight: 600;
-      }
-    }
-
-    &.done {
-      opacity: 0.7;
-
-      .step-circle {
-        background: $color-success;
-        border-color: $color-success;
-      }
-
-      .step-title {
-        color: $color-text-secondary;
-      }
-    }
-
-    .step-circle {
-      width: 48rpx;
-      height: 48rpx;
-      border-radius: $radius-circle;
-      border: 2rpx solid $color-border;
-      display: flex;
-      align-items: center;
-      justify-content: center;
-      flex-shrink: 0;
-      margin-right: 20rpx;
-      transition: all $duration-normal $ease-out;
-
-      .step-number {
-        font-size: 24rpx;
-        font-weight: 600;
-        color: $color-text-tertiary;
-      }
-
-      .step-check {
-        font-size: 24rpx;
-        color: #fff;
-        font-weight: 700;
-      }
-    }
-
-    .step-content {
-      flex: 1;
-      padding-top: 6rpx;
-
-      .step-title {
-        font-size: 28rpx;
-        color: $color-text-tertiary;
-        margin-bottom: 4rpx;
-        transition: color $duration-normal $ease-out;
-      }
-
-      .step-desc {
-        font-size: 22rpx;
-        color: $color-text-tertiary;
-      }
-    }
-  }
-
-  .step-line {
-    width: 2rpx;
-    height: 40rpx;
-    background: $color-border;
-    margin-left: 23rpx;
-    transition: background $duration-normal $ease-out;
-
-    &.active {
-      background: $color-primary;
-    }
-  }
-}
-
-/* ===== 绑定按钮 ===== */
-.bind-btn {
-  width: 100%;
-  height: 100rpx;
-  background: linear-gradient(135deg, $color-primary 0%, $color-primary-dark 100%);
-  border-radius: 50rpx;
-  border: none;
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  box-shadow: $shadow-primary;
-  transition: all $duration-fast $ease-out;
-
-  &::after {
-    border: none;
-  }
-
-  text {
-    font-size: 32rpx;
-    font-weight: 600;
-    color: #1A1A1A;
-    letter-spacing: 4rpx;
-  }
-
-  &.bind-btn-hover {
-    opacity: 0.9;
-    transform: scale(0.98);
-  }
-
-  &[disabled] {
-    opacity: 0.5;
-    box-shadow: none;
-  }
-}
-
-.back-link {
-  display: block;
-  text-align: center;
-  margin-top: 32rpx;
-  font-size: 26rpx;
-  color: $color-text-tertiary;
-  padding: 16rpx;
-
-  &:active {
-    opacity: 0.6;
-  }
-}
-
-/* ===== 成功状态 ===== */
-.success-section {
-  display: flex;
-  flex-direction: column;
-  align-items: center;
-  justify-content: center;
-  min-height: 70vh;
-  animation: fadeIn 0.4s $ease-out;
-
-  .success-icon {
-    width: 160rpx;
-    height: 160rpx;
-    background: $color-primary-bg;
-    border-radius: $radius-circle;
-    display: flex;
-    align-items: center;
-    justify-content: center;
-    margin-bottom: 32rpx;
-    animation: pop-in 0.3s $ease-out;
-
-    .success-check {
-      font-size: 72rpx;
-      color: $color-primary-dark;
-      font-weight: 700;
-    }
-  }
-
-  .success-title {
-    font-size: 40rpx;
-    font-weight: 700;
-    color: $color-text-primary;
-    margin-bottom: 12rpx;
-    animation: slideUp 0.4s $ease-out 0.3s both;
-  }
-
-  .success-desc {
-    font-size: 28rpx;
-    color: $color-text-secondary;
-    animation: slideUp 0.4s $ease-out 0.4s both;
-  }
-
-  .success-dots {
-    display: flex;
-    gap: 12rpx;
-    margin-top: 40rpx;
-
-    .dot {
-      width: 12rpx;
-      height: 12rpx;
-      border-radius: $radius-circle;
-      background: $color-primary;
-      animation: loadingDot 1.2s ease-in-out infinite;
-
-      &:nth-child(1) { animation-delay: 0s; }
-      &:nth-child(2) { animation-delay: 0.2s; }
-      &:nth-child(3) { animation-delay: 0.4s; }
-    }
-  }
-}
-
-/* ===== 动画 ===== */
-@keyframes slideDown {
-  from {
-    opacity: 0;
-    transform: translateY(-40rpx);
-  }
-  to {
-    opacity: 1;
-    transform: translateY(0);
-  }
-}
-
-@keyframes slideUp {
-  from {
-    opacity: 0;
-    transform: translateY(40rpx);
-  }
-  to {
-    opacity: 1;
-    transform: translateY(0);
-  }
-}
-
-@keyframes fadeIn {
-  from { opacity: 0; }
-  to { opacity: 1; }
-}
-
-@keyframes pop-in {
-  0% {
-    transform: scale(0);
-    opacity: 0;
-  }
-  50% {
-    transform: scale(1.1);
-  }
-  100% {
-    transform: scale(1);
-    opacity: 1;
-  }
-}
-
-@keyframes loadingDot {
-  0%, 80%, 100% {
-    transform: scale(0.6);
-    opacity: 0.4;
-  }
-  40% {
-    transform: scale(1);
-    opacity: 1;
-  }
-}
-</style>

+ 0 - 5
haha-mp/src/utils/config.ts

@@ -72,9 +72,4 @@ export const API_PATHS = {
   // 公告相关
   getAnnouncementList: '/announcement/list',
   getAnnouncementDetail: '/announcement/detail',
-  
-  // 补货员相关
-  replenisherWechatLogin: '/replenisher/login/wechat',
-  replenisherBind: '/replenisher/login/bind',
-  replenisherMyInfo: '/replenisher/my-info'
 };