【从零搭建物联网智能充电桩系统】6、用户服务与钱包:微信登录、JWT 鉴权与乐观锁并发控制

用户侧的业务都集中在这个模块:微信小程序登录如何换取 JWT、钱包余额的冻结与结算如何用乐观锁保证并发安全、订单生命周期如何用事务管理。本文逐一拆解。


一、模块定位

charge-user(端口 8086)是业务面最广的模块(49 个 Java 文件),也是全项目工程化最完整的模块:

  • ✅ 唯一的全局异常处理(GlobalExceptionHandler)
  • ✅ 唯一的统一响应体(ApiResult)
  • ✅ JWT 拦截器 + CORS 配置
  • ✅ MyBatis-Plus 乐观锁 / 逻辑删除 / 字段自动填充
  • ⚠️ 但:7 个管理接口为 mock/硬编码数据(login 固定账号、statistics 随机数、admins/roles/permissions/op-logs/configs 为纯 mock)、管理员口令硬编码、无测试

二、模块结构

复制代码
charge-user/.../user/
├── UserApplication.java
├── config/
│   ├── MybatisPlusConfig.java       乐观锁拦截器
│   ├── MybatisPlusMetaHandler.java  自动填充 createTime/updateTime
│   └── WebMvcConfig.java            JWT 拦截器 + CORS
├── controller/           7 个 Controller(AdminController 最大 355 行)
├── dto/                  ApiResult + 各类请求/响应
├── entity/               10 个实体(对应 10 张表)
├── exception/            GlobalExceptionHandler.java(全项目唯一)
├── interceptor/          JwtInterceptor.java
├── mapper/               10 个空接口(纯 BaseMapper)
├── service/
│   ├── UserService.java + impl/UserServiceImpl.java   ★唯一有接口抽象的
│   └── impl/{WalletService, OrderService, StationService, AdminService}.java
└── utils/
    ├── JwtUtils.java     HS256 签发/解析
    └── WxApiUtils.java   code2Session

三、微信登录 + JWT 全链路(核心要点)

时序

复制代码
① 小程序 uni.login() 取临时凭证 code
② POST /user/login/wx {code}
③ 后端 WxApiUtils 调微信接口 code2Session:
   https://api.weixin.qq.com/sns/jscode2session
   → 返回 openid(用户唯一标识)+ session_key
④ 按 openid 查/建 cs_user
⑤ JwtUtils 签发 token(HS256,payload 含 userId + 过期时间)
⑥ 返回 {token, userInfo}
⑦ 小程序 uni.setStorageSync('cs_token')

JWT 关键点

java 复制代码
// jjwt 0.11.5,HS256 对称加密
String token = Jwts.builder()
    .setSubject(String.valueOf(userId))
    .setExpiration(new Date(System.currentTimeMillis() + expire))
    .signWith(SignatureAlgorithm.HS256, secretKey)
    .compact();
问题 回答
为什么 HS256? 对称加密,单服务一个密钥签发+校验,无需 RSA 公钥分发
token 存哪? 小程序本地 storage(无 httpOnly 保护,XSS 可窃取)
服务端如何校验? JwtInterceptor 拦截非放行路径 → 解析 Bearer token → userId 写入 request attribute

⚠️ 已知缺陷

  1. JWT secret 明文硬编码在 application.yml:拿到源码可伪造任意用户 token。应环境变量注入 + 轮换。
  2. 登出是纯前端:只清本地 storage,token 在过期前仍有效。真登出需 Redis 黑名单。
  3. 无 refresh token :过期后只能重新登录。后端有 /user/login/refresh 但前端从未调用(未被使用的接口)。
  4. 未登录跳转用 navigateTo 而非 reLaunch:用户可点返回键退回受保护页面。

四、钱包乐观锁并发控制(本项目质量最高的业务代码)

场景

充电下单需要冻结余额,充电结束结算解冻------同一用户的资金操作可能并发(多设备同时充电、充值+消费同时发生)。

方案:MyBatis-Plus 乐观锁

java 复制代码
// CsWallet.java
@Version
private Integer version;
java 复制代码
// MybatisPlusConfig.java
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
    MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
    interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
    return interceptor;
}

实现原理(核心要点)

sql 复制代码
-- 乐观锁拦截器自动改写 UPDATE:
UPDATE cs_wallet SET balance = ?, version = version + 1
WHERE id = ? AND version = #{旧version}
  • 影响行数 = 1 → 更新成功
  • 影响行数 = 0 → 版本冲突(他人已改)→ CAS 重试

乐观锁 vs 悲观锁(核心要点)

维度 乐观锁(本项目) 悲观锁(SELECT FOR UPDATE)
适用 低冲突(同一用户同时充电概率低) 高冲突(秒杀、热点账户)
等待 不等待,失败重试 等待锁释放
死锁 无死锁 可能死锁
吞吐
代价 CAS 重试浪费 CPU 持锁阻塞

结论话术:钱包场景读写冲突低,乐观锁免锁等待、无死锁、吞吐高;若未来做秒杀充值(高冲突),应评估悲观锁或 Redis 原子扣减。


五、订单事务管理

OrderService(150 行)

方法 事务 流程
createOrder @Transactional 校验余额 → 冻结(乐观锁)→ 创建订单 → 写流水
stopCharge @Transactional 结算 → 解冻 → 更新订单状态 → 写流水

事务边界注意事项(深入思考)

  1. Redis 操作不要放在事务内:Redis 不参与 JDBC 事务回滚,事务失败 Redis 已改 → 数据不一致
  2. 远程调用(IoTDB/推送)放事务提交后:远程调用可能超时,事务内调用会拉长锁持有时间
  3. @Transactional 失效场景:同类内部调用(this.xxx())、非 public 方法、异常被 catch 吞掉、自调用不走代理

⚠️ 已知缺陷:崩溃后冻结资金无法自动解冻

充电中途系统崩溃 → 订单状态停留在"充电中" → 冻结余额永远锁死。生产方案:

  • 定时任务扫描超时未完成订单,自动结算/解冻
  • 引入 Seata/Saga 分布式事务做补偿

六、数据库设计(10 张表)

实体 关键字段/设计
cs_user CsUser openid、手机号
cs_wallet CsWallet balance、frozen_amount、version(乐观锁)
cs_wallet_log CsWalletLog 流水:冻结/解冻/充值
cs_order CsOrder 充电订单
cs_recharge_order CsRechargeOrder 充值订单
cs_charge_record CsChargeRecord 充电记录(与协议 chargeRecordId 对应)
cs_station CsStation 充电站
cs_station_price CsStationPrice 分时电价
cs_connector CsConnector 充电枪
cs_alarm CsAlarm 告警

工程化配置:逻辑删除(global-config.logic-delete-field)、自动填充(createTime/updateTime)、雪花 ID(ASSIGN_ID)。

📌 数据库初始化说明

全项目未提供独立 .sql 建表脚本(无 Flyway/Liquibase),表结构由 Entity 注解定义。如需在新环境初始化数据库,可通过 JPA 的 ddl-auto=update 自动建表,或从 Entity 反向生成 DDL。


七、AdminController 的 mock 缺陷

355 行的 AdminController 中,以下接口返回写死数据(代码注释"供前端联调,后续可接真实表"):

接口 状态
/admin/login ⚠️ 固定账号 admin/Admin@123 硬编码,不查库
/admin/statistics ⚠️ Math.random() 生成趋势数据
/admin/admins ⚠️ mock
/admin/roles ⚠️ mock
/admin/permissions ⚠️ mock
/admin/op-logs ⚠️ mock
/admin/configs ⚠️ mock
/admin/overview 等 ✅ 真实查库

八、常见问题与解答

  1. 微信 code2Session 的 code 能复用吗?(不能,一次性;且 5 分钟有效)
  2. JWT 和 Session 的区别?(状态 vs 无状态、分布式友好性、过期后失效问题)
  3. 乐观锁的 version 字段初始值?更新策略?(默认 0,每次成功更新 +1)
  4. 冻结金额和余额是什么关系?(balance 可用 + frozen_amount 冻结,下单扣冻结额而非直接扣 balance)
  5. 为什么 charge-user 是唯一有全局异常处理的模块?(其他模块没写------工程化不统一)

小结

用户服务是工程化最完整的一层:JWT 鉴权、乐观锁钱包、事务订单,加上唯一一个全局异常处理。同时它也是隐患最集中处------mock 数据、明文密钥、无建表脚本。下一篇 【从零搭建物联网智能充电桩系统】7、IoTDB 时序库与双端前端讲解数据存储选型与两个前端工程。

相关推荐
GoFly开发者2 小时前
GoFly 社区 GMQT|国产自研 MQTT‑Broker,打造私有化物联网消息底座
物联网·mqtt·golang·mqtt broker·mqtt私有化部署
程序猿老杨2 小时前
MQTT协议深度解析:从ESP32设备端到云端Broker的工程化实践
后端·物联网·芯片
sunywz3 小时前
【从零搭建物联网智能充电桩系统】3、MQTT 客户端:设备接入云端的第一道关口
物联网
sunywz3 小时前
【从零搭建物联网智能充电桩系统】4、Netty WebSocket 服务:小程序实时推送背后的长连接管理
物联网·websocket·小程序
sunywz3 小时前
【从零搭建物联网智能充电桩系统】选型篇:Netty、MQTT、RabbitMQ、IoTDB 为什么这么选?
物联网·rabbitmq·iotdb
桐盛科技3 小时前
智慧公厕厕位监测技术选型实战:激光测距、毫米波雷达、红外测距的优缺点对比与场景适配
物联网·智慧城市
sunywz5 小时前
【从零搭建物联网智能充电桩系统】系列导读:从硬件到小程序的完整链路
物联网·小程序
microrain12 小时前
从数据烟囱到融合中心:SagooIoT统一数据处理平台的设计实践
物联网·golang·开源·sagooiot
W.W.H.14 小时前
将驱动编译成模块并安装到rootfs
linux·物联网·wifi·rootfs·驱动