◀ 索引angkela_backend 学习指南

昂科拉 (angkela) 后端学习指南

面向新同学的入门与进阶学习文档,涵盖 angkela_backendangkela_netty 两个核心模块。

本文档基于源码分析编写,路径均指仓库内实际文件。


目录

  1. 系统概览
  2. 技术栈总览
  3. angkela_backend 依赖库详解
  4. 为什么这样设计
  5. 源码目录结构与分层
  6. 学习路线图
  7. Redis 专题教程(结合 angkela_netty)
  8. 常见问题与排错

1. 系统概览

仓库根目录包含四个部分:

目录作用
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


2. 技术栈总览

2.1 angkela_backend(Spring Boot 2.1.1)

技术版本用途
Spring Boot2.1.1.RELEASE微服务基础框架
Spring MVC-REST 接口
Spring Security + JWTjjwt 0.9.0认证授权(无状态 Token)
MyBatis1.3.2 starterORM 持久层
PageHelper1.2.5分页插件
Druid1.1.14数据库连接池 + SQL 监控
Redis (Spring Data Redis + Lettuce)-缓存、Token 存储、验证码、小程序 session
FastJSON1.2.47JSON 序列化
Swagger22.9.2接口文档
Apache POI3.17Excel 导入导出
Velocity1.7代码生成模板
OSHI + JNA3.9.1服务器监控(CPU/内存)
阿里云 SDK (OSS/ECS)-文件存储、云服务器操作
JSCH / commons-net-SFTP / FTP 文件传输
BouncyCastle1.46加密算法库
UserAgentUtils1.19解析浏览器/操作系统

2.2 angkela_netty(Spring Boot 1.5.6)

技术版本用途
Netty4.1.25.Final设备 TCP 长连接服务端 + 客户端
Spring Boot1.5.6.RELEASE基础框架(老版本,与 Netty 独立使用)
MyBatis1.3.2持久层
Redis + commons-pool2-设备状态缓存、分布式延时任务
Protostuff + Objenesis1.0.10POJO 二进制序列化(protobuf 风格,省带宽)
Apache MINA2.0.16遗留的会话工具依赖
Shiro + shiro-redis1.4.0认证框架(部分接口使用)
fluent-hc4.5.6HTTP 客户端(调用后台/第三方)
spring-boot-starter-mail-邮件告警
阿里云 SDK-SMS 短信、ECS

3. angkela_backend 依赖库详解

3.1 核心框架

spring-boot-starter / web / aop / test / devtools

spring-boot-starter-security

3.2 数据层

mybatis-spring-boot-starter + pagehelper-spring-boot-starter

druid-spring-boot-starter

mysql-connector-java

3.3 缓存与会话

spring-boot-starter-data-redis + commons-pool2

3.4 认证与安全

jjwt(io.jsonwebtoken)

bcprov-jdk15(BouncyCastle)

UserAgentUtils(bitwalker)

3.5 工具与业务库

说明典型用法
fastjson高性能 JSONJSONObjectJSON.toJSONString
commons-lang3 / commons-io字符串/IO 工具StringUtils、文件读写
commons-fileupload文件上传头像/图片上传
jschSFTP上传固件到设备升级服务器(SFTPUtil
commons-netFTP传统 FTP 上传
aliyun-java-sdk-core / ecs阿里云 API操作 ECS、发短信(AliSMS
aliyun-sdk-ossOSS 对象存储存储固件/图片
poi-ooxmlExcel@Excel 注解导出
velocity模板引擎代码生成器生成 Controller/Service/Mapper
oshi-core + jna + jna-platform系统信息服务器监控 Server/Cpu/Jvm
springfox-swagger2 + uiAPI 文档/swagger-ui.html
spring-context-support缓存抽象等辅助 Spring 集成

4. 为什么这样设计

4.1 为什么基于 RuoYi(若依)框架

angkela_backend 的包结构(common/framework/project)与经典的 RuoYi 框架几乎一致,这是在 RuoYi 基础上二次开发的。选择它的理由:

4.2 为什么 JWT 里只存 UUID,用户信息放 Redis

传统 JWT 把用户信息直接加密进 Token,一旦需要"踢人/封号"就无法撤销。

本项目设计(TokenService.java):

  1. 登录成功后生成 UUID Token,将 LoginUser 写入 Redis(key:login_tokens:<uuid>),TTL 30 分钟。
  2. JWT 中只存这个 UUID,签名防篡改。
  3. 每次请求由 JwtAuthenticationTokenFilter 解析 Token → 查 Redis → 还原用户。
  4. 有效期不足 20 分钟时自动 refreshToken 续期(滑动过期)。

核心收益:登出=删 Redis key;封禁=删 key;多端登录管理简单。

4.3 为什么设备服务单独用 Netty(angkela_netty)

4.4 为什么用 Redis 做"分布式延时任务"

净水器"预约升级""滤芯到期提醒""严重故障告警"等场景,需要到点执行

用 Redis 的 Key 过期事件Keyspace notifications)实现:

4.5 为什么分了这么多 environment 配置文件

application-dev/dev2/dev3/test/prod/prodA/prodf.yml 对应不同环境(本机、测试服务器、阿里云正式机等),通过 spring.profiles.active 切换。好处是数据库、Redis 地址等环境差异集中管理,发布时只改一个参数。


5. 源码目录结构与分层

5.1 angkela_backend

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(通用)

5.2 angkela_netty

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 → 设备

6. 学习路线图

阶段一:环境与基础(1~2 周)

  1. 学会用 IDEA 打开 angkela_backend,了解 Maven 依赖与启动方式。
  2. 阅读 pom.xml,逐个认识依赖(对照本文第 3 章)。
  3. 阅读 application-dev.yml,理解配置(端口、Redis、数据库、token)。
  4. 跑通登录接口(验证码→登录→Token→带 Token 请求),用 Swagger 调试。
  5. 必须掌握:Spring Boot 自动装配、@Configuration/@Bean、@RestController、依赖注入。

阶段二:框架核心(2~3 周)

  1. 认证链路SysLoginService → TokenService → JwtAuthenticationTokenFilter → SecurityConfig,串起来画一张时序图。
  2. 通用层AjaxResultBaseControllerBaseEntityGlobalExceptionHandlerPageHelper 分页。
  3. MyBatis:在 mybatis/ 目录看一个 XML(如 device),掌握 resultMap、动态 SQL(<if>/<where>)。
  4. AOP 三件套LogAspect(操作日志)、DataSourceAspect(主从切换)、DataScopeAspect(数据权限)。
  5. 代码生成:用 tool 模块的代码生成器生成一个简单的 CRUD 模块,看生成的结构。

阶段三:业务模块(2~3 周)

按"简单→复杂"顺序研究:

  1. device(设备档案 CRUD)→ 最容易,掌握标准分层写法。
  2. wechat(小程序)→ 理解小程序登录换 token、设备绑定、控制指令下发(调 netty HTTP 接口)。
  3. statistical(统计)→ 大量 Redis 读取 + 报表。
  4. share / monitor / system

阶段四:Netty 与物联网(3~4 周)

  1. 前置学习 Netty 基础:EventLoopGroup、ChannelPipeline、ByteBuf、编解码器、粘包拆包。
  2. 阅读 NettyServer(启动)→ MyChannelInitializer(pipeline 组装)→ ServerDecodeHandler(解码)。
  3. 阅读 MyServerHandler,对照协议文档文档/云平台5.昂科拉/通讯协议)理解功能码。
  4. 结合第 7 章 Redis 专题理解设备状态如何在 Netty 与 backend 之间共享。

阶段五:Redis 深入(详见第 7 章)


7. Redis 专题教程(结合 angkela_netty)

本项目的 Redis 是两个服务之间的"共享内存",理解它 = 理解整个物联网架构。

7.1 Redis 是什么

Redis 是内存键值数据库,支持 String/Hash/List/Set/ZSet 五种数据结构,支持设置过期时间(TTL),单线程执行命令保证原子性。本项目用它解决:

7.2 项目中的 Redis 配置

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

7.3 RedisCache 工具类(两个项目基本一致)

封装了 RedisTemplate 的所有常用操作,业务代码只用它,不要直接 new RedisTemplate:

方法底层命令用途
setCacheObject(key, value)SET缓存对象/字符串
setCacheObject(key, value, timeout, unit)SETEX带过期时间缓存
getCacheObject(key)GET取缓存
deleteObject(key)DEL删缓存
setCacheList/getCacheListLPUSH/LRANGEList 操作
setCacheSet/getCacheSetSADD/SMEMBERSSet 操作
setCacheMap/getCacheMapHMSET/HGETALLHash 操作
keys(pattern)KEYS按通配符查 key(* 慎用,阻塞大)
refresh(key, seconds)-重设 TTL 续期

7.4 典型场景一:设备实时状态共享(netty 写 → backend 读)

这是最核心的跨服务共享场景。

  1. 设备上报状态帧 → MyServerHandler 解析后:
  2. // MyServerHandler.java:"n">698
    redisCache.setCacheObject(mac + "devSta", JSONObject.toJSONString(deviceState));
    redisCache.setCacheObject(mac + "devStaHex", HexUtils.bytesToHex(bytes));

key 是 <MAC>devSta,value 是设备状态的 JSON 字符串。

  1. backend 的小程序接口要查设备状态,直接:
  2. // 如 wechat 模块
    "k">Object obj = redisCache.getCacheObject(mac + "devSta");
    JSONObject state = JSON.parseObject(obj.toString());

或 netty 自己的 DeviceController 也直接读这个 key(getCacheObject(strMac+"devSta"))。

好处:设备每秒上报,不必每次查 MySQL;状态是"最新的",Redis 充当两级存储中的热数据层。

7.5 典型场景二:登录会话 / Token(backend)

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

登录后每次请求由 JwtAuthenticationTokenFiltergetLoginUser

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 观察有效期;调接口后再看是否刷新(滑动续期)。

7.6 典型场景三:Key 过期事件实现延时任务(重点)

Redis 的 Keyspace Notifications(键空间通知)能发布"key 过期"事件。项目借此实现延时任务。

  1. 配置监听容器(两个项目都有 RedisListenerConfig):
  2. @Bean
    RedisMessageListenerContainer container(RedisConnectionFactory connectionFactory) {
        RedisMessageListenerContainer container = "k">new RedisMessageListenerContainer();
        container.setConnectionFactory(connectionFactory);
        "k">return container;
    }
  1. 监听器继承 KeyExpirationEventMessageListener,重写 doHandleMessage
  2. @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 与业务标识,分发处理
        }
    }
  1. 业务上如何"预约到点执行"(backend 的预约升级):
  2. // 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 小时。

  1. netty 侧的应用(config/RedisKeyExpirationListener.java):
    • openWebSetTime 过期:页面监控超时,发送 WRITE_A 帧让设备退出网页控制模式。
    • adjustCity 过期:重新解析设备所在城市并写库。
    • seriousFault 过期:严重故障 → 微信/短信告警、写故障通知表。
    • weixin_access_token 过期:自动刷新微信 access_token(提前 60 秒续期)。

注意:Redis 默认不开启过期通知,需要设置 notify-keyspace-events Ex(项目生产环境已配置)。命令:

```

config set notify-keyspace-events Ex

```

事件只保证"最终送达",极端情况可能丢失,所以项目里还会有补偿逻辑(如升级失败顺延)。

7.7 典型场景四:防重复/去重(netty)

7.8 典型场景五:分布式计数(netty 自动升级)

redisCache.setCacheObject("autoUpNum", deviceList.size()); // 总设备数
redisCache.setCacheObject("autoUpPage", "n">1);                // 当前页
// 分页升级,逐批给设备下发 :autoUp 并设置 "n">7200 秒 TTL

7.9 Redis 使用规范(本项目约定)

  1. key 命名<MAC>业务标识<MAC>:业务标识,如 AB12CD34devStaAB12CD34:reservationUp。前缀可以按业务分组,但注意不要过长。
  2. TTL 必设:凡是缓存都设过期时间,避免 Redis 无限膨胀。
  3. 只走 RedisCache 工具类:不要绕过封装直接用 template,保持统一序列化策略。
  4. 大 key 警惕redisCache.keys("*") 在测试代码里出现过,线上严禁KEYS *(会阻塞 Redis),要用 SCAN
  5. 序列化一致:两个项目写同一个 key(如 mac+devSta)时,value 用纯 JSON 字符串,这样跨项目反序列化互不依赖(backend 用 FastJSON、netty 也用 FastJSON,格式一致即可)。

7.10 Redis 学习资源

主题建议
基础命令Redis 官方文档 https://redis.io/docs/latest/commands/
数据结构String/Hash/List/Set/ZSet 各写 5 个命令练习
Spring Data RedisRedisTemplate API 与本文第 7.3 节对照
过期通知搜 "Redis keyspace notification KeyExpirationEventMessageListener"
集群/哨兵了解即可,本项目单机 Redis

8. 常见问题与排错

现象可能原因
启动报 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.ymlspring.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)--> 小程序/管理后台。把这条链路里的每一步数据流画通,整个项目就学懂了。