本文档描述系统上货员模块的完整架构,涵盖独立补货员体系、微信绑定流程、补货操作流程及相关实现。 帮助开发人员理解"为什么这样设计"以及各环节的特殊处理方式。
上货员(系统内称"补货员",Replenisher)指负责为智能售卖柜补货的工作人员。他们通过手机小程序查看库存、执行补货操作。
系统使用独立补货员体系,核心表:
| 体系 | 数据表 | 说明 |
|---|---|---|
| 独立补货员 | t_replenisher + t_replenisher_device |
独立账号体系,设备级绑定粒度,微信扫码绑定 |
历史背景:早期存在基于
t_shop_replenisher+t_admin的 V1 体系(补货员需是系统管理员),已于 2026年4月 完成迁移清理,V1 表t_shop_replenisher及所有相关代码已彻底移除。
独立补货员体系相比旧的 V1(基于 t_admin 管理员表)解决了以下问题:
V2 独立补货员体系的核心优势:
t_replenisher 表独立管理,不与管理员混合t_replenisher_device 表实现设备级粒度绑定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 | 创建时间 |
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 | 创建时间 |
docs/database/migrate_replenisher.sql 提供了从旧表 t_stocker 到 t_replenisher 的数据迁移脚本。
V1 表
t_shop_replenisher及所有相关后端/前端代码已于 2026年4月 彻底清理移除。
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 # 微信绑定页
┌─────────────┐ ┌──────────────────────┐ ┌──────────┐
│ t_replenisher│────<│ t_replenisher_device │────>│ t_device │
│ (补货员) │ │ (补货员-设备关联) │ │ (设备) │
└─────────────┘ └──────────────────────┘ └──────────┘
│
│ (wechat_openid 绑定)
│
┌────┴─────┐
│ 微信 OpenID│
└──────────┘
| 接口 | 方法 | 路径 | 权限 | 说明 |
|---|---|---|---|---|
| 分页列表 | 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 |
生成微信绑定码 |
| 接口 | 方法 | 路径 | 权限 | 说明 |
|---|---|---|---|---|
| 获取门店补货员 | GET | /shops/{id}/replenishers |
shop:read |
通过设备绑定反向推导门店关联的补货员 |
| 绑定到门店 | POST | /shops/{id}/replenishers |
shop:update |
将补货员绑定到门店下的所有设备 |
| 接口 | 方法 | 路径 | 说明 |
|---|---|---|---|
| 微信静默登录 | POST | /replenisher/login/wechat |
已绑定微信的补货员直接登录 |
| 扫码绑定微信 | POST | /replenisher/login/bind |
绑定码 + 微信凭证进行绑定 |
| 接口 | 方法 | 路径 | 说明 |
|---|---|---|---|
| 获取个人信息 | GET | /replenisher/my-info |
含设备绑定数 |
| 设备列表 | GET | /replenisher/device/list |
含库存概览 |
| 设备库存 | GET | /replenisher/device/inventory/{deviceId} |
详细库存 |
| 执行补货 | POST | /replenisher/stock/replenish |
增加库存 |
┌─────────────────────────────────────────────────────────────────┐
│ 补货员微信绑定流程 │
│ │
│ ┌──────────┐ ┌──────────────┐ ┌──────────────────┐ │
│ │ 管理员后台 │───>│ 生成一次性绑定码│───>│ 展示二维码/绑定码 │ │
│ │ 创建补货员 │ │ (Redis 24h) │ │ (前端弹窗展示) │ │
│ └──────────┘ └──────────────┘ └────────┬─────────┘ │
│ │ │
│ ▼ │
│ ┌──────────┐ ┌──────────────┐ ┌──────────────────┐ │
│ │ 绑定成功 │<───│ 存入openid + │<───│ 补货员扫码/输入 │ │
│ │ 自动登录 │ │ 删除绑定码 │ │ + 微信授权 │ │
│ └──────────┘ └──────────────┘ └──────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
1. 管理员创建补货员
wechat_openid 为空2. 管理员生成绑定码
pages/replenish/bind?code=xxx)3. 补货员扫码绑定
haha-admin-mp)或用户端小程序(haha-mp)uni.login() 获取微信 codePOST /replenisher/login/bind 接口,传入 bindingCode + codejscode2session 获取 openid → 检查 openid 唯一性 → 存入补货员记录 → 删除 Redis 绑定码 → 自动登录4. 后续登录
POST /replenisher/login/wechatwechat_openid 定位补货员 → 检查状态 → Sa-Token 登录| 机制 | 说明 |
|---|---|
| 绑定码一次性有效 | 使用后立即从 Redis 删除 |
| 绑定码过期 | Redis TTL 24 小时 |
| openid 唯一性检查 | 绑定和登录时都检查 openid 是否已被其他账号占用 |
| 禁止重复绑定 | 已绑定微信的补货员不能再生成绑定码 |
| 账号状态检查 | 禁用状态的补货员无法登录 |
补货员使用管理端微信小程序的 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
补货员登录使用 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, "无访问权限");
}
┌──────────┐ ┌──────────────┐ ┌──────────────────┐ ┌──────────┐
│ 登录小程序 │───>│ 查看绑定设备 │───>│ 查看设备库存详情 │───>│ 执行补货 │
│ 微信静默 │ │ 列表含库存概览 │ │ 含商品库存明细 │ │ 增加库存 │
└──────────┘ └──────────────┘ └──────────────────┘ └──────────┘
请求:
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
}
]
}
}
补货操作前进行权限验证:
userType 身份标识)t_replenisher_device 查询)src/views/replenisher/index.vue — 补货员管理页面src/api/replenisher.ts — 全部 CRUD + 绑定设备 + 生成绑定码qrcode npm 包用于生成二维码图片src/pages/login/login.vue
POST /replenisher/login/wechat)src/pages/replenish/bind.vue
options.code / scene 获取绑定码src/pages/replenish/ 目录下
src/pages/replenish/bind.vue
options.code / options.bindingCode / options.scene 获取绑定码| 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:";
决策:补货员不允许自主注册,必须由管理员在后台创建后,通过绑定码完成微信绑定。
原因:
补货员和管理端共用同一套微信小程序(haha-admin-mp),使用管理端小程序的 AppId/Secret 进行微信登录。这意味着:
userType 区分身份和权限补货操作的 increaseStock 方法内部通过 synchronized (deviceId.intern()) 实现设备级锁,防止同一设备并发补货导致库存数据不一致。每条商品补货独立执行,失败不影响其他商品。
<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>
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
INSERT IGNORE INTO t_replenisher_device (id, replenisher_id, device_id, source, create_time)
VALUES (?, ?, ?, ?, ?)