小智AI复现日志02
本次复现虽然代码不多,但是我作为一个初学者也是花了一天的时间才勉强完成,有疏漏的地方还请各位读者谅解,也可以在评论区指出我的不足之处,我会加紧学习的。使用ddd架构复现小智ai并不是说原本的架构不好。只是我想要用这种方法吃透小智ai和ddd架构本身,如果只是使用mvc复现我可能会不停的复制粘贴,这就无法学习到新的知识了。
DDD架构复现小智AI02
在上一次复现中我成功跑通源码,并且在DDD架构下完成数据库po对象的改写,这次复现的目的是实现用户管理相关的代码。
引入API管理工具Apipost
注意到小智项目源码在每个Contorller中都使用了swagger3的注释,所以可以在启动项目后网址http://172.19.8.14:8091/v3/api-docs记录的项目所有的api,然后再导入Apipost软件或者任意api管理软件中。导入时选择Swagger URL粘贴http://172.19.8.14:8091/v3/api-docs即可。这里对网址进行解释172.19.8.14:8091为源码启动后打印知识中的前部分
🔧 OTA服务地址: http://172.19.8.14:8091/api/device/ota,其中8091为项目使用的端口,可以在src/main/resources/application.properties中修改,v3表示Swagger的版本,api-docs保持即可。
用户登录复现
由于DDD架构只有api层对外暴露,所以对外的接口都要写在这一层,登录接口也不例外,接口写好后由trigger层实现。又由于需要使用swagger3注释接口,所以需要在配置文件中加上如下配置.当use-fqn设置为 true
时,SpringDoc 使用完整的包路径+类名+方法名作为标识符,虽然这里一个接口只会有一个实现类,但是还是需要设置为true,这是在接口添加注解的前提条件。在接口添加注解的目的是让整个项目只有api层对外暴露,实现类只管实现就可以了
#用于在接口上添加注释
springdoc:
use-fqn: true
show-actuator: true
api-docs:
path: /api-docs
swagger-ui:
path: /swagger-ui.html
加上这个配置就可以实现在接口上添加相关注解,实现类只需要加@RestController即可,springdoc会自动扫描。
api层编写代码
在api层新建类UserLoginRequestDTO和UserLoginResponseDTO暴露给外面,这里面的变量我是通过启动源码,然后打开浏览器按下f12,在网络一栏监控登录发出的请求和收到的响应得到的。
源码的请求为http://localhost:8087/api/user/login,请求方法为POST,负载为{“username”:“admin”,“password”:“123456”,“rememberMe”:false},响应为如下JOSN字符串。依次对应从源码中复制粘贴到新的类中即可,同时可以在这些变量上添加如@Schema(description = “用户名”)注解,以供Swagger3使用。
{
“code”: 200,
“data”: {
“createTime”: “2025-03-09 18:32:29”,
“updateTime”: null,
“userId”: 1,
“startTime”: null,
“endTime”: null,
“username”: “admin”,
“name”: “小智”,
“totalMessage”: 0,
“aliveNumber”: 0,
“totalDevice”: 0,
“avatar”: null,
“state”: “1”,
“isAdmin”: “1”,
“roleId”: null,
“tel”: null,
“email”: null,
“loginIp”: null,
“code”: null,
“loginTime”: null
},
“message”: “操作成功”
}
在查看响应实例可知data部分需要是泛型的,为此我编写响应类如下。
package cn.yangfanqihang.xiaozhi.api.response;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class Response<T> implements Serializable {
private static final long serialVersionUID = 1L;
private Integer code;
private T data;
private String message;
}
在 Java 中,实现 Serializable
接口的类可以被序列化。serialVersionUID
是用于 版本控制的唯一标识符。如果没有显式定义 serialVersionUID
,系统会根据类的结构自动生成一个。当类发生微小改动(如新增方法、修改方法实现)时,系统自动生成的 serialVersionUID
可能会发生变化。对于 Java 原生序列化,如果使用旧的序列化数据反序列化到新版本类时,serialVersionUID
不匹配就会抛出 InvalidClassException
。需要注意的是,如果你使用的是 JSON 序列化/反序列化,通常不会使用 serialVersionUID
,因为 JSON 反序列化只关心字段名和类型,而不依赖类的版本号。不过,显式定义 serialVersionUID
仍然是一个良好的习惯,以保证类在使用 Java 原生序列化时的兼容性。
随后我编写了接口IUserController供外部调用和trigger层实现。
package cn.yangfanqihang.xiaozhi.api;
import cn.yangfanqihang.xiaozhi.api.dto.UserLoginRequestDTO;
import cn.yangfanqihang.xiaozhi.api.dto.UserLoginResponseDTO;
import cn.yangfanqihang.xiaozhi.api.response.Response;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.web.bind.annotation.*;
import jakarta.servlet.http.HttpServletRequest;
@RequestMapping("/api/user")
@Tag(name = "用户管理", description = "用户相关操作")
public interface IUserController {
@PostMapping("/login")
@Operation(summary = "用户登录", description = "返回登录结果")
Response<UserLoginResponseDTO> login(@RequestBody UserLoginRequestDTO requestDTO, HttpServletRequest request);
}
前文提到在接口上加注解@RequestMapping(“/api/user”)需要依赖于springdoc技术,加上注解以后外部来的以/api/user开头的请求都会走这个接口的实现类。
Trigger层代码编写
package cn.yangfanqihang.xiaozhi.trigger.http;
import cn.yangfanqihang.xiaozhi.api.IUserController;
import cn.yangfanqihang.xiaozhi.api.dto.UserLoginRequestDTO;
import cn.yangfanqihang.xiaozhi.api.dto.UserLoginResponseDTO;
import cn.yangfanqihang.xiaozhi.api.response.Response;
import cn.yangfanqihang.xiaozhi.domain.user.model.valobj.UserLoginVO;
import cn.yangfanqihang.xiaozhi.domain.user.service.IUserService;
import cn.yangfanqihang.xiaozhi.types.enums.ResponseCode;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpSession;
import org.springframework.beans.BeanUtils;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class UserController implements IUserController {
@Resource
private IUserService userService;
@Override
public Response<UserLoginResponseDTO> login(UserLoginRequestDTO requestDTO, HttpServletRequest request) {
try {
String username = requestDTO.getUsername();
String password = requestDTO.getPassword();
UserLoginVO userLoginVO= userService.login(username,password);
UserLoginResponseDTO userLoginResponseDTO = new UserLoginResponseDTO();
BeanUtils.copyProperties(userLoginVO,userLoginResponseDTO);
userLoginResponseDTO.setAliveNumber(userLoginVO.getDeviceStatsVO().getAliveNumber());
userLoginResponseDTO.setTotalDevice(userLoginVO.getDeviceStatsVO().getTotalDevice());
userLoginResponseDTO.setTotalMessage(userLoginVO.getDeviceStatsVO().getTotalMessage());
// 保存用户到会话
HttpSession session = request.getSession();
session.setAttribute(UserLoginResponseDTO.USER_SESSIONKEY, userLoginResponseDTO);
return Response.<UserLoginResponseDTO>builder()
.code(ResponseCode.SUCCESS.getCode())
.message(ResponseCode.SUCCESS.getMessage())
.data(userLoginResponseDTO)
.build();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
源码保存登录状态使用的session技术,还有另外一种方案是JWT/Token方案,对比如下。由于需要修改前端相关代码才能实现JWT/Token方案,这里先不做改动,目前以复现源码为主。其他代码逻辑基本和源码保持一致不做过多解释了。
对比维度 | Session 方案 🗂️ | JWT/Token 方案 🔑 |
---|---|---|
安全性 | 服务端可控,可强制失效;易受 CSRF 攻击 | Token 无状态,泄露风险大;需 HTTPS + 短期有效 + 刷新机制 |
性能 | 服务端需存 Session,分布式要用 Redis | 无需存储,天然支持分布式,性能更好 |
实现难度 | 简单,框架内置 | 较复杂,需要 Token 签发/校验/刷新机制 |
前端支持 | 浏览器天然支持(Cookie 自动带上) | 需要前端代码存储和携带 Token |
强制下线 | 支持,直接销毁 Session 即可 | 不支持,需引入黑名单机制 |
适用场景 | 传统 Web 应用,后台管理系统 | 分布式、微服务、移动端/前后端分离系统 |
Domain层代码编写
在domain层我建了user包用于专门处理用户管理相关代码逻辑和相关实体传输类等。新建类UserEntity和UserLoginVO,UserEntity用于定义查询数据库的接口,UserLoginVO用于给Controller提供返回值,详细信息在我提供的代码中查看荣先海/xiaozhi-esp32-ddd - Gitee.com。
登录服务实现类如下
package cn.yangfanqihang.xiaozhi.domain.user.service.impl;
import cn.yangfanqihang.xiaozhi.domain.user.adapter.repository.IUserRepository;
import cn.yangfanqihang.xiaozhi.domain.user.model.entity.UserEntity;
import cn.yangfanqihang.xiaozhi.domain.user.model.valobj.UserLoginVO;
import cn.yangfanqihang.xiaozhi.domain.user.service.IAuthenticationService;
import cn.yangfanqihang.xiaozhi.domain.user.service.IUserService;
import cn.yangfanqihang.xiaozhi.types.enums.ResponseCode;
import cn.yangfanqihang.xiaozhi.types.exception.AppException;
import jakarta.annotation.Resource;
import org.apache.commons.lang3.ObjectUtils;
import org.springframework.beans.BeanUtils;
import org.springframework.stereotype.Service;
@Service
public class UserServiceImpl implements IUserService {
@Resource
private IAuthenticationService authenticationService;
@Resource
private IUserRepository repository;
@Override
public UserLoginVO login(String username, String password) {
UserEntity userEntity=repository.queryUserEntityBuyUsername(username);
if (ObjectUtils.isEmpty(userEntity)) {
throw new AppException(ResponseCode.USERNAME_NOTFOUND.getCode(),ResponseCode.USERNAME_NOTFOUND.getMessage());
} else if (!authenticationService.isPasswordValid(password, userEntity.getPassword())) {
throw new AppException(ResponseCode.USER_PASSWORD_ERROR.getCode(),ResponseCode.USER_PASSWORD_ERROR.getMessage());
}
UserLoginVO userLoginVO = new UserLoginVO();
BeanUtils.copyProperties(userEntity,userLoginVO);
userLoginVO.setDeviceStatsVO(repository.queryDeviceStatsVOBuyUserUserId(userEntity.getUserId()));
return userLoginVO;
}
}
@Resource是spring的一种注入方式,按变量名注入优先,登录认证的方法与源码一致不做修改。IUserRepository创建在domain层的domain/user/adapter/repository包下实现在infrastructure层。这就实现了业务和仓储分离的效果了。
Infrastructure层代码编写
在infrastructure层实现了domain层定义的两个接口queryUserEntityBuyUsername和queryDeviceStatsVOBuyUserUserId。代码如下
package cn.yangfanqihang.xiaozhi.infrastructure.adapter.repository;
import cn.yangfanqihang.xiaozhi.domain.user.adapter.repository.IUserRepository;
import cn.yangfanqihang.xiaozhi.domain.user.model.entity.UserEntity;
import cn.yangfanqihang.xiaozhi.domain.user.model.valobj.UserLoginVO;
import cn.yangfanqihang.xiaozhi.infrastructure.dao.ISysDeviceDao;
import cn.yangfanqihang.xiaozhi.infrastructure.dao.ISysMessageDao;
import cn.yangfanqihang.xiaozhi.infrastructure.dao.ISysUserDao;
import cn.yangfanqihang.xiaozhi.infrastructure.dao.po.SysDevice;
import cn.yangfanqihang.xiaozhi.infrastructure.dao.po.SysUser;
import cn.yangfanqihang.xiaozhi.types.utils.DateUtils;
import jakarta.annotation.Resource;
import org.springframework.beans.BeanUtils;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
public class UserRepository implements IUserRepository {
@Resource
private ISysUserDao userDao;
@Resource
private ISysDeviceDao deviceDao;
@Resource
private ISysMessageDao messageDao;
@Override
public UserEntity queryUserEntityBuyUsername(String username) {
SysUser sysUser=userDao.queryUserBuyUsername(username);
UserEntity userEntity = new UserEntity();
BeanUtils.copyProperties(sysUser,userEntity);
return userEntity;
}
@Override
public UserLoginVO.DeviceStatsVO queryDeviceStatsVOBuyUserUserId(Integer userId) {
Integer totalMessage= 0,totalDevice=0,aliveNumber=0;
List<SysDevice> deviceList= deviceDao.queryDeviceListBuyUserId(userId);
totalDevice=deviceList.size();
for (SysDevice sysDevice : deviceList) {
totalMessage+=messageDao.queryTotalMessageBuyDeviceId(sysDevice.getDeviceId(), DateUtils.dayOfMonthStart(),DateUtils.dayOfMonthEnd());
aliveNumber += "1".equals(sysDevice.getState()) ? 1 : 0;
}
return UserLoginVO.DeviceStatsVO.builder()
.aliveNumber(aliveNumber)
.totalDevice(totalDevice)
.totalMessage(totalMessage)
.build();
}
}
我发现源码中是使用一个语句查询三张表然后给出的返回值,既然我这里使用的DDD架构就要实现不同功能的分分离,所以我这里的查询是三张表分开查的。同时我发现源码中关于aliveNumber的查询与totalDevice相同,可能有误,我做了些许改动。
复现02总结
本次复现虽然代码不多,但是我作为一个初学者也是花了一天的时间才勉强完成,有疏漏的地方还请各位读者谅解,也可以在评论区指出我的不足之处,我会加紧学习的。使用ddd架构复现小智ai并不是说原本的架构不好。只是我想要用这种方法吃透小智ai和ddd架构本身,如果只是使用mvc复现我可能会不停的复制粘贴,这就无法学习到新的知识了。另外还有一点就是感谢小智ai作者的开源,以及ddd框架的授课者@小傅哥。
更多推荐
所有评论(0)