苍穹外卖[Day 2]记录-(1)-新增员工
新增员工根据产品原型进行需求分析--设计功能接口--设计后端返回数据格式--数据库设计employee表新增员工代码位置及原因分析新的新增员工代码应该写在EmployeeController类中这是基于软件工程的“分层架构”和“职责单一原则”1. 分层架构原则三层/MVC在一个典型的 Spring Boot 应用中代码被划分为不同的层各司其职层级职责对应的类ControllerWeb 层接收和响应 HTTP 请求处理请求路由调用 Service 层。EmployeeControllerService业务逻辑层实现核心业务逻辑事务控制对数据进行计算和处理。EmployeeServiceMapper/Repository数据持久化层直接与数据库交互进行 CRUD 操作。EmployeeMapper/EmployeeRepository新增员工是一个外部用户发起的 HTTP 操作其职责属于处理外部请求的Controller 层。因此将新增员工的接口定义和请求处理放在EmployeeController是完全符合架构规范的。2. 职责单一原则Single Responsibility PrincipleEmployeeController类是专门负责“员工管理”相关的 Web 接口的。登录 (/login) 属于员工管理。退出 (/logout) 属于员工管理。新增员工 (/) 同样属于员工管理。未来可能有的修改员工、查询员工列表等功能都应该放在这里。将所有与员工管理相关的接口集中在一个 Controller 中可以提高内聚性相关的代码都在一起便于查找和维护。清晰路由客户端通过路径/admin/employee/...就能清晰知道请求是关于员工管理的。DTO是什么DTO的全称是Data Transfer Object中文译为数据传输对象。DTO 是一种设计模式它的核心目的是在应用层的不同部分之间如 Controller 层和 Service 层之间或者前端和后端之间传输数据。它是一个纯粹的数据载体只包含公共的字段属性以及对应的 getter/setter 方法或使用 Lombok 的Data自动生成不包含任何业务逻辑。DTO 的定位在一个典型的后端应用中我们会区分三种常见的对象类型对象类型缩写全称/作用用途DTOData Transfer Object数据传输对象用于传输数据。通常作为Controller 的入参接收前端数据或Service 层的出参。Entity实体持久化对象直接映射数据库中的表结构。用于Mapper/Repository 层进行数据库操作。VOView Object视图对象专门用于前端展示的数据结构。通常作为Controller 的最终出参响应给前端。“使用 DTO 接收”就是指接收输入客户端前端通过 HTTP 请求体发送 JSON 数据。Spring 自动转换Spring 框架通过RequestBody自动将这个 JSON 结构映射反序列化成一个EmployeeLoginDTO的 Java 对象实例。Controller 使用Controller 方法接收并使用这个EmployeeLoginDTO对象来获取用户名、密码等输入数据。示例新增员工时使用 DTO 接收前端发送的 JSON (HTTP Request Body):{ username: zhangsan, name: 张三, gender: 1, idNumber: 1234567890 }后端接收的 DTO (EmployeeSaveDTO):public class EmployeeSaveDTO { private String username; private String name; private Integer gender; private String idNumber; // ... (省略 getter/setter) }Spring 将 JSON 字段值填充到EmployeeSaveDTO对象的对应属性中。RequestBody用于标注在Controller 方法的参数上。它的核心作用是解析请求体告诉 Spring 框架需要将 HTTP 请求如 POST、PUT 方法的整个**请求体Request Body**内容读取出来。反序列化使用配置的HTTP Message Converters消息转换器如 Jackson 库用于处理 JSON将请求体中的数据通常是JSON或XML格式自动解析并转换反序列化成对应的Java 对象实例例如 DTO。employeeService.save(employeeDTO)调用 Service 方法时您需要在EmployeeService接口和EmployeeServiceImp实现类中都定义save(EmployeeDTO employeeDTO)方法当 Spring 启动时Spring 扫描到Service标记的EmployeeServiceImp将其创建为一个 Bean。Spring 扫描到EmployeeController中的Autowired private EmployeeService employeeService;。因为EmployeeController依赖于EmployeeService接口Spring 就会将EmployeeServiceImp这个实现类的实例注入给 Controller。最终Controller调用employeeService.save(...)时执行的正是EmployeeServiceImp中包含具体业务逻辑的那个save方法。写接口实现类时用到的方法解析一、StatusConstant类的理解为什么用常量类如果你在代码里直接写employee.setStatus(1);别人不知道1是什么意思。如果你写employee.setStatus(StatusConstant.ENABLE);含义一目了然提高了可读性。核心语法点public static final关键字含义作用public公开的允许项目中的任何地方都能访问到这个常量。static静态的这个属性属于类本身而不是类的某个对象。这意味着你不需要创建StatusConstant的对象就能使用它。final最终的这个属性的值1或0是固定不变的。一旦定义就不能被修改保证了代码的稳定性和安全性。假设没有StatusConstant类你想设置一个员工为启用状态你会直接这样写// 硬编码直接把数字“1”写死在业务逻辑中 employee.setStatus(1); // 这里的 1 就是硬编码存在的问题可读性差 (Magic Number)当你看到employee.setStatus(1)你必须去翻阅文档才知道1到底代表“启用”、“男”还是“已付款”。这个1被称为“魔法数字”Magic Number。维护困难如果有一天老板要求把“启用”的状态码从1改成E。你必须翻遍项目里所有写了setStatus(1)的地方一个一个手动改成setStatus(E)。如果你漏改了一个地方代码就会出现Bug因为数据库期待E但你传了1。容易出错在多处手写1时很容易不小心写成11或01等错误的值。什么是“避免硬编码”避免硬编码就是用一个有意义的、统一的名字来代替那个固定的、可能会变动的数值。使用了StatusConstant类后你的代码变成这样// 避免硬编码使用常量名代替数字 employee.setStatus(StatusConstant.ENABLE);带来的好处极高的可读性任何人看到StatusConstant.ENABLE都立即明白这是“启用”的意思无需查阅文档。维护性强一处修改全局生效如果状态码需要从1改成E你只需要修改一个地方在StatusConstant.java文件中把public static final Integer ENABLE 1;改成public static final Integer ENABLE E;整个项目中使用StatusConstant.ENABLE的地方都会自动更新你不需要改动任何业务逻辑代码。安全性编译时检查你不可能拼错StatusConstant.ENABLE因为它是一个类属性。如果你写错了Java 编译器会立刻报错而不是等到程序运行才发现错误。二、关于在接口拓展insert方法1. 为什么下一步是在接口里拓展这个insert方法回答因为这个接口就是你和 MyBatis 框架之间的契约合同。接口的作用你和 MyBatis 签合同你开发者你在这个EmployeeMapper接口中声明了void insert(Employee employee);。你是在告诉 MyBatis“我需要一个能插入员工数据的方法。”MyBatis框架MyBatis 在程序启动时会自动读取这个接口发现你声明了这个方法并且在上面用Insert注解写了 SQL 语句。运行时发生的事情当 Service 层代码调用employeeMapper.insert(employee);时MyBatis 不会去找一个手动编写的实现类。MyBatis 会在运行时根据你在这个接口上写的注解 (Insert和 SQL)动态地、偷偷地生成一个实现了这个接口的代理对象你可以理解为一个“虚拟实现类”。这个代理对象接收到insert()调用后会立刻执行你提供的 SQL 语句并将employee对象的数据自动填充进去。2. 为什么可以在这里写 SQL 语法Insert注解的使用回答因为 MyBatis 提供了像Insert这样的注解Annotation它的目的就是用来嵌入 SQL 语句。注解 (Insert) 的工作方式告诉 MyBatis 意图Insert告诉 MyBatis“这个方法是用来执行数据库插入操作的。”提供指令注解后面跟着的字符串就是 MyBatis 需要执行的SQL 语句。数据如何自动填充#{...}的使用请看 SQL 语句中的这部分... values (#{name},#{username},#{password},...)这里的#{name}不是普通的字符串它是 MyBatis 的一个占位符。当 Service 层调用insert(Employee employee)并传入一个employee对象时MyBatis 会自动查找这个对象里叫name的属性把它的值取出来安全地替换掉 SQL 语句中的#{name}。整个过程就像这样你声明insert(Employee employee)。你告诉 MyBatis执行INSERT INTO ... VALUES (#{name}, ...)。你传入一个员工对象e它的name是张三。MyBatis 自动将 SQL 变成INSERT INTO ... VALUES (张三, ...)然后发送给数据库。关于持久层的理解1. 什么是“持久化”Persistence“持久化”这个词指的是将数据从易失性存储比如程序运行时的内存转换为非易失性存储比如硬盘上的数据库从而使数据在程序结束后仍然能够长期存在持久。内存中的数据当程序关闭时内存中的对象和变量就会消失。持久化的数据存储在数据库、文件或磁盘上的数据即使服务器重启它们也不会丢失。持久层的核心职责就是处理这种数据的长期存储和检索。2. Mapper/DAO 的职责Mapper在 MyBatis 等框架中常用或DAO数据访问对象就是专门用来执行持久化操作的代码组件。它的具体职责包括连接数据库建立和管理与数据库的连接。SQL 语句执行负责编写、组织和执行 SQL 语句如INSERT、SELECT、UPDATE、DELETE。数据转换将数据库返回的结果集ResultSet转换成 Java实体对象Entity反之亦然。结论因为Mapper承担了所有与数据库交互、实现数据持久化的工作所以它被称为持久层。3. 在三层架构中的位置在常见的后端三层架构中职责是明确划分的层次 (Layer)名称职责核心类组件第一层表现层(Presentation/Web)接收和响应用户的请求处理参数校验和结果返回格式。Controller第二层业务逻辑层(Business/Service)实现核心业务流程制定业务规则处理事务管理。Service(ServiceImpl)第三层持久层(Persistence/Data Access)负责与数据库的交互实现数据的持久化操作。Mapper/DAOService层需要新增一个员工它自己并不知道如何写 SQL它只会调用mapper.insert(employee)然后持久层Mapper负责完成剩下的数据库操作。这种分层设计使得每一层职责单一、耦合度低易于维护和测试。通过接口文档测试localhost:8080/doc.html响应码401 报错通过断点调试进入到JwtTokenAdminInterceptor拦截器报错原因由于JWT令牌校验失败导致EmployeeController的save方法没有被调用解决方法调用员工登录接口获得一个合法的JWT令牌使用admin用户登录获取令牌添加令牌将合法的JWT令牌添加到全局参数中文档管理--全局参数设置--添加参数关掉页面重新使用admin用户登录获取令牌对象属性拷贝在数据库中也可以看到新建员工成功测试结束代码完善目前程序存在的问题主要有两个录入的用户名已存抛出的异常后没有处理当数据库中已存在相同的用户名时数据库会抛出唯一约束Unique Constraint异常。如果不处理这个底层的数据库错误会被直接抛到 Controller 层最终可能导致服务器返回 500 错误码username是unique的唯一索引所以数据库会抛异常--异常类型SQLIntegrityConstraintViolationException但目前对这个异常没有处理方式需要修改新增员工时创建人id和修改人id设置为固定值(createUser和updateUser字段在审计Audit中非常重要它们必须是当前登录操作者的 ID而不是固定的假数据例如10L。要获取当前登录用户的 ID程序需要依赖用户认证和授权Security机制。解决方案是利用 ThreadLocal 机制在用户请求的整个生命周期中存储当前用户的 ID)接下来将对上述两个问题依次进行分析和解决问题一通过全局异常处理器来处理。进入到sky-server模块com.sky.hander包下GlobalExceptionHandler.java添加方法/** * 处理SQL异常 * param ex * return */ ExceptionHandler public Result exceptionHandler(SQLIntegrityConstraintViolationException ex){ //Duplicate entry zhangsan for key employee.idx_username String message ex.getMessage(); if(message.contains(Duplicate entry)){ String[] split message.split( ); String username split[2]; String msg username MessageConstant.ALREADY_EXISTS; return Result.error(msg); }else{ return Result.error(MessageConstant.UNKNOWN_ERROR); } }进入到sky-common模块在MessageConstant.java添加public static final String ALREADY_EXISTS 已存在;步骤操作目的1. 捕获使用ExceptionHandler捕获特定异常。确保只有SQLIntegrityConstraintViolationException会进入此方法。2. 获取信息调用ex.getMessage()获取详细错误信息字符串。数据库异常信息中包含了导致冲突的具体数据例如哪个用户名重复了。3. 判断类型使用message.contains(Duplicate entry)进行判断。确认这是最常见的唯一键冲突而不是外键约束等其他约束冲突。4. 解析数据使用split( )分割字符串提取重复的值。将zhangsan这个重复的用户名从长串信息中提取出来。5. 构造提示将提取的值与预定义的提示常量拼接。构造出如zhangsan 已存在这样的友好提示。6. 返回调用Result.error(msg)返回统一的错误结构。前端收到结构化数据展示给用户。问题二通过某种方式动态获取当前登录员工的id。员工登录成功后会生成JWT令牌并响应给前端package com.sky.controller.admin; /** * 员工管理 */ RestController RequestMapping(/admin/employee) Slf4j Api(tags 员工相关接口) public class EmployeeController { Autowired private EmployeeService employeeService; Autowired private JwtProperties jwtProperties; /** * 登录 * * param employeeLoginDTO * return */ PostMapping(/login) ApiOperation(value 员工登录) public ResultEmployeeLoginVO login(RequestBody EmployeeLoginDTO employeeLoginDTO) { //......... //登录成功后生成jwt令牌 MapString, Object claims new HashMap(); claims.put(JwtClaimsConstant.EMP_ID, employee.getId()); String token JwtUtil.createJWT( jwtProperties.getAdminSecretKey(), jwtProperties.getAdminTtl(), claims); //............ return Result.success(employeeLoginVO); } }分页查询/** * 员工分页查询 * param employeePageQueryDTO * return */ GetMapping(/page) //1. 请求方法GET 2. 请求路径/employee/page ApiOperation(员工分页查询) //文档显示员工分页查询前端知道接口用途 public ResultPageResult page(EmployeePageQueryDTO employeePageQueryDTO){ log.info(员工分页查询参数为{}, employeePageQueryDTO); PageResult pageResult employeeService.pageQuery(employeePageQueryDTO); return Result.success(pageResult); }┌──────────────────────────────────────────────────────────────────────┐ │ 数据转换全流程 │ └──────────────────────────────────────────────────────────────────────┘ Java对象 (后端内存) 序列化 JSON文本 (HTTP传输) ┌────────────────┐ ┌───────┐ ┌──────────────────┐ │ PageResult │ │ │ │ { │ │ ┌──────────┐ │ │ JSON │ │ total: 100, │ │ │ total100│ │───────│ 序列化 │──────────│ records: [...]│ │ └──────────┘ │ │ │ │ } │ │ ┌──────────┐ │ │ │ └──────────────────┘ │ │records[…]│ │ │ │ │ │ └──────────┘ │ │ │ │ └────────────────┘ └───────┘ ▼ │ HTTP响应体 │ 发送给浏览器 │ ▼ Java内存中的对象 网络传输的文本 (只有JVM能理解) (任何语言都能解析)Result.success() 是什么流程