面向新同学的入门与进阶学习文档,涵盖
angkela_backend与angkela_netty两个核心模块。本文档基于源码分析编写,路径均指仓库内实际文件。
仓库根目录包含四个部分:
| 目录 | 作用 |
|---|---|
angkela_backend | 管理后台 + 小程序接口服务(Spring Boot 2.1,端口 8066/28066) |
angkela_netty | 设备接入服务(Spring Boot 1.5 + Netty 4.1,负责 WiFi 净水器/壁挂炉等设备长连接) |
angkela_static | 前端静态资源 / 管理后台页面 |
文档 | 产品需求、接口说明、通讯协议等资料 |
整体架构是一个"物联网 + 管理平台"架构:
设备(WiFi模组) <----TCP长连接(私有协议)----> angkela_netty (Netty网关)
│ (解析上报帧、下发控制帧)
│ Redis 共享状态 / 延时任务
│ MySQL 持久化
│
小程序(微信) <------HTTPS/JSON------> angkela_backend (REST API + JWT鉴权)
│ 读取设备状态(Redis/Mysql)
│ 下发控制指令(调 netty 接口)
管理后台(Vue) <------HTTPS/JSON------> angkela_backend
两个 Spring Boot 服务共享同一个 Redis 与 MySQL:
angkela_netty 负责设备数据上报、实时状态、故障告警、远程升级、定时任务。angkela_backend 负责用户(Customer)、设备绑定、分享、统计报表、小程序登录、验证码等业务。| 技术 | 版本 | 用途 |
|---|---|---|
| Spring Boot | 2.1.1.RELEASE | 微服务基础框架 |
| Spring MVC | - | REST 接口 |
| Spring Security + JWT | jjwt 0.9.0 | 认证授权(无状态 Token) |
| MyBatis | 1.3.2 starter | ORM 持久层 |
| PageHelper | 1.2.5 | 分页插件 |
| Druid | 1.1.14 | 数据库连接池 + SQL 监控 |
| Redis (Spring Data Redis + Lettuce) | - | 缓存、Token 存储、验证码、小程序 session |
| FastJSON | 1.2.47 | JSON 序列化 |
| Swagger2 | 2.9.2 | 接口文档 |
| Apache POI | 3.17 | Excel 导入导出 |
| Velocity | 1.7 | 代码生成模板 |
| OSHI + JNA | 3.9.1 | 服务器监控(CPU/内存) |
| 阿里云 SDK (OSS/ECS) | - | 文件存储、云服务器操作 |
| JSCH / commons-net | - | SFTP / FTP 文件传输 |
| BouncyCastle | 1.46 | 加密算法库 |
| UserAgentUtils | 1.19 | 解析浏览器/操作系统 |
| 技术 | 版本 | 用途 |
|---|---|---|
| Netty | 4.1.25.Final | 设备 TCP 长连接服务端 + 客户端 |
| Spring Boot | 1.5.6.RELEASE | 基础框架(老版本,与 Netty 独立使用) |
| MyBatis | 1.3.2 | 持久层 |
| Redis + commons-pool2 | - | 设备状态缓存、分布式延时任务 |
| Protostuff + Objenesis | 1.0.10 | POJO 二进制序列化(protobuf 风格,省带宽) |
| Apache MINA | 2.0.16 | 遗留的会话工具依赖 |
| Shiro + shiro-redis | 1.4.0 | 认证框架(部分接口使用) |
| fluent-hc | 4.5.6 | HTTP 客户端(调用后台/第三方) |
| spring-boot-starter-mail | - | 邮件告警 |
| 阿里云 SDK | - | SMS 短信、ECS |
spring-boot-starter / web / aop / test / devtools
spring-boot-maven-plugin 中 fork=true 保证 devtools 生效。spring-boot-starter-security
SecurityConfig + TokenService)。mybatis-spring-boot-starter + pagehelper-spring-boot-starter
PageHelper 通过 MyBatis 插件机制自动改写 SQL 实现分页。mapperLocations: classpath*:mybatis/**/*Mapper.xml。PageHelper.startPage() + TableDataInfo。druid-spring-boot-starter
DynamicDataSource 支持主从库动态切换(@DataSource 注解 + AOP)。/druid/*。mysql-connector-java
serverTimezone=GMT%2B8(东八区)与 allowMultiQueries=true(允许一条语句多查询)。spring-boot-starter-data-redis + commons-pool2
jjwt(io.jsonwebtoken)
TokenService。bcprov-jdk15(BouncyCastle)
UserAgentUtils(bitwalker)
| 库 | 说明 | 典型用法 |
|---|---|---|
| fastjson | 高性能 JSON | JSONObject、JSON.toJSONString |
| commons-lang3 / commons-io | 字符串/IO 工具 | StringUtils、文件读写 |
| commons-fileupload | 文件上传 | 头像/图片上传 |
| jsch | SFTP | 上传固件到设备升级服务器(SFTPUtil) |
| commons-net | FTP | 传统 FTP 上传 |
| aliyun-java-sdk-core / ecs | 阿里云 API | 操作 ECS、发短信(AliSMS) |
| aliyun-sdk-oss | OSS 对象存储 | 存储固件/图片 |
| poi-ooxml | Excel | @Excel 注解导出 |
| velocity | 模板引擎 | 代码生成器生成 Controller/Service/Mapper |
| oshi-core + jna + jna-platform | 系统信息 | 服务器监控 Server/Cpu/Jvm |
| springfox-swagger2 + ui | API 文档 | /swagger-ui.html |
| spring-context-support | 缓存抽象等 | 辅助 Spring 集成 |
angkela_backend 的包结构(common/framework/project)与经典的 RuoYi 框架几乎一致,这是在 RuoYi 基础上二次开发的。选择它的理由:
controller -> service -> mapper 清晰,新人容易上手。传统 JWT 把用户信息直接加密进 Token,一旦需要"踢人/封号"就无法撤销。
本项目设计(TokenService.java):
LoginUser 写入 Redis(key:login_tokens:<uuid>),TTL 30 分钟。JwtAuthenticationTokenFilter 解析 Token → 查 Redis → 还原用户。refreshToken 续期(滑动过期)。核心收益:登出=删 Redis key;封禁=删 key;多端登录管理简单。
IdleStateHandler(180,180,180))。净水器"预约升级""滤芯到期提醒""严重故障告警"等场景,需要到点执行。
用 Redis 的 Key 过期事件(Keyspace notifications)实现:
RedisKeyExpirationListener(继承 KeyExpirationEventMessageListener)订阅事件,在 doHandleMessage 里根据 key 名称分发逻辑。application-dev/dev2/dev3/test/prod/prodA/prodf.yml 对应不同环境(本机、测试服务器、阿里云正式机等),通过 spring.profiles.active 切换。好处是数据库、Redis 地址等环境差异集中管理,发布时只改一个参数。
com.dafeng
├── AdfApplication.java # 启动类
├── AdfServletInitializer.java # 打 war 包用(可选)
├── common # 通用层:常量、工具类、异常、枚举、XSS
│ ├── constant # Constants/HttpStatus/UserConstants...
│ ├── core # UUID、文本处理(Convert/CharsetKit)
│ ├── utils # 日期/字符串/MD5/安全/IP/SFTP/Excel...
│ │ ├── file / html / http / ip / poi / reflect / security / sign / sql / text
│ └── exception # 业务异常体系(BaseException 等)
├── framework # 框架层:可复用的基础设施
│ ├── aspectj # AOP 切面:操作日志、数据权限、数据源切换
│ │ └── lang # 自定义注解 @Log @DataScope @DataSource @Excel
│ ├── config # 各种配置类(Redis/MyBatis/Druid/Security/Swagger...)
│ ├── datasource # 动态数据源(主从切换)
│ ├── interceptor # 拦截器(App 鉴权、防重复提交)
│ ├── manager # 异步任务管理器(日志落库)
│ ├── redis # RedisCache 工具、Key 过期监听
│ ├── security # JWT 认证核心:TokenService/LoginUser/过滤器
│ └── web # BaseController、AjaxResult、分页封装、全局异常
└── project # 业务层:具体功能模块
├── system # 后台管理:用户/角色/菜单/部门
├── monitor # 监控:服务器、在线用户、操作日志
├── device # 设备档案:经销商、品牌、设备编码
├── wechat # 小程序:登录、绑定设备、分享、控制、预约
├── share # 分享/评论/点赞
├── statistical # 统计报表
└── tool # 代码生成、数据字典
请求处理链路(理解这个就懂 90%):
请求 → SecurityConfig过滤链 → JwtAuthenticationTokenFilter(解析Token)
→ Controller(@RestController)
→ Service(接口+impl,@Transactional)
→ Mapper(接口+XML)
→ MySQL
拦截器:AppInterceptor(部分接口)/RepeatSubmitInterceptor(防重复)
AOP:LogAspect(操作日志) / DataSourceAspect(主从) / DataScopeAspect(数据权限)
异常:GlobalExceptionHandler 统一返回 AjaxResult
返回:TableDataInfo(分页) 或 AjaxResult(通用)
com.dafeng.wifi
├── SpringbootStartApplication.java # 启动类
├── netty/ # Netty 核心
│ ├── NettyServer.java # ServerBootstrap,boss+worker 线程组
│ ├── MyChannelInitializer.java # 编解码器 + 心跳 + 业务 Handler 组装
│ ├── ServerDecodeHandler.java # 粘包/拆包 + 字节->报文
│ ├── ServerEncodeHandler.java # 报文->字节
│ └── MyServerHandler.java # 核心业务:解析帧、下发指令
├── client/ # Netty 客户端(主动连服务器/对其他服务)
├── controllers/ # HTTP 接口(给 backend/小程序调)
│ ├── DeviceController.java # 设备控制入口
│ └── LoadBalanceController.java # 负载均衡
├── services/ # 业务服务(设备、故障告警、升级、短信...)
├── mappers/ # MyBatis(DataHour/DataMonth/Device...)
├── pojos/ models/ # 实体
├── constants/ # 协议常量:FunCode/ConCode/InConCode/FaultConstant
├── utils/ # 编解码帧工具、缓存、负载均衡、短信/邮件/微信
└── config/ # Redis、启动初始化、跨域
Netty 报文处理流程:
设备TCP数据 → ServerDecodeHandler(解码成帧) → MyServerHandler(按功能码分发)
├─ 心跳帧 → 回复、更新在线状态
├─ 状态上报帧 → 存 Redis(mac+devSta) + MySQL、判断故障
├─ 回复帧 → 唤醒等待结果的任务(如远程升级、串口读取)
└─ 请求下发 → EncodeFrame → ServerEncodeHandler → 设备
angkela_backend,了解 Maven 依赖与启动方式。pom.xml,逐个认识依赖(对照本文第 3 章)。application-dev.yml,理解配置(端口、Redis、数据库、token)。SysLoginService → TokenService → JwtAuthenticationTokenFilter → SecurityConfig,串起来画一张时序图。AjaxResult、BaseController、BaseEntity、GlobalExceptionHandler、PageHelper 分页。mybatis/ 目录看一个 XML(如 device),掌握 resultMap、动态 SQL(<if>/<where>)。LogAspect(操作日志)、DataSourceAspect(主从切换)、DataScopeAspect(数据权限)。tool 模块的代码生成器生成一个简单的 CRUD 模块,看生成的结构。按"简单→复杂"顺序研究:
device(设备档案 CRUD)→ 最容易,掌握标准分层写法。wechat(小程序)→ 理解小程序登录换 token、设备绑定、控制指令下发(调 netty HTTP 接口)。statistical(统计)→ 大量 Redis 读取 + 报表。share / monitor / system。NettyServer(启动)→ MyChannelInitializer(pipeline 组装)→ ServerDecodeHandler(解码)。MyServerHandler,对照协议文档(文档/云平台5.昂科拉/通讯协议)理解功能码。本项目的 Redis 是两个服务之间的"共享内存",理解它 = 理解整个物联网架构。
Redis 是内存键值数据库,支持 String/Hash/List/Set/ZSet 五种数据结构,支持设置过期时间(TTL),单线程执行命令保证原子性。本项目用它解决:
autoUpNum、autoUpPage)。angkela_backend(RedisConfig.java):
@Configuration
@EnableCaching
"k">public "k">class RedisConfig "k">extends CachingConfigurerSupport {
@Bean
"k">public RedisTemplate<"k">Object, "k">Object> redisTemplate(RedisConnectionFactory connectionFactory) {
RedisTemplate<"k">Object, "k">Object> template = "k">new RedisTemplate<>();
template.setConnectionFactory(connectionFactory);
FastJson2JsonRedisSerializer serializer = "k">new FastJson2JsonRedisSerializer("k">Object."k">class);
// 用 Jackson 开启多态序列化,确保反序列化时还原具体类
ObjectMapper mapper = "k">new ObjectMapper();
mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
mapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
serializer.setObjectMapper(mapper);
template.setValueSerializer(serializer); // value 用 JSON
template.setKeySerializer("k">new StringRedisSerializer()); // key 用字符串
template.afterPropertiesSet();
"k">return template;
}
}
关键点:key 用 String 序列化,value 用 JSON。这样 key 可读、value 能还原对象。
angkela_netty(config/RedisCache.java)则统一把 key 与 hash key 都设为 StringRedisSerializer,因为 netty 侧的 key 几乎都是拼接字符串(如 mac+devSta)。
封装了 RedisTemplate 的所有常用操作,业务代码只用它,不要直接 new RedisTemplate:
| 方法 | 底层命令 | 用途 |
|---|---|---|
setCacheObject(key, value) | SET | 缓存对象/字符串 |
setCacheObject(key, value, timeout, unit) | SETEX | 带过期时间缓存 |
getCacheObject(key) | GET | 取缓存 |
deleteObject(key) | DEL | 删缓存 |
setCacheList/getCacheList | LPUSH/LRANGE | List 操作 |
setCacheSet/getCacheSet | SADD/SMEMBERS | Set 操作 |
setCacheMap/getCacheMap | HMSET/HGETALL | Hash 操作 |
keys(pattern) | KEYS | 按通配符查 key(* 慎用,阻塞大) |
refresh(key, seconds) | - | 重设 TTL 续期 |
这是最核心的跨服务共享场景。
MyServerHandler 解析后:// MyServerHandler.java:"n">698
redisCache.setCacheObject(mac + "devSta", JSONObject.toJSONString(deviceState));
redisCache.setCacheObject(mac + "devStaHex", HexUtils.bytesToHex(bytes));
key 是 <MAC>devSta,value 是设备状态的 JSON 字符串。
// 如 wechat 模块
"k">Object obj = redisCache.getCacheObject(mac + "devSta");
JSONObject state = JSON.parseObject(obj.toString());
或 netty 自己的 DeviceController 也直接读这个 key(getCacheObject(strMac+"devSta"))。
好处:设备每秒上报,不必每次查 MySQL;状态是"最新的",Redis 充当两级存储中的热数据层。
TokenService.createToken:
"k">String token = IdUtils.fastUUID(); // 生成 uuid
loginUser.setToken(token);
refreshToken(loginUser); // 写 Redis,TTL=expireTime
// JWT 中只放 uuid
claims.put(Constants.LOGIN_USER_KEY, token);
TokenService.refreshToken:
loginUser.setExpireTime(loginTime + expireTime * MILLIS_MINUTE);
"k">String userKey = getTokenKey(loginUser.getToken()); // login_tokens:<uuid>
redisCache.setCacheObject(userKey, loginUser, expireTime, TimeUnit.MINUTES);
登录后每次请求由 JwtAuthenticationTokenFilter 调 getLoginUser:
Claims claims = parseToken(token); // 解出 uuid
"k">String userKey = getTokenKey(uuid);
LoginUser user = redisCache.getCacheObject(userKey); // 从 Redis 还原用户
登出/踢人 = redisCache.deleteObject(userKey),立即生效。
给学习者的练习:用
redis-cli看登录后keys login_tokens:*,TTL观察有效期;调接口后再看是否刷新(滑动续期)。
Redis 的 Keyspace Notifications(键空间通知)能发布"key 过期"事件。项目借此实现延时任务。
RedisListenerConfig):@Bean
RedisMessageListenerContainer container(RedisConnectionFactory connectionFactory) {
RedisMessageListenerContainer container = "k">new RedisMessageListenerContainer();
container.setConnectionFactory(connectionFactory);
"k">return container;
}
KeyExpirationEventMessageListener,重写 doHandleMessage:@Component
"k">public "k">class RedisKeyExpirationListener "k">extends KeyExpirationEventMessageListener {
@Override
"k">protected "k">void doHandleMessage(Message message) {
"k">String expKey = message.toString(); // 收到过期的 key 名
// 按 key 名解析 MAC 与业务标识,分发处理
}
}
// AppletsDeviceController.java:"n">450 写两个 key
redisCache.setCacheObject(mac + ":reservationTime", date + " " + hour); // 记录预约时间
redisCache.setCacheObject(mac + ":reservationUp", date + " " + hour,
("k">int)(reservationUpDate.getTime() - timeStamp), TimeUnit.MILLISECONDS); // TTL=到点剩余毫秒
key <mac>:reservationUp 到点过期 → backend 的 RedisKeyExpirationListener 收到 → 调用 netty 接口触发升级(HttpUtil.request(upgradeFontUrl...))。失败则顺延 24 小时。
openWebSetTime 过期:页面监控超时,发送 WRITE_A 帧让设备退出网页控制模式。adjustCity 过期:重新解析设备所在城市并写库。seriousFault 过期:严重故障 → 微信/短信告警、写故障通知表。weixin_access_token 过期:自动刷新微信 access_token(提前 60 秒续期)。注意:Redis 默认不开启过期通知,需要设置
notify-keyspace-events Ex(项目生产环境已配置)。命令:```
config set notify-keyspace-events Ex
```
事件只保证"最终送达",极端情况可能丢失,所以项目里还会有补偿逻辑(如升级失败顺延)。
DeviceServiceImpl 中用 setIfAbsent 思想的 key(mac:faultId、mac:allQuery)判断某条指令是否已处理,防止重复下发。MyServerHandler 中用 getCacheObject(mac+"openWeb")==null 判断设备是否处于"网页控制模式"。redisCache.setCacheObject("autoUpNum", deviceList.size()); // 总设备数
redisCache.setCacheObject("autoUpPage", "n">1); // 当前页
// 分页升级,逐批给设备下发 :autoUp 并设置 "n">7200 秒 TTL
<MAC>业务标识 或 <MAC>:业务标识,如 AB12CD34devSta、AB12CD34:reservationUp。前缀可以按业务分组,但注意不要过长。redisCache.keys("*") 在测试代码里出现过,线上严禁用 KEYS *(会阻塞 Redis),要用 SCAN。mac+devSta)时,value 用纯 JSON 字符串,这样跨项目反序列化互不依赖(backend 用 FastJSON、netty 也用 FastJSON,格式一致即可)。| 主题 | 建议 |
|---|---|
| 基础命令 | Redis 官方文档 https://redis.io/docs/latest/commands/ |
| 数据结构 | String/Hash/List/Set/ZSet 各写 5 个命令练习 |
| Spring Data Redis | 看 RedisTemplate API 与本文第 7.3 节对照 |
| 过期通知 | 搜 "Redis keyspace notification KeyExpirationEventMessageListener" |
| 集群/哨兵 | 了解即可,本项目单机 Redis |
| 现象 | 可能原因 |
|---|---|
| 启动报 Redis 连接失败 | 检查 application-<env>.yml 中 host/port/password;Redis 未启动 |
| 登录提示验证码错误 | CaptchaController 生成的验证码存 Redis,检查过期时间 |
| Token 过期/未登录 | token.expireTime 配置(默认 30 分钟);Redis 被 flush 后所有会话失效 |
| 设备离线 | IdleStateHandler(180...) 3 分钟无数据即离线;检查设备网络与心跳 |
| 设备状态读不到 | netty 未运行;key 名不一致(mac+devSta vs mac+":devSta");序列化不一致 |
| 过期事件不触发 | Redis 未开启 notify-keyspace-events Ex;多个实例重复消费(需幂等) |
| 切换数据库环境 | 修改 application.yml 的 spring.profiles.active |
| 代码改了不生效 | devtools 热部署需 fork=true;或重启服务 |
1. Spring Boot 基础(IOC/AOP/自动装配)
2. 阅读 pom.xml + 配置文件
3. 跑通登录全链路(验证码→登录→Token→鉴权)
4. MyBatis + PageHelper 写第一个 CRUD
5. AOP 日志/数据源切换
6. wechat 模块(小程序业务)
7. Netty 基础 → netty 编解码 → MyServerHandler
8. Redis 专题(第 7 章)贯穿 netty 与 backend
9. 对照协议文档,完整走一遍"设备上报→状态存储→用户查询→控制下发"
学习主线一句话:设备 --(TCP)--> netty --(Redis状态/MySQL)--> backend --(HTTP/JSON)--> 小程序/管理后台。把这条链路里的每一步数据流画通,整个项目就学懂了。