【从零搭建物联网智能充电桩系统】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 时序库与双端前端讲解数据存储选型与两个前端工程。

相关推荐
qq_316411035 小时前
物联网大健康管理平台的功能模块与技术架构详解
物联网
SL-staff8 小时前
技术实践:解决IoT网关单点故障导致子设备批量‘失联’问题
物联网·zigbee·边缘缓存·jvs·iot架构·物模型设计·设备生命周期
TDengine (老段)9 小时前
TDengine 应用案例 — 能源与电力监控
大数据·数据库·物联网·能源·时序数据库·tdengine·涛思数据
虎王物联10 小时前
Docker WASM 边缘计算:12ms 冷启动如何重塑 IoT 网关部署
物联网·docker·容器·边缘计算·wasm
2601_9567436810 小时前
上海物联网应用开发技术选型指南:从HTTP/MQTT/Modbus协议适配、平台化架构设计到源代码交付与私有化部署,解析工程落地的关键考量点
物联网·网络协议·http·开发经验·上海
黎阳之光11 小时前
软硬协同国产化 | 黎阳之光矩阵式视频融合管理系统完成摩尔线程国产GPU兼容适配
大数据·人工智能·物联网·安全·数字孪生
智圣新创0111 小时前
存量数字化校园升级破局:从基础数据集成到校务服务全域提效的通用落地路径
大数据·人工智能·物联网
HZZD_HZZD14 小时前
远传水表系统技术选型:大口径物联网水表与 OCR 摄像水表对比
物联网·ocr
虎王物联21 小时前
工业Modbus TCP协议深度实战:STM32 HAL库实现从机通信与数据上云
stm32·物联网·网络协议·tcp/ip·嵌入式
虎王物联1 天前
PHP实现MQTT消息处理:物联网设备数据接收与指令下发
android·物联网·php