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框架的授课者@小傅哥。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐