上货员模块.md 20 KB

上货员(补货员)模块说明

本文档描述系统上货员模块的完整架构,涵盖独立补货员体系、微信绑定流程、补货操作流程及相关实现。 帮助开发人员理解"为什么这样设计"以及各环节的特殊处理方式。


一、模块概述

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_stockert_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:

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 字段区分身份:

StpUtil.login(replenisher.getId());
StpUtil.getTokenSession().set("userType", "REPLENISHER");

登录成功后返回 LoginVO

{
  "token": "sa-token-value",
  "userInfo": {
    "id": "182734...",
    "nickname": "张三",
    "avatar": "https://...",
    "phone": "138..."
  }
}

补货员操作接口通过 getCurrentReplenisher() 方法验证身份:

String userType = StpUtil.getTokenSession().getString("userType");
if (!"REPLENISHER".equals(userType)) {
    throw new BusinessException(403, "无访问权限");
}

五、补货操作流程

5.1 补货流程

┌──────────┐    ┌──────────────┐    ┌──────────────────┐    ┌──────────┐
│ 登录小程序 │───>│ 查看绑定设备   │───>│ 查看设备库存详情   │───>│ 执行补货  │
│ 微信静默   │    │ 列表含库存概览 │    │ 含商品库存明细    │    │ 增加库存  │
└──────────┘    └──────────────┘    └──────────────────┘    └──────────┘

5.2 补货 API

请求

POST /replenisher/stock/replenish
{
  "deviceId": "B142977",
  "items": [
    {
      "productId": 123456,
      "productCode": "6921234567890",
      "productName": "矿泉水550ml",
      "quantity": 24,
      "shelfNum": 1,
      "position": "A1"
    }
  ]
}

响应

{
  "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 — 补货员管理页面
  • APIsrc/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

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 补货员分页查询(含设备绑定数)

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

9.2 通过设备查询补货员

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 批量插入关联(忽略已存在)

INSERT IGNORE INTO t_replenisher_device (id, replenisher_id, device_id, source, create_time)
VALUES (?, ?, ?, ?, ?)