当前位置: 首页 > news >正文

【智能协同云图库】智能协同云图库第二弹:用户管理系统后端设计与接口开发

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述


用户管理系统


一、需求分析


对于用户模块,通常要具有下列功能:

image-20250625163902847


二、方案设计


(一)库表设计


实现用户模块的难度不大,在方案设计阶段,我们需要确认以下内容:

  1. 库表设计
  2. 用户登录流程
  3. 如何对用户权限进行控制

1. 核心设计

用户表的核心是用户登录凭证(账号密码)和个人信息,SQL如下:

-- 创建数据库
create database if not exists yu_picture;-- 切换库
use yu_picture;-- 用户表
-- 用户表
create table if not exists user
(id           bigint auto_increment comment 'id' primary key,userAccount  varchar(256)                           not null comment '账号',userPassword varchar(512)                           not null comment '密码',userName     varchar(256)                           null comment '用户昵称',userAvatar   varchar(1024)                          null comment '用户头像',userProfile  varchar(512)                           null comment '用户简介',userRole     varchar(256) default 'user'            not null comment '用户角色:user/admin',editTime     datetime     default current_timestamp not null comment '编辑时间',createTime   datetime     default current_timestamp not null comment '创建时间',updateTime   datetime     default current_timestamp not null on update current_timestamp comment '更新时间',isDelete     tinyint      default 0                 not null comment '是否删除',unique key uk_userAccount (userAccount),index idx_userName (userName)
) comment '用户' collate = utf8mb4_unicode_ci;

注意事项:

  1. editTimeupdateTime 的区别:editTime 表示用户编辑个人信息的时间(需要业务代码来更新),而 updateTime 表示这条用户记录任何字段发生修改的时间(由数据库自动更新)。
  2. 给唯一值添加唯一键(唯一索引),比如账号 userAccount,利用数据库天然防重复,同时可以增加查询效率。
  3. 给经常用于查询的字段添加索引,比如用户昵称 userName,可以增加查询效率。
  4. 建议:养成好习惯,将库表设计 SQL 保存到项目的目录中,比如新建 sql/create_table.sql 文件,这样其他开发者就能更快地了解项目。image-20250623171350250

在当前情况下,我们创建的 SQL 文件无法通过 Ctrl+Alt+L 快捷键进行格式化,因为 IDEA 在未连接数据库之前,无法识别应使用 MySQL 或其他数据库的特定格式来格式化 SQL 语句。

image-20250623172830043


按照下面的操作,配置数据源,即可格式化代码:

image-20250623175131373


执行 sql 语句:

image-20250623175722162


2. 扩展设计

(1)会员功能扩展

如果要实现会员功能,可以对表进行如下扩展:

  • userRole 字段新增枚举值 vip,表示会员用户,可根据该值判断用户权限。
  • 新增会员过期时间字段,可用于记录会员有效期。
  • 新增会员兑换码字段,可用于记录会员的开通方式。
  • 新增会员编号字段,可便于定位用户并提供额外服务,并增加会员归属感。
    对应的 SQL 如下:
vipExpireTime datetime     null comment '会员过期时间',
vipCode       varchar(128) null comment '会员兑换码',
vipNumber     bigint       null comment '会员编号'

(2)用户邀请功能扩展

如果要实现用户邀请功能,可以对表进行如下扩展:

  • 新增 shareCode 分享码字段,用于记录每个用户的唯一邀请标识,可拼接到邀请网址后面,比如 面试鸭 - 程序员求职面试刷题神器,高频编程题目免费刷。
  • 新增 inviteUser 字段,用于记录该用户被哪个用户邀请了,可通过这个字段查询某用户邀请的用户列表。

对应的 SQL 如下:

shareCode     varchar(20)  DEFAULT NULL COMMENT '分享码',
inviteUser    bigint       DEFAULT NULL COMMENT '邀请用户 id'

(二)用户登录流程


  1. 建立初始会话:前端与服务器建立连接后,服务器会为该客户端创建一个初始的匿名 Session,并将其状态保存下来。这个 Session 的 ID 会作为唯一标识,返回给前端。

  2. 登录成功,更新会话信息:当用户在前端输入正确的账号密码并提交到后端验证成功后,后端会更新该用户的 Session,将用户的登录信息(如用户 ID、用户名等)保存到与该 Session 关联的存储中。同时,服务器会生成一个 Set-Cookie 的响应头,指示前端保存该用户的 Session ID。

  3. 前端保存 Cookie:前端接收到后端的响应后,浏览器会自动根据 Set-Cookie 指令,将 Session ID 存储到浏览器的 Cookie 中,与该域名绑定。

  4. 带 Cookie 的后续请求:当前端再次向相同域名的服务器发送请求时,浏览器会自动在请求头中附带之前保存的 Cookie,其中包含 Session ID。

  5. 后端验证会话:服务器接收到请求后,从请求头中提取 Session ID,找到对应的 Session 数据。

  6. 获取会话中存储的信息:后端通过该 Session 获取之前存储的用户信息(如登录名、权限等),从而识别用户身份并执行相应的业务逻辑。

mermaid


(三)用户权限控制


可以将接口分为以下 4 种权限:

  1. 未登录也可以使用
  2. 登录用户才能使用
  3. 未登录也可以使用,但登录用户能进行更多操作(比如登录后查看全文)
  4. 仅管理员才能使用

1. 传统的权限控制方法

传统的权限控制方法,是在每个接口内单独编写逻辑:先获取到当前登录用户信息,然后判断用户的权限是否符合要求。

这种方法最灵活,但会写很多重复的代码,且其他开发者无法一眼得知接口所需要的权限。


2. 推荐的权限控制方法

权限校验其实是一个比较通用的业务需求,一般会通过 Spring AOP 切面 + 自定义权限校验注解 实现统一的接口拦截和权限校验。如果有特殊的权限校验逻辑,再单独在接口中编码。

如果需要更复杂、更灵活的权限控制,可以引入以下专门的权限管理框架:

  • Shiro
  • Spring Security
  • Sa-Token

💡 建议:选择合适的权限管理框架,可以大大简化权限控制的实现。


三、后端开发


(一)数据访问层代码生成


连接数据库:首先利用 IDEA 连接 MySQL 数据库。

image-20250623182307239


执行 SQL 脚本:创建数据库表。

image-20250623175722162


生成代码:数据访问层的代码一般包括实体类、MyBatis 的 Mapper 类和 XML 等。

相比手动编写,建议使用 MyBatisX 代码生成插件(可在 idea 中装这个插件),可以快速得到这些文件。

选中数据库的表,右键选择 MybatisX 生成器。

image-20250623182519078


按照下图进行配置。

image-20250623192704281


查看生成的代码:生成的代码包括实体类、Mapper、Service 等。

image-20250623192829639


Git 托管:

从当前情况可以看出,生成的文件字体显示为红色,这通常意味着该文件尚未提交到 Git 版本控制系统中。接下来,我们需要先将代码提交到Git中。

image-20250623193208335


Git 回滚:

通过使用 Git,如果我们在后续开发过程中发现某些代码不再需要,或者需要撤销某些更改,可以利用 Git 的回滚机制,轻松恢复到之前的代码版本:

image-20250623193413722


展开所有子目录:

image-20250623194952158


移动代码:将生成的代码移动到项目对应的位置,例如:

  • User 移动到 model.entity 包。image-20250623195550988
  • Mapper 移动到 mapper 包。
  • Service 移动到 service 包。image-20250623201747946

为了增加数据爬取的难度,我们可以调整数据库中字段的主键生成策略:

image-20250623201355870

  • 将字段的主键类型从自动递增(IdType.AUTO)更改为手动指定(IdType.ASSIGN_ID):image-20250623195928682

  • 这样的修改,可以使得主键的生成,不再遵循简单的递增规则,从而提高数据抓取的复杂性。


@TableLogic 是 MyBatis 框架中用于标识逻辑删除的注解:

image-20250623200540581

通过使用 @TableLogic 注解:

  • 在指示 MyBatis 在执行插入和更新操作时,会忽略该字段;
  • 在查询操作时,MyBatis 会根据配置自动添加条件,来排除那些被标记为已删除的记录。

这样,开发者就无需在每次查询时手动添加过滤条件,从而简化了代码并提高了开发效率。


如果我们不理解一些 mybatis-plus 的注解,可以仔细阅读官方文档:

image-20250623201256558


修改包名:移动之后,注意修改 UserMapper.xml 等文件的包名,确保路径正确。

image-20250624114446604


(二)数据模型开发


1. 实体类

生成的代码可能无法完全满足需求,例如数据库实体类,我们可以手动更改其字段配置,指定主键策略和逻辑删除:

  • 主键策略:默认是连续生成的,容易被爬虫抓取,因此更换策略为 ASSIGN_ID(雪花算法生成)。
  • 逻辑删除:数据删除时默认为彻底删除记录,如果出现误删,将难以恢复。因此采用逻辑删除,通过修改 isDelete 字段为 1 表示已失效的数据。

image-20250624114711804

@TableName(value = "user")
@Data
public class User implements Serializable {/*** id(要指定主键策略)*/@TableId(type = IdType.ASSIGN_ID)private Long id;// ...(其他字段省略)/*** 是否删除(逻辑删除)*/@TableLogicprivate Integer isDelete;
}

使用框架时的建议:在使用框架的过程中,如果有任何疑问,都可以在官方文档中查阅。例如,了解 MyBatis Plus 的主键生成注解,可以参考以下链接:
MyBatis Plus 注解配置


2. 枚举类

对于用户角色这样值的数量有限的、可枚举的字段,最好定义一个枚举类,便于在项目中获取值、减少枚举值输入错误的情况。

image-20250624114948038


model.enums 包下新建 UserRoleEnum

image-20250624114745497

@Getter
public enum UserRoleEnum {USER("用户", "user"),ADMIN("管理员", "admin");private final String text;private final String value;UserRoleEnum(String text, String value) {this.text = text;this.value = value;}/*** 根据 value 获取对应的枚举实例** @param value 要查找的枚举实例的 value* @return 匹配的枚举实例,如果未找到则返回 null*/public static UserRoleEnum getEnumByValue(String value) {if (ObjUtil.isEmpty(value)) {// 使用 Hutool 工具类 ObjUtil 检查 value 是否为空return null;}for (UserRoleEnum anEnum : UserRoleEnum.values()) {// values() 方法是枚举类型提供的,用于获取枚举类中定义的所有枚举常量if (anEnum.value.equals(value)) {return anEnum;}}return null;}
}

优化建议:如果枚举值特别多,可以使用 Map 缓存所有枚举值来加速查找,而不是遍历列表。

@Getter
public enum UserRoleEnum {USER("用户", "user"),ADMIN("管理员", "admin");private final String text;private final String value;// 定义一个静态的Map来存储枚举值和枚举实例的对应关系private static final Map<String, UserRoleEnum> CACHE = new HashMap<>();// 构造函数UserRoleEnum(String text, String value) {this.text = text;this.value = value;// 在构造函数中将当前枚举实例添加到CACHE中CACHE.put(this.value, this);// way1}// 将枚举元素放入哈希表,有两个方法: way1, way2 , 二选一即可// way2// 静态初始化块,在类加载时执行static {// 遍历UserRoleEnum的所有实例for (UserRoleEnum role : UserRoleEnum.values()) {// 将每个实例的value作为键,实例本身作为值,存入CACHE Map中CACHE.put(role.value, role);}}/*** 根据 value 获取对应的枚举实例** @param value 要查找的枚举实例的 value* @return 匹配的枚举实例,如果未找到则返回 null*/public static UserRoleEnum getEnumByValue(String value) {if (ObjUtil.isEmpty(value)) {// 使用 Hutool 工具类 ObjUtil 检查 value 是否为空return null;}// 从CACHE Map中获取枚举实例return CACHE.get(value);}
}

我们暂时先继续使用列表获取枚举类;


各功能接口的开发


用户注册


1. 数据模型


model.dto.user 包下新建用于接收请求参数的类 UserRegisterRequest

image-20250624120337471

/*** 用于用户注册的实体类*/@Data
public class UserRegisterRequest implements Serializable {private static final long serialVersionUID = 6055996074332767695L;/*** 账号*/private String userAccount;/*** 密码*/private String userPassword;/*** 确认密码*/private String checkPassword;
}6055996074332767695L

  1. 序列化的目的:

    • 在Java中,实现Serializable接口允许对象转换为字节序列。
    • 这种转换使得对象可以被写入文件通过网络发送到另一台机器上,从而实现对象的持久化和网络传输
  2. 实现Serializable接口:

    • 实现 Serializable 接口,是启用对象序列化的简单方法。
    • 一旦对象实现了Serializable接口,就可以使用 Java 的序列化机制来转换对象状态
  3. 序列化ID(serialVersionUID):

    • 实现Serializable接口后,通常需要为类提供一个唯一的序列化 ID

    • 安装插件GenerateSerialversionUID,使用该插件为类生成唯一序列化 ID

      image-20250624122505452

    • 这个ID用于验证序列化对象的版本兼容性,确保在反序列化过程中,发送方和接收方的类版本是兼容的

  4. 序列化ID的更新:

    • 如果类的实现发生了变化,如添加或删除字段,应该更新序列化ID
    • 更新序列化 ID ,可以防止因类结构变化而导致的反序列化错误

开发建议:在 Java 接口开发中,为每个接口定义一个专门的类,来接收请求参数,可以提高代码的可读性和维护性,便于对参数进行统一验证和扩展,同时减少接口方法参数过多导致的复杂性,有助于在复杂场景下更清晰地管理和传递数据。


2. 服务开发


service 包的 UserService 中增加方法声明:

image-20250624123123897

public interface UserService extends IService<User> {/*** 用户注册** @param userAccount   用户账户* @param userPassword  用户密码* @param checkPassword 校验密码* @return 新用户 id*/long userRegister(String userAccount, String userPassword, String checkPassword);
}

UserServiceImpl 中增加实现代码,注意补充一些校验条件:

image-20250624123428923

@Service// (8) 在 MyBatis-Plus 框架中,ServiceImpl 类为服务层提供了基础实现,它包含了对数据库进行 CRUD(创建、读取、更新、删除)操作的方法。
public class UserServiceImpl extends ServiceImpl<UserMapper, User>implements UserService {@Overridepublic long userRegister(String userAccount, String userPassword, String checkPassword) {// (5) 先写步骤 1、2、3、4, 后续可以利用 AI 根据注释生成相应代码// (1) 校验参数if(StrUtil.hasBlank(userAccount, userPassword, checkPassword)){// (6) StrUtil.hasBlank 校验传入的参数是否为空, 抛出自定义异常throw new BusinessException(ErrorCode.PARAMS_ERROR, "参数为空");}if (userAccount.length() < 4) {throw new BusinessException(ErrorCode.PARAMS_ERROR, "用户账号过短");}if (userPassword.length() < 8 || checkPassword.length() < 8) {throw new BusinessException(ErrorCode.PARAMS_ERROR, "用户密码过短");}if (!userPassword.equals(checkPassword)) {throw new BusinessException(ErrorCode.PARAMS_ERROR, "两次输入的密码不一致");}// (2) 检查用户账号是否存于数据库中// (7) 构造查询条件QueryWrapper<User> queryWrapper = new QueryWrapper<>();queryWrapper.eq("userAccount",userAccount);// (9) 校验当前账户是否是未注册账户Long count = this.baseMapper.selectCount(queryWrapper);// this 指的是 UserServiceImpl 的一个对象// baseMapper 是 ServiceImpl 类中的一个属性,它是一个泛型参数,通常对应于与实体类相关的 Mapper 接口// 调用 selectCount 方法时,它的作用是执行一个数据库查询,计算与提供的查询条件相匹配的记录数量// (10) 当前是注册账户, 所以 count > 0 表示该用户已存在, 注册失败if(count > 0){throw new BusinessException(ErrorCode.PARAMS_ERROR, "账号重复");}// (3) 密码使用加盐加密String encryptPassword = getEncryptPassword(userPassword);// (4) 插入数据到数据库中User user = new User();user.setUserAccount(userAccount);user.setUserPassword(encryptPassword);user.setUserName("无名");user.setUserRole(UserRoleEnum.USER.getValue());// (14) 保存用户信息到数据库中boolean saveResult = this.save(user);// save() 的作用是将 user 对象保存到数据库中, 是 ServiceImpl 接口内置的方法// save() 返回一个 boolean 类型的结果,表示保存操作是否成功// (15) 保存失败, 说明是系统内部的问题if(!saveResult){throw new BusinessException(ErrorCode.SYSTEM_ERROR, "注册失败, 数据库错误");}// (16)// this.save(user) , mybatis 框架利用主键回填// 在调用 save() 时, 自动生成 user 的主键 id, 因此在这里可以直接获取 idreturn user.getId();}/*** (11) 获取加密后的密码* @param userPassword 传入用户输入的密码* @return 经过加密算法加密后的密码*/@Overridepublic String getEncryptPassword(String userPassword){// (12) 加盐, 混淆密码final String SALT = "yupi";// (13) 返回加密后的结果return DigestUtils.md5DigestAsHex((SALT + userPassword).getBytes());// (盐值 + 密码) 的字符串拼接结果, 转为字节数组// 再利用 spring 内置的加密工具类, 对字节数组使用 md5 算法进行单向加密}
}

3. 接口开发


controller 包中新建 UserController,新增用户注册接口:

image-20250624132649436

@RestController
// @RestController 会把所有接口的返回结果转为 JSON 格式@RequestMapping("/user")
public class UserController {// (4) 注入 service 对象@Resourceprivate UserService userService;@PostMapping("/register")public BaseResponse<Long> userRegister(@RequestBody UserRegisterRequest userRegisterRequest){// (1) 使用 @RequestBody 注解,后端能够接收前端以 JSON 格式, 发送的对象参数// (2) 传入对象为空, 使用自定义工具类抛异常ThrowUtils.throwIf(userRegisterRequest == null, ErrorCode.PARAMS_ERROR);// (3) 安装插件 Generate All Getter And Setter, 下面三行代码只需要输入 userRegisterRequest.allgetString userAccount = userRegisterRequest.getUserAccount();String userPassword = userRegisterRequest.getUserPassword();String checkPassword = userRegisterRequest.getCheckPassword();// (5) 封装返回结果long result = userService.userRegister(userAccount, userPassword, checkPassword);return ResultUtils.success(result);}
}

4. 测试接口


每开发完一个接口,都可以使用 Swagger 接口文档 来测试,测试可以采取 Debug 模式,同样可以满足测试接口功能:

image-20250624142845320


💡 测试注意事项:测试时要尤其注意边界值和特殊值,不能只测试正常的情况,如:

image-20250624151445303


用户登录


1. 数据模型


model.dto 包下新建用于接收请求参数的类 UserLoginRequest

image-20250624151845969

@Data
public class UserLoginRequest implements Serializable {private static final long serialVersionUID = 4110612674161298294L;/*** 账号*/private String userAccount;/*** 密码*/private String userPassword;
}

model.vo 包下新建用于返回响应参数的类 LoginUserVO,表示脱敏后的数据:

  1. 复制 model.entity包下的 User 类,粘贴到 model.vo
  2. 重构UserLoginUserVO
  3. 删除不必要的字段(如 Password, isdelete)
  4. 删除注解(返回给前端的数据不需要这些注解)

image-20250624153504626

/*** 已登录用户视图脱敏后的数据*/@Data
public class LoginUserVO implements Serializable {/*** id*/private Long id;/*** 账号*/private String userAccount;/*** 用户昵称*/private String userName;/*** 用户头像*/private String userAvatar;/*** 用户简介*/private String userProfile;/*** 用户角色:user/admin*/private String userRole;/*** 编辑时间*/private Date editTime;/*** 创建时间*/private Date createTime;/*** 更新时间*/private Date updateTime;private static final long serialVersionUID = 1L;
}

2. 服务开发


service 包的 UserService 中增加方法声明:

image-20250624153056258

/*** 用户登录** @param userAccount  用户账户* @param userPassword 用户密码* @param request      HTTP 请求对象* @return 脱敏后的用户信息*/
LoginUserVO userLogin(String userAccount, String userPassword, HttpServletRequest request);

image-20250624153032041


UserServiceImpl 中增加实现代码:

image-20250624160530937


注意补充一些校验条件。在用户登录成功后,将用户信息存储在当前的 Session 中。代码如下:

/*** 用户登录* @param userAccount  用户账户* @param userPassword 用户密码* @param request      HTTP 请求对象* @return 脱敏后的用户信息*/
@Override
public LoginUserVO userLogin(String userAccount, String userPassword, HttpServletRequest request) {// 1. 校验if (StrUtil.hasBlank(userAccount, userPassword)) {throw new BusinessException(ErrorCode.PARAMS_ERROR, "参数为空");}if (userAccount.length() < 4) {throw new BusinessException(ErrorCode.PARAMS_ERROR, "账号错误");}if (userPassword.length() < 8) {throw new BusinessException(ErrorCode.PARAMS_ERROR, "密码错误");}// 2. 对用户传递的秘密进行加密String encryptPassword = getEncryptPassword(userPassword);// 3. 查询用户是否存在于数据库QueryWrapper<User> queryWrapper = new QueryWrapper<>();queryWrapper.eq("userAccount", userAccount);queryWrapper.eq("userPassword",encryptPassword); // 用加密密码比较// 5. selectOne() 保证查出来的结果只有一条数据User user = this.baseMapper.selectOne(queryWrapper);// 注意, 我们的数据库表对 userAccount 字段设置为唯一索引,// 根据上面的构造条件, 只会查询出一条结果, 所以不需要使用事务// 6. 查询结果为空, 表示账户不存在if(user == null){log.info("user login failed, userAccount cannot match userPassword");throw new BusinessException(ErrorCode.PARAMS_ERROR, "用户不存在或者密码错误");}// 4. 保留用户的登录态request.getSession().setAttribute("USER_LOGIN_STATE", user);// "USER_LOGIN_STATE" 是写死的, 会造成硬编码, 后续需要拓展常量 constant// 7. 将查询的 user 转为 LoginUserVOreturn this.getLoginUserVO(user);
}/*** 8. 获取脱敏后的登录用户信息* @param user 查询数据库得到的结果* @return 对查询结果脱敏后, 得到的结果*/
@Override
public LoginUserVO getLoginUserVO(User user) {if(user == null){return null;}LoginUserVO loginUserVO = new LoginUserVO();BeanUtil.copyProperties(user,loginUserVO);// hutool 工具类, 拷贝源对象的属性, 对给目标对象已有且对应的属性进行赋值return loginUserVO;
}

注意事项:

  • 由于注册用户时存入数据库的密码是加密后的,查询用户信息时,也要对用户输入的密码进行同样算法的加密,才能与数据库中的信息对应上。
  • 可以将上述的 Session 理解为一个 Map,可以给 Map 设置 keyvalue。每个不同的 SessionID 对应的 Session 存储都是不同的,不用担心会污染。
  • 所以上述代码中,给 Session 设置了固定的 keyUSER_LOGIN_STATE),可以将这个 key 值提取为常量,便于后续获取。

constant 包下新建 UserConstant 接口类,统一声明用户相关的常量:

image-20250624163835234

UserConstant 设计为接口类是合适的,因为接口中的字段默认是 public static final 的,这意味着它们是常量,且只能被赋值一次。这种特性使得接口成为定义常量的理想选择。

/*** 用户常量*/
public interface UserConstant {/*** 用户登录态键*/String USER_LOGIN_STATE = "user_login";// region 权限/*** 默认角色*/String DEFAULT_ROLE = "user";/*** 管理员角色*/String ADMIN_ROLE = "admin";// endregion
}

修改登录态写死的部分:

image-20250624163801482


3. 接口开发


UserController 中新增用户登录接口:

@PostMapping("/login")
public BaseResponse<LoginUserVO> userLogin(@RequestBody UserLoginRequest userLoginRequest, HttpServletRequest request) {// 1 登录接口的参数, 比校验接口多了一个 HttpServletRequest requestThrowUtils.throwIf(userLoginRequest == null, ErrorCode.PARAMS_ERROR);String userAccount = userLoginRequest.getUserAccount();String userPassword = userLoginRequest.getUserPassword();LoginUserVO loginUserVO = userService.userLogin(userAccount, userPassword, request);return ResultUtils.success(loginUserVO);
}

4. 测试接口


重新运行程序,测试接口:

image-20250624164805016


image-20250624164840977


获取当前登录用户


可以从 request 请求对象对应的 Session 中直接获取到之前保存的登录用户信息,无需其他请求参数。


1. 服务开发


service 包的 UserService 中增加方法声明:

image-20250624173107134

/*** 获取当前登录用户** @param request HTTP 请求对象* @return 用户信息*/
User getLoginUser(HttpServletRequest request);

UserServiceImpl 中增加实现代码:

image-20250624173119366

此处为了保证获取到的数据始终是最新的,先从 Session 中获取登录用户的 id,然后从数据库中查询最新的结果。代码如下:

@Override
public User getLoginUser(HttpServletRequest request) {// 1. 先判断是否已经登录Object userObj = request.getSession().getAttribute(UserConstant.USER_LOGIN_STATE);// 2. 根据 Sesssion 获取对象后, 先把 Object 转为 UserUser currentUser = (User) userObj;// 3. 根据对象判断当前是否登录if(currentUser == null || currentUser.getId() == null){// 4. 多判断一个 id 是否为空, 后续前端需要根据 id 来判断用户是否登录throw new BusinessException(ErrorCode.NOT_LOGIN_ERROR);}// 5. 从数据库中查询信息, 如果追求性能, 直接返回 currentUser 对象(利用缓存, 不推荐)Long userId = currentUser.getId();// 6. 根据 id 从数据库中查询, 查询的结果重新赋值给 currentUsercurrentUser = this.getById(userId);// 7. 再次校验, 当前用户信息在数据库中查不到, 可能该用户被管理员删除if(currentUser == null){throw new BusinessException(ErrorCode.NOT_LOGIN_ERROR);}// 8. 返回数据库查询结果return currentUser;
}

2. 接口开发


UserController 中新增获取当前登录用户接口:

image-20250624173144586

// 获取当前登录用户信息@GetMapping("/get/login")
public BaseResponse<LoginUserVO> getLoginUser(HttpServletRequest request) {// 1. 调用 service , 获取用户信息User loginUser = userService.getLoginUser(request);// 2. Controller 接口可以被前端直接调用, 因此要对 Service 拿到的信息进行脱敏LoginUserVO loginUserVO = userService.getLoginUserVO(loginUser);// 3. 统一返回结果, 封装脱敏后的数据, 返回给前端return ResultUtils.success(loginUserVO);
}

注意事项:上述代码直接将数据库查到的所有信息都返回给了前端(包括密码),可能存在信息泄露的安全风险。因此,需要对返回结果进行脱敏处理。


用户注销


用户注销可以从 request 请求对象对应的 Session 中直接获取到之前保存的登录用户信息,来完成注销,无需其他请求参数。


1. 服务开发


service 包的 UserService 中增加方法声明:

image-20250624173107134

/*** 用户注销** @param request HTTP 请求对象* @return 是否注销成功*/
boolean userLogout(HttpServletRequest request);

UserServiceImpl 中增加实现代码,从 Session 中移除掉当前用户的登录态即可:

image-20250624173119366

@Override
public boolean userLogout(HttpServletRequest request) {// 先判断是否已登录Object userObj = request.getSession().getAttribute(USER_LOGIN_STATE);if (userObj == null) {throw new BusinessException(ErrorCode.OPERATION_ERROR, "未登录");}// 移除登录态request.getSession().removeAttribute(USER_LOGIN_STATE);return true;
}

2. 接口开发


UserController 中新增用户注销接口:

image-20250624173144586

@PostMapping("/logout")
public BaseResponse<Boolean> userLogout(HttpServletRequest request) {ThrowUtils.throwIf(request == null, ErrorCode.PARAMS_ERROR);boolean result = userService.userLogout(request);return ResultUtils.success(result);
}

用户权限控制


在本节教程的方案设计中提到:权限校验是一个比较通用的业务需求,通常通过 Spring AOP 切面 + 自定义权限校验注解 实现统一的接口拦截和权限校验。

如果有特殊的权限校验逻辑,再单独在接口中编码。


1. 权限校验注解


首先编写权限校验注解,将其放在 annotation 包下:

image-20250624173953812

下面的代码定义了一个名为 AuthCheck 的自定义注解,用于在运行时进行权限检查:

@Target(ElementType.METHOD)          // 注解的生效范围: 定义为方法注解
@Retention(RetentionPolicy.RUNTIME)  // 注解的生效时机: 运行时生效
public @interface AuthCheck {/*** 必须有某个角色, 也就是说, */String mustRole() default "";
}

注解 AuthCheck 包含一个元素 mustRole()

  • mustRole() 一个字符串类型的元素,用于指定必须拥有的角色
  • 如果该元素不指定值(即默认值),则默认为空字符串 ""

使用这个注解的方法,需要确保调用者具有指定的角色,否则可能会拒绝访问执行相应的权限检查逻辑。这个注解可以用于实现基于角色的访问控制(RBAC)


2. 权限校验切面


编写权限校验 AOP,采用环绕通知,在 打上该注解的方法 执行前后进行一些额外的操作,比如校验权限:

image-20250624194319719


代码如下,放在 aop 包下:

image-20250624184537786

@Aspect     // 表示该类是一个切面类,用于定义横切关注点
@Component  // 将该类声明为 Spring 组件,使其成为 Spring 应用上下文中的一个 Bean
public class AuthInterceptor {@Resourceprivate UserService userService;/*** 执行权限拦截* @param joinPoint 切点参数,用于获取被拦截方法的信息* @param authCheck 自定义的权限注解,用于指定权限要求* @return 拦截方法的执行结果* @throws Throwable 可能抛出的异常*/@Around("@annotation(authCheck)")  // 定义切点,指定只有被 @AuthCheck 注解标记的方法才会被拦截public Object doInterceptor(ProceedingJoinPoint joinPoint, AuthCheck authCheck) throws Throwable {// 1. 获取必须具备的角色权限String mustRole = authCheck.mustRole();// 2. 从全局请求上下文中获取当前请求RequestAttributes requestAttributes = RequestContextHolder.currentRequestAttributes();// 根据获取全局请求上下文 RequestContextHolder, 获取当前请求的各个属性 currentRequestAttributes()// 3. 从请求上下文中获取 HttpServletRequest 对象HttpServletRequest request = ((ServletRequestAttributes) requestAttributes).getRequest();// 需要把 RequestAttributes 类型的对象, 转为子类 ServletRequestAttributes 类型的对象, 才可以调用 getRequest()// 4. 根据请求对象获取当前登录用户的详细信息User loginUser = userService.getLoginUser(request);// 5. 将角色权限字符串转换为枚举类型UserRoleEnum mustRoleEnum = UserRoleEnum.getEnumByValue(mustRole);// 6. 如果mustRoleEnum 为空,表示没有指定必须具备的角色权限,直接放行if(mustRoleEnum == null){return joinPoint.proceed();}// 7. 获取当前登录用户的角色权限(会员权限、管理员权限等特殊权限)UserRoleEnum userRoleEnum = UserRoleEnum.getEnumByValue(loginUser.getUserRole());// 8. 如果用户角色权限为空,表示用户没有权限,抛出异常if (userRoleEnum == null) {throw new BusinessException(ErrorCode.NO_AUTH_ERROR);}// 9. 如果必须具备管理员权限,但当前登录用户不是管理员,则抛出异常if(UserRoleEnum.ADMIN.equals(mustRoleEnum) && !UserRoleEnum.ADMIN.equals(userRoleEnum)){throw new BusinessException(ErrorCode.NO_AUTH_ERROR);}// 10. 如果当前用户是管理员,放行执行原方法return joinPoint.proceed();}
}

3. 使用注解


只要给方法添加了 @AuthCheck 注解,就必须要登录,否则会抛出异常。可以设置 mustRole 为管理员,这样仅管理员才能使用该接口:

@AuthCheck(mustRole = UserConstant.ADMIN_ROLE)

对于不需要登录就能使用的接口,不需要使用该注解。


4. 注解测试


我们测试以下权限校验切面的功能,在注册接口设置管理员权限的注解:

image-20250624194532322


重新启动程序,当前用户不是管理员,如果调用注册接口,会产生如下效果:

image-20250624194805563


我们调用登录接口登录,再调用注册接口:

image-20250624194926914


修改后端用户权限:

image-20250624195029549


重新运行程序,再次调用注册接口,继续测试权限校验切面的功能:

image-20250624195251674

账户重复,表示程序已经进入到切点内了,也就表示权限校验注解已经开发完成了;


注册接口在当前用户的 userRole = admin 的情况下,可以正常访问:

image-20250624195411473


用户管理


用户管理功能具体可以拆分为以下几部分:

  • 管理员操作

    • 创建用户

    • 根据 id 删除用户

    • 更新用户

    • 分页获取用户列表(需要脱敏)

    • 根据 id 获取用户(未脱敏)

  • 普通用户操作

    • 根据 id 获取用户(脱敏)

1. 数据模型


每个操作都需要提供一个请求类,都放在 dto.user 包下;用户管理功能,本质上就是对用户信息进行增删改查:

image-20250625124137748


用户删除请求

/*** 通用的删除请求类(删除操作都是根据用户 id 进行删除, 所以放在 common 进行复用)*/
@Data
public class DeleteRequest implements Serializable {/*** 数据 ID*/private Long id;private static final long serialVersionUID = 1L;
}

用户创建请求

@Data
public class UserAddRequest implements Serializable {/*** 用户昵称*/private String userName;/*** 账号*/private String userAccount;/*** 用户头像*/private String userAvatar;/*** 用户简介*/private String userProfile;/*** 用户角色: user, admin*/private String userRole;private static final long serialVersionUID = 1L;
}

用户更新请求

@Data
public class UserUpdateRequest implements Serializable {/*** id*/private Long id;/*** 用户昵称*/private String userName;/*** 用户头像*/private String userAvatar;/*** 简介*/private String userProfile;/*** 用户角色:user/admin*/private String userRole;private static final long serialVersionUID = 1L;
}

用户查询请求

需要继承公共包中的 PageRequest 来支持分页查询:

@EqualsAndHashCode(callSuper = true)
@Data
public class UserQueryRequest extends PageRequest implements Serializable {/*** id*/private Long id;/*** 用户昵称*/private String userName;/*** 账号*/private String userAccount;/*** 简介*/private String userProfile;/*** 用户角色:user/admin/ban*/private String userRole;private static final long serialVersionUID = 1L;
}@Data
public class PageRequest {/*** 当前页号,默认值为 1*/private int current = 1;/*** 页面大小,默认值为 10*/private int pageSize = 10;/*** 排序字段*/private String sortField;/*** 排序顺序(默认降序)*/private String sortOrder = "descend";
}

用户信息脱敏

由于要提供获取用户信息的接口,需要和获取当前登录用户的接口一样,对用户信息进行脱敏

model.vo 包下新建 UserVO,表示脱敏后的用户信息:

image-20250625124613198

@Data
public class UserVO implements Serializable {/*** id*/private Long id;/*** 账号*/private String userAccount;/*** 用户昵称*/private String userName;/*** 用户头像*/private String userAvatar;/*** 用户简介*/private String userProfile;/*** 用户角色:user/admin*/private String userRole;/*** 创建时间*/private Date createTime;private static final long serialVersionUID = 1L;
}

2. 服务开发


(1)获取脱敏后的单个用户信息

UserService 中编写获取脱敏后的单个用户信息方法:

image-20250625131309697

/*** 获取脱敏后的登录用户信息(普通用户、管理员)** @param user 查询数据库得到的结果* @return 对查询结果脱敏后, 得到的结果*/
UserVO getUserVO(User user);

image-20250625131840249

@Override
public UserVO getUserVO(User user) {if (user == null) {return null;}UserVO userVO = new UserVO();BeanUtils.copyProperties(user, userVO);return userVO;
}

(2)获取脱敏后的用户列表

UserService 中编写获取脱敏后的用户列表方法:

image-20250625131857719

/*** 获取脱敏后的登录用户信息列表(管理员)** @param userList 查询数据库, 得到的结果列表* @return 对查询结果脱敏后, 得到的结果列表*/
List<User> getUserVOList(List<User> userList);

image-20250625131848334

@Override
public List<UserVO> getUserVOList(List<User> userList) {if(CollUtil.isEmpty(userList)){// CollUtil 是 Hutool 关于集合操作的工具类return new ArrayList<>();// 传入的 userList 为空, 脱敏结果也是一个空数组}return userList.stream().map(this::getUserVO).collect(Collectors.toList());
}

image-20250625134209466


(3)获取查询条件

除了上述方法外,对于分页查询接口,需要根据用户传入的参数来构造 SQL 查询。

由于使用了 MyBatis Plus 框架,无需手动拼接 SQL,而是通过构造 QueryWrapper 对象来生成 SQL 查询。


可以在 UserService 中编写一个方法,专门用于将查询请求转换为 QueryWrapper 对象:

image-20250625135045242

/*** 获取查询条件* @param userQueryRequest* @return*/
QueryWrapper<User> getQueryWrapper(UserQueryRequest userQueryRequest);

实现获取查询条件的接口方法:

image-20250625135220877

@Override
public QueryWrapper<User> getQueryWrapper(UserQueryRequest userQueryRequest) {if (userQueryRequest == null) {throw new BusinessException(ErrorCode.PARAMS_ERROR, "请求参数为空");}// 1. userQueryRequest.allgetLong id = userQueryRequest.getId();String userName = userQueryRequest.getUserName();String userAccount = userQueryRequest.getUserAccount();String userProfile = userQueryRequest.getUserProfile();String userRole = userQueryRequest.getUserRole();// 2. 下面两个字段展示用不到// int current = userQueryRequest.getCurrent();// int pageSize = userQueryRequest.getPageSize();String sortField = userQueryRequest.getSortField();String sortOrder = userQueryRequest.getSortOrder();// 3. 将获取到的值作为为请求参数来构造条件QueryWrapper<User> queryWrapper = new QueryWrapper<>();// 4. 传入参数中, 获取到的 id 不为空, 根据数据库 id 字段, 查找传入的 id 值queryWrapper.eq(ObjUtil.isNotEmpty(id), "id", id);queryWrapper.eq(StrUtil.isNotBlank(userRole), "userRole", userRole);// 5. 用到模糊查询 like, 只要账号, 用户名....等字段内容部分相同, 就可以匹配出符合条件的结果queryWrapper.like(StrUtil.isNotBlank(userAccount), "userAccount", userAccount);queryWrapper.like(StrUtil.isNotBlank(userName), "userName", userName);queryWrapper.like(StrUtil.isNotBlank(userProfile), "userProfile", userProfile);// 6. 利用排序, 如果传入参数中, 获取到的排序规则参数不为空, 就按照升序排序 ascend 排序查找到的数据, 降序规则: descendqueryWrapper.orderBy(StrUtil.isNotEmpty(sortField), sortOrder.equals("ascend"), sortField);return queryWrapper;
}

补充说明,光标放在方法上,按ctrl + p 可以显示方法的参数:

image-20250625140515217

orderBy() 的第二个参数,和我们自己传入的参数sortOrder.equals("ascend") 是呼应的;


3. 接口开发


上述功能主要是常见的 CRUD(增删改查) 操作,代码实现相对简单。需要注意以下几点:

  1. 添加对应的权限注解。
  2. 做好参数校验。

image-20250625142823137


(1)创建用户

/*** 创建用户* @param userAddRequest 创建用户请求* @return 创建好用户后, 在数据库中存的该用户的 id*/@PostMapping("/add")// 6. 该接口需要管理员权限
@AuthCheck(mustRole = UserConstant.ADMIN_ROLE)
public BaseResponse<Long> addUser(@RequestBody UserAddRequest userAddRequest) {// 1. 传入参数为空, 调用自定义工具类, 抛出参数错误异常ThrowUtils.throwIf(userAddRequest == null, ErrorCode.PARAMS_ERROR);// 2. 把传入的参数赋值给 User 对象User user = new User();BeanUtil.copyProperties(userAddRequest,user);// 3. 给 User 对象设置密码默认值final String DEFAULT_PASSWORD = "12345678";String encryptPassword = userService.getEncryptPassword(DEFAULT_PASSWORD); // 对默认密码加密user.setUserPassword(encryptPassword);// 4. 插入数据库boolean result = userService.save(user);ThrowUtils.throwIf(result == false, ErrorCode.OPERATION_ERROR);  // 操作异常// 5. 统一返回结果 BaseResponse<Long>: user.getId() 对应 Long 类型  return ResultUtils.success(user.getId());
}

(2)根据 id 获取用户(仅管理员)

@GetMapping("/get")
@AuthCheck(mustRole = UserConstant.ADMIN_ROLE)
public BaseResponse<User> getUserById(long id) {ThrowUtils.throwIf(id <= 0, ErrorCode.PARAMS_ERROR);User user = userService.getById(id);ThrowUtils.throwIf(user == null, ErrorCode.NOT_FOUND_ERROR);return ResultUtils.success(user);
}

(3)根据 id 获取包装类

@GetMapping("/get/vo")
public BaseResponse<UserVO> getUserVOById(long id) {BaseResponse<User> response = getUserById(id);User user = response.getData();return ResultUtils.success(userService.getUserVO(user));
}

(4)删除用户

/*** 根据 id 删除用户, 仅管理员* @param deleteRequest* @return*/
@PostMapping("/delete")
@AuthCheck(mustRole = UserConstant.ADMIN_ROLE)
public BaseResponse<Boolean> deleteUser(@RequestBody DeleteRequest deleteRequest) {if (deleteRequest == null || deleteRequest.getId() <= 0) {throw new BusinessException(ErrorCode.PARAMS_ERROR);}boolean b = userService.removeById(deleteRequest.getId());return ResultUtils.success(b);
}

(5)更新用户

类比创建用户接口:

  1. 参数校验

    • 首先,对输入的请求参数进行校验。
  2. 创建用户对象

    • 根据校验通过的请求参数,创建一个新的 User 对象,并复制相关的属性。
  3. 更新数据库

    • 使用创建的 User 对象更新数据库中的相应记录。
  4. 处理更新结果

    • 根据数据库更新操作的结果,采取相应的措施:
      • 如果更新失败(例如,由于违反约束或技术问题),则抛出自定义异常
      • 如果更新成功,则封装结果并返回给调用者。
@PostMapping("/update")
@AuthCheck(mustRole = UserConstant.ADMIN_ROLE)
public BaseResponse<Boolean> updateUser(@RequestBody UserUpdateRequest userUpdateRequest) {if (userUpdateRequest == null || userUpdateRequest.getId() == null) {throw new BusinessException(ErrorCode.PARAMS_ERROR);}User user = new User();BeanUtils.copyProperties(userUpdateRequest, user);boolean result = userService.updateById(user);ThrowUtils.throwIf(!result, ErrorCode.OPERATION_ERROR);return ResultUtils.success(true);
}

(6)分页获取用户封装列表(仅管理员)

注意事项

  1. 因为分页查询涉及到复杂的查询,参数会比较复杂,POST GET 能发送的数据量更大
  2. BaseResponse<Page<UserVO>>中,要注意引入 Page 的包:image-20250625144809796
@PostMapping("/list/page/vo")
@AuthCheck(mustRole = UserConstant.ADMIN_ROLE)
public BaseResponse<Page<UserVO>> listUserVOByPage(@RequestBody UserQueryRequest userQueryRequest){// 1. 校验参数ThrowUtils.throwIf(userQueryRequest == null, ErrorCode.PARAMS_ERROR);// 2. 从参数中获取当前页码、每页显示的记录树int current = userQueryRequest.getCurrent();    // 当前页码int pageSize = userQueryRequest.getPageSize();  // 每页显示的记录数// 3. 构建分页查询条件并执行查询,获取分页结果Page<User> userPage = userService.page(new Page<>(current, pageSize), // 创建分页对象,包含当前页码和页面大小userService.getQueryWrapper(userQueryRequest) // 根据请求参数构造查询条件);// page(Page<current, pageSize> 对象, 查询条件) 是 MyBatis-Plus 提供的一个方法, 用于执行分页查询// 4. 根据当前页码、页面大小以及从数据库查询得到的总记录数, 创建一个新的 Page<UserVO> 分页对象, 用于后续存储转换后的 UserVO 列表Page<UserVO> userVOPage = new Page<>(current, pageSize, userPage.getTotal());// userPage.getTotal() 表示从数据库查询得到的总记录数;// 这里从 userPage(一个 Page<User> 对象)获取总记录数, 因为 userPage 已经包含了这次查询的分页信息// 5. 对分页结果进行脱敏List<UserVO> userVOList = userService.getUserVOList(userPage.getRecords());//  getRecords() 方法的作用是从一个分页对象 userPage 中提取当前页的记录列表// 6. 将 userVOList 列表, 设置为 userVOPage 分页对象中的当前页记录userVOPage.setRecords(userVOList);// setRecords() 用于设置分页对象中, 当前页的数据记录列表return ResultUtils.success(userVOPage);
}

4.分页功能修复


使用 Swagger 接口文档 依次对上述接口进行测试,发现 listUserVOByPage 接口有一些问题!

image-20250625152555445


发送请求:

image-20250625152715782


分页好像没有生效,还是查出了全部数据。

image-20250625152842431

由于我们用的是 MyBatis Plus 来操作数据库,所以需要通过 官方文档 来查询解决方案。


查阅后发现,原来必须要配置一个分页插件。必须要注意,本项目使用的 v3.5.9 版本引入分页插件的方式和之前不同!v3.5.9 版本后需要独立安装分页插件依赖!!!

image-20250625153005552


pom.xml 中引入分页插件依赖:

<!-- MyBatis Plus 分页插件 -->
<dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-jsqlparser-4.9</artifactId>
</dependency>

image-20250625153844887

注意,这个插件适用于 JDK8,如果是其他版本的 JDK ,需要去官方文档中引入其他版本的分页插件;

光引入这一条,大概率是无法成功下载依赖的;


pom.xml 的依赖管理配置中补充 mybatis-plus-bom

<dependencyManagement><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-dependencies</artifactId><version>${spring-boot.version}</version><type>pom</type><scope>import</scope></dependency><dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-bom</artifactId><version>3.5.9</version><type>pom</type><scope>import</scope></dependency></dependencies>
</dependencyManagement>

image-20250625153615692


依赖下载成功后,根据官方文档的提示,在 config 包下新建 MyBatis Plus 拦截器配置,添加分页插件:

image-20250625154153227

@Configuration
@MapperScan("com.yupi.yupiturebackend.mapper")  
// 注意: 扫描路径需要复制自己项目中 mapper 对应的包
public class MyBatisPlusConfig {/*** 拦截器配置** @return {@link MybatisPlusInterceptor}*/@Beanpublic MybatisPlusInterceptor mybatisPlusInterceptor() {MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();// 分页插件interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));return interceptor;}
}

重启项目,重新调用登录接口(项目使用本地 session,重启需要重新登录),再次调用分页接口,这次就能正常完成分页了:

image-20250625154603575


5. 数据精度修复


但是,在测试中,如果你打开 F12 控制台,利用预览来查看响应数据,就会发现另一个问题:

image-20250625155347955


id 的最后两位好像都变成 0 了!

image-20250625155417253


但是在响应中、以及 Swagger 中查看,却是正常的。

image-20250625155525922

这是由于前端 JS 的精度范围有限,我们后端返回的 id 范围过大,导致前端精度丢失,会影响前端页面获取到的数据结果。


为了解决这个问题,可以在后端 config 包下新建一个 全局 JSON 配置,将整个后端 Spring MVC 接口返回值的长整型数字转换为字符串进行返回,从而集中解决问题。

image-20250625155812605

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import org.springframework.boot.jackson.JsonComponent;
import org.springframework.context.annotation.Bean;
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;/*** Spring MVC Json 配置*/
@JsonComponent
public class JsonConfig {/*** 添加 Long 转 json 精度丢失的配置*/@Beanpublic ObjectMapper jacksonObjectMapper(Jackson2ObjectMapperBuilder builder) {ObjectMapper objectMapper = builder.createXmlMapper(false).build();SimpleModule module = new SimpleModule();module.addSerializer(Long.class, ToStringSerializer.instance);module.addSerializer(Long.TYPE, ToStringSerializer.instance);objectMapper.registerModule(module);return objectMapper;}
}

重启项目进行测试,这次看到的 id 值就正常了。

image-20250625155944328

至此,用户相关的后端接口开发完毕,大家可以按需完善上述代码~~~


在这里插入图片描述

在这里插入图片描述

相关文章:

  • 淘客做网站还是做app五种常用的网站推广方法
  • 古镇网站建设公司网络推广项目计划书
  • 做网站5年工资多少seo站内优化站外优化
  • 静态网站模板中英文谷歌广告联盟
  • 一起合伙做项目的网站广州品牌seo推广
  • 洛阳网站建设 培训长沙正规关键词优化价格从优
  • AR美型SDK,重塑面部美学,开启智能美颜新纪元
  • 定制开发开源AI智能名片与S2B2C商城小程序的内容分发体系构建:基于“1+N“素材复用模型的创新实践
  • 如何在 Manjaro Linux 的图像界面上安装 Stremio 而不是使用命令行
  • 服务器不支持PUT,DELETE 的解决方案
  • C# Avalonia 的 Source Generators 用处
  • ZArchiver×亚矩云手机:云端文件管理的“超维解压”革命
  • 以太网基础②RGMII 与 GMII 转换电路设计
  • Spring AOP 中有多个切面时执行顺序是怎样的?
  • 深度学习的人工神经元
  • Node.js 版本管理工具对比:nvm、n、volta
  • 盘口语言 开盘三分钟知涨跌
  • Git常用操作详解
  • Java-中断流程控制
  • .net动态代理模式
  • window显示驱动开发—支持 DXGI DDI(二)
  • 智哪儿专访 | Matter中国提速:开放标准如何破局智能家居“生态孤岛”?
  • 图像融合中损失函数【4】--复杂图像特征的损失函数
  • 南北差异之——理解业务和理解产品
  • 机器学习-线性模型
  • 强化学习概述