# 上货员(补货员)模块说明 > 本文档描述系统上货员模块的完整架构,涵盖独立补货员体系、微信绑定流程、补货操作流程及相关实现。 > 帮助开发人员理解"为什么这样设计"以及各环节的特殊处理方式。 --- ## 一、模块概述 ### 1.1 什么是上货员/补货员? 上货员(系统内称"补货员",Replenisher)指负责为智能售卖柜补货的工作人员。他们通过手机小程序查看库存、执行补货操作。 ### 1.2 独立补货员体系(V2) 系统使用独立补货员体系,核心表: | 体系 | 数据表 | 说明 | |------|--------|------| | **独立补货员** | `t_replenisher` + `t_replenisher_device` | 独立账号体系,设备级绑定粒度,微信扫码绑定 | > **历史背景**:早期存在基于 `t_shop_replenisher` + `t_admin` 的 V1 体系(补货员需是系统管理员),已于 2026年4月 完成迁移清理,V1 表 `t_shop_replenisher` 及所有相关代码已彻底移除。 ### 1.3 为什么使用独立补货员体系? 独立补货员体系相比旧的 V1(基于 `t_admin` 管理员表)解决了以下问题: - 补货员必须是系统管理员,具备过多后台权限 - 粒度仅到门店级别,无法精确控制设备权限 - 全凭管理员账号密码登录,补货员使用体验差 V2 独立补货员体系的核心优势: - **独立账号**:`t_replenisher` 表独立管理,不与管理员混合 - **设备级绑定**:通过 `t_replenisher_device` 表实现设备级粒度绑定 - **微信扫码绑定**:后台创建账号 → 生成绑定码 → 微信扫码绑定,纯手机端操作 --- ## 二、数据库设计 ### 2.1 核心表 #### `t_replenisher` — 补货员表 | 字段 | 类型 | 说明 | |------|------|------| | `id` | BIGINT | 主键,雪花算法 | | `name` | VARCHAR(50) | 姓名 | | `phone` | VARCHAR(20) | 手机号(可选) | | `employee_id` | VARCHAR(50) | 工号(可选) | | `wechat_openid` | VARCHAR(100) | 微信 OpenID(扫码绑定时回填) | | `avatar` | VARCHAR(500) | 头像 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(50) | 设备SN号 | | `source` | VARCHAR(20) | 绑定来源:`MANUAL`-手动,`QR`-扫码绑定,`SHOP_INHERIT`-门店继承 | | `create_time` | DATETIME | 创建时间 | ### 2.2 补货记录表 #### `t_stock_record` — 上货/补货记录表 | 字段 | 类型 | 说明 | |------|------|------| | `id` | BIGINT | 主键,雪花算法 | | `device_id` | VARCHAR(50) | 设备ID | | `activity_id` | VARCHAR(100) | 哈哈平台活动号 | | `stock_type` | INT | 类型:1-上货,2-补货 | | `stocker_id` | BIGINT | 上货员ID | | `stocker_name` | VARCHAR(50) | 上货员名称 | | `stocker_phone` | VARCHAR(20) | 上货员电话 | | `total_items` | INT | 上货商品种类数 | | `total_quantity` | INT | 上货总数量 | | `status` | INT | 状态:1-进行中,2-已完成,3-已取消 | | `start_time` | DATETIME | 开始时间 | | `end_time` | DATETIME | 结束时间 | | `remark` | VARCHAR(500) | 备注 | | `create_time` | DATETIME | 创建时间 | | `update_time` | DATETIME | 更新时间 | #### `t_stock_record_item` — 补货明细表 | 字段 | 类型 | 说明 | |------|------|------| | `id` | BIGINT | 主键,雪花算法 | | `record_id` | BIGINT | 上货记录ID | | `device_id` | VARCHAR(50) | 设备ID | | `product_id` | BIGINT | 商品ID | | `product_code` | VARCHAR(50) | 商品编码 | | `product_name` | VARCHAR(100) | 商品名称 | | `shelf_num` | INT | 货架层号 | | `quantity` | INT | 上货数量 | | `before_stock` | INT | 上货前库存 | | `after_stock` | INT | 上货后库存 | | `create_time` | DATETIME | 创建时间 | ### 2.3 数据迁移 `docs/database/migrate_replenisher.sql` 提供了从旧表 `t_stocker` 到 `t_replenisher` 的数据迁移脚本。 > V1 表 `t_shop_replenisher` 及所有相关后端/前端代码已于 2026年4月 彻底清理移除。 --- ## 三、后端实现 ### 3.1 模块目录结构 ``` haha-entity ├── Replenisher.java # 补货员实体 ├── ReplenisherDevice.java # 补货员-设备关联实体 ├── StockRecord.java # 补货记录实体 ├── StockRecordItem.java # 补货明细实体 ├── dto/ │ ├── ReplenisherCreateDTO.java # 创建补货员 │ ├── ReplenisherUpdateDTO.java # 更新补货员 │ ├── ReplenisherQueryDTO.java # 分页查询参数 │ ├── ReplenisherBindDTO.java # 微信扫码绑定 │ ├── BindReplenisherDTO.java # 绑定补货员到设备/门店 │ ├── ReplenishDTO.java # 执行补货操作 │ ├── BindDeviceDTO.java # 绑定设备 │ ├── BatchBindDevicesDTO.java # 批量绑定设备 │ ├── StatusDTO.java # 通用状态更新 │ └── WechatLoginDTO.java # 微信登录 haha-mapper ├── ReplenisherMapper.java # 补货员 Mapper ├── ReplenisherDeviceMapper.java # 关联 Mapper ├── resources/mapper/ │ ├── ReplenisherMapper.xml # 分页带设备绑定数查询 │ └── ReplenisherDeviceMapper.xml # 批量插入忽略已存在 haha-service ├── ReplenisherService.java # 补货员服务接口 ├── impl/ │ └── ReplenisherServiceImpl.java # 补货员服务实现 haha-admin (运营平台) ├── controller/ │ ├── ReplenisherController.java # 补货员 CRUD 管理 │ ├── ReplenisherLoginController.java # 微信登录/绑定 │ └── ReplenisherOperationController.java # 补货操作(设备查询/库存/补货) haha-admin-web (前端) ├── src/api/replenisher.ts # API 接口层 ├── src/views/replenisher/ │ ├── index.vue # 补货员管理页面 │ └── utils/ │ ├── hook.tsx # 逻辑钩子 │ └── types.ts # 类型定义 haha-admin-mp (管理端小程序) ├── src/api/replenish.ts # 补货员 API ├── src/pages/ │ ├── login/login.vue # 微信登录 + 绑定码入口 │ └── replenish/ │ ├── bind.vue # 微信绑定页 │ └── index.vue / ... # 补货工作台 haha-mp (用户端小程序) ├── src/api/replenish.ts # 补货员 API ├── src/pages/replenish/bind.vue # 微信绑定页 ``` ### 3.2 核心实体关系 ``` ┌─────────────┐ ┌──────────────────────┐ ┌──────────┐ │ t_replenisher│────<│ t_replenisher_device │────>│ t_device │ │ (补货员) │ │ (补货员-设备关联) │ │ (设备) │ └─────────────┘ └──────────────────────┘ └──────────┘ │ │ (wechat_openid 绑定) │ ┌────┴─────┐ │ 微信 OpenID│ └──────────┘ ``` ### 3.3 API 接口总览 #### 3.3.1 补货员管理(运营平台,需要管理员权限) | 接口 | 方法 | 路径 | 权限 | 说明 | |------|------|------|------|------| | 分页列表 | GET | `/replenishers/list` | `replenisher:read` | 关键词+状态筛选,含设备绑定数 | | 详情 | GET | `/replenishers/{id}` | `replenisher:read` | 含设备绑定数 | | 搜索 | GET | `/replenishers/search` | `replenisher:read` | 仅搜索启用状态,用于下拉选择 | | 创建 | POST | `/replenishers` | `replenisher:create` | 姓名必填,手机号可选 | | 更新 | PUT | `/replenishers/{id}` | `replenisher:update` | 仅更新非空字段 | | 状态 | PUT | `/replenishers/{id}/status` | `replenisher:update` | 启用/禁用 | | 删除 | DELETE | `/replenishers/{id}` | `replenisher:delete` | 级联解绑设备 | | 绑定设备 | POST | `/replenishers/{id}/devices` | `replenisher:update` | 绑定设备到补货员 | | 解绑设备 | DELETE | `/replenishers/{id}/devices/{deviceId}` | `replenisher:update` | 解绑单个设备 | | 批量绑定 | POST | `/replenishers/batch-bind` | `replenisher:update` | 多补货员→多设备 | | **生成绑定码** | POST | `/replenishers/{id}/binding-code` | `replenisher:update` | **生成微信绑定码** | #### 3.3.2 门店补货员关联(运营平台) | 接口 | 方法 | 路径 | 权限 | 说明 | |------|------|------|------|------| | 获取门店补货员 | GET | `/shops/{id}/replenishers` | `shop:read` | 通过设备绑定反向推导门店关联的补货员 | | 绑定到门店 | POST | `/shops/{id}/replenishers` | `shop:update` | 将补货员绑定到门店下的所有设备 | #### 3.3.3 补货员登录/绑定(免登录) | 接口 | 方法 | 路径 | 说明 | |------|------|------|------| | 微信静默登录 | POST | `/replenisher/login/wechat` | 已绑定微信的补货员直接登录 | | 扫码绑定微信 | POST | `/replenisher/login/bind` | 绑定码 + 微信凭证进行绑定 | #### 3.3.4 补货员操作(需补货员身份认证) | 接口 | 方法 | 路径 | 说明 | |------|------|------|------| | 获取个人信息 | GET | `/replenisher/my-info` | 含设备绑定数 | | 设备列表 | GET | `/replenisher/device/list` | 含库存概览 | | 设备库存 | GET | `/replenisher/device/inventory/{deviceId}` | 详细库存 | | 执行补货 | POST | `/replenisher/stock/replenish` | 增加库存 | --- ## 四、微信绑定流程 ### 4.1 流程概述 ``` ┌─────────────────────────────────────────────────────────────────┐ │ 补货员微信绑定流程 │ │ │ │ ┌──────────┐ ┌──────────────┐ ┌──────────────────┐ │ │ │ 管理员后台 │───>│ 生成一次性绑定码│───>│ 展示二维码/绑定码 │ │ │ │ 创建补货员 │ │ (Redis 24h) │ │ (前端弹窗展示) │ │ │ └──────────┘ └──────────────┘ └────────┬─────────┘ │ │ │ │ │ ▼ │ │ ┌──────────┐ ┌──────────────┐ ┌──────────────────┐ │ │ │ 绑定成功 │<───│ 存入openid + │<───│ 补货员扫码/输入 │ │ │ │ 自动登录 │ │ 删除绑定码 │ │ + 微信授权 │ │ │ └──────────┘ └──────────────┘ └──────────────────┘ │ └─────────────────────────────────────────────────────────────────┘ ``` ### 4.2 详细步骤 **1. 管理员创建补货员** - 运营平台后台 → 补货员管理 → 创建补货员 - 输入姓名、手机号(可选)、工号(可选) - 创建后状态为启用,`wechat_openid` 为空 **2. 管理员生成绑定码** - 在补货员列表操作列点击"绑定码" - 后端生成 24 位 UUID 大写绑定码,存入 Redis(24 小时 TTL) - 前端弹窗展示绑定码和二维码(小程序路径 `pages/replenish/bind?code=xxx`) - 同一补货员如已绑定微信则拒绝生成 **3. 补货员扫码绑定** - 打开管理端小程序(`haha-admin-mp`)或用户端小程序(`haha-mp`) - 扫描二维码或手动输入绑定码 - 点击"绑定微信" → `uni.login()` 获取微信 code - 调用 `POST /replenisher/login/bind` 接口,传入 `bindingCode` + `code` - 后端验证 Redis 绑定码 → `jscode2session` 获取 openid → 检查 openid 唯一性 → 存入补货员记录 → 删除 Redis 绑定码 → 自动登录 **4. 后续登录** - 已绑定微信的补货员可直接使用微信静默登录 `POST /replenisher/login/wechat` - 后端通过 `wechat_openid` 定位补货员 → 检查状态 → Sa-Token 登录 ### 4.3 安全机制 | 机制 | 说明 | |------|------| | **绑定码一次性有效** | 使用后立即从 Redis 删除 | | **绑定码过期** | Redis TTL 24 小时 | | **openid 唯一性检查** | 绑定和登录时都检查 openid 是否已被其他账号占用 | | **禁止重复绑定** | 已绑定微信的补货员不能再生成绑定码 | | **账号状态检查** | 禁用状态的补货员无法登录 | ### 4.4 微信配置 补货员使用**管理端微信小程序**的 AppId/Secret: ```yaml wechat: admin-miniapp: app-id: xxxxxxxxxxxxxx secret: xxxxxxxxxxxxxx ``` 通过 `jscode2session` 接口获取 openid: ``` GET https://api.weixin.qq.com/sns/jscode2session ?appid={appid} &secret={secret} &js_code={code} &grant_type=authorization_code ``` ### 4.5 认证方式 补货员登录使用 **Sa-Token 同一域名**(与运营平台共享 `adminAccessToken`),通过 Token Session 中的 `userType` 字段区分身份: ```java StpUtil.login(replenisher.getId()); StpUtil.getTokenSession().set("userType", "REPLENISHER"); ``` 登录成功后返回 `LoginVO`: ```json { "token": "sa-token-value", "userInfo": { "id": "182734...", "nickname": "张三", "avatar": "https://...", "phone": "138..." } } ``` 补货员操作接口通过 `getCurrentReplenisher()` 方法验证身份: ```java String userType = StpUtil.getTokenSession().getString("userType"); if (!"REPLENISHER".equals(userType)) { throw new BusinessException(403, "无访问权限"); } ``` --- ## 五、补货操作流程 ### 5.1 补货流程 ``` ┌──────────┐ ┌──────────────┐ ┌──────────────────┐ ┌──────────┐ │ 登录小程序 │───>│ 查看绑定设备 │───>│ 查看设备库存详情 │───>│ 执行补货 │ │ 微信静默 │ │ 列表含库存概览 │ │ 含商品库存明细 │ │ 增加库存 │ └──────────┘ └──────────────┘ └──────────────────┘ └──────────┘ ``` ### 5.2 补货 API **请求**: ```json POST /replenisher/stock/replenish { "deviceId": "B142977", "items": [ { "productId": 123456, "productCode": "6921234567890", "productName": "矿泉水550ml", "quantity": 24, "shelfNum": 1, "position": "A1" } ] } ``` **响应**: ```json { "code": 200, "message": "补货完成,成功1项,失败0项", "data": { "total": 1, "success": 1, "details": [ { "productId": 123456, "productName": "矿泉水550ml", "quantity": 24, "afterStock": 120, "success": true } ] } } ``` ### 5.3 安全校验 补货操作前进行权限验证: 1. 验证当前用户是否为补货员(通过 `userType` 身份标识) 2. 验证设备是否绑定到此补货员(从 `t_replenisher_device` 查询) 3. 逐项执行补货,每项独立事务,失败不影响其他项 --- ## 六、前端实现 ### 6.1 管理后台(haha-admin-web) - **页面**:`src/views/replenisher/index.vue` — 补货员管理页面 - **API**:`src/api/replenisher.ts` — 全部 CRUD + 绑定设备 + 生成绑定码 - **功能**: - 补货员分页列表(含设备绑定数) - 创建/编辑/删除补货员 - 启用/禁用 - 绑定/解绑设备 - **生成绑定码弹窗**(展示 24 位绑定码 + 二维码) - **依赖**:`qrcode` npm 包用于生成二维码图片 ### 6.2 管理端小程序(haha-admin-mp) - **登录页**:`src/pages/login/login.vue` - 微信一键登录(`POST /replenisher/login/wechat`) - 未绑定提示 → 展开绑定码输入区域 - 绑定码 + 微信授权 → 绑定登录 - **绑定页**:`src/pages/replenish/bind.vue` - 从 `options.code` / `scene` 获取绑定码 - 绿色清新 UI,绑定流程指引 - 微信授权 → 绑定 API → 自动跳转 - **补货工作台**:`src/pages/replenish/` 目录下 - 设备列表 - 库存查看 - 执行补货 ### 6.3 用户端小程序(haha-mp) - **绑定页**:`src/pages/replenish/bind.vue` - 金色品牌风格 UI,三步骤流程指引 - 支持 `options.code` / `options.bindingCode` / `options.scene` 获取绑定码 - 绑定成功后保存 token 并跳转首页 - 已加入免登录白名单 --- ## 七、Redis 键设计 | Key | 格式 | 说明 | TTL | |-----|------|------|-----| | 补货员绑定码 | `replenisher:bind:code:{bindingCode}` | Value 为补货员 ID | 24 小时 | 定义在 `RedisConstants.java`: ```java public static final String REPLENISHER_BIND_CODE_KEY = "replenisher:bind:code:%s"; public static final String REPLENISHER_BIND_CODE_KEY_PREFIX = "replenisher:bind:code:"; ``` --- ## 八、关键设计决策 ### 8.1 为什么不允许主动注册? **决策**:补货员不允许自主注册,必须由管理员在后台创建后,通过绑定码完成微信绑定。 **原因**: - 补货员属于内部人员,需要经过公司审核和培训 - 设备绑定权限不可随意分配(关系到库存数据和设备安全) - 避免未授权的第三方通过扫描二维码自动注册 ### 8.2 为什么使用 Redis 存储绑定码而非数据库? - 绑定码是一次性凭证,无需持久化存储 - Redis 自动过期(TTL)机制天然适合此类场景 - 避免数据库表膨胀(绑定码数据量少但频繁生成/删除) - 高性能读写,不增加数据库压力 ### 8.3 为什么补货员使用管理端小程序的 AppId? 补货员和管理端共用同一套微信小程序(`haha-admin-mp`),使用管理端小程序的 AppId/Secret 进行微信登录。这意味着: - 补货员和管理员扫码的是同一个微信小程序 - 通过 `userType` 区分身份和权限 - 补货员无法访问管理后台页面 ### 8.4 补货操作的事务处理 补货操作的 `increaseStock` 方法内部通过 `synchronized (deviceId.intern())` 实现设备级锁,防止同一设备并发补货导致库存数据不一致。每条商品补货独立执行,失败不影响其他商品。 --- ## 九、SQL 参考 ### 9.1 补货员分页查询(含设备绑定数) ```xml ``` ### 9.2 通过设备查询补货员 ```sql 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 ``` ### 9.3 批量插入关联(忽略已存在) ```sql INSERT IGNORE INTO t_replenisher_device (id, replenisher_id, device_id, source, create_time) VALUES (?, ?, ?, ?, ?) ```