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

Sa-Token 单体架构使用实战

Sa-Token 单体架构使用实战

官网地址:https://sa-token.cc/

在项目开发中,我曾因 SpringSecurity 和 Shiro 的复杂性及文档理解难度而困扰,国产安全框架 Sa-Token 很好地解决了权限认证相关问题,确实为开发工作提供了诸多便利。官方文档已对框架功能和配置进行了详尽说明,若您在使用中遇到配置或概念疑问,建议优先查阅官方文档。此手册主要用于记录 SpringBoot 单体架构下管理后台的实战开发过程,希望能为您提供一些参考。

项目前端代码可通过以下链接查阅:Vue3后台管理系统

数据库表结构设计

在这里插入图片描述

  • 一个用户拥有多个角色
  • 一个角色拥有多个菜单权限
  • 创建用户角色关系表 通过用户id查询到角色列表
  • 创建角色权限关系表 通过角色id查询到权限列表
  • 通过上述关系即可通过用户id查询到权限列表进行权限控制
CREATE DATABASE `management` CHARACTER SET 'utf8mb4' COLLATE 'utf8mb4_0900_ai_ci';
CREATE TABLE `user` (`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键',`username` varchar(255) NOT NULL COMMENT '用户名',`password` varchar(255) NOT NULL COMMENT '密码',`nickname` varchar(255) DEFAULT NULL COMMENT '昵称',`avatar` varchar(255) DEFAULT NULL COMMENT '头像',`create_time` datetime NOT NULL COMMENT '创建时间',`update_time` datetime NOT NULL COMMENT '更新时间',`is_delete` int(1) DEFAULT '0' COMMENT '是否删除 0-否 1-是',PRIMARY KEY (`id`)
) ENGINE=InnoDB CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='用户表';
CREATE TABLE `role` (`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键',`name` varchar(255) DEFAULT NULL COMMENT '角色名',`tag` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '角色标识',`create_time` datetime NOT NULL COMMENT '创建时间',`update_time` datetime NOT NULL COMMENT '更新时间',`is_delete` int(1) DEFAULT '0' COMMENT '是否删除 0-否 1-是',PRIMARY KEY (`id`)
) ENGINE=InnoDB CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='角色表';
CREATE TABLE `menu` (`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键',`name` varchar(255) DEFAULT NULL COMMENT '权限名称',`acl` varchar(255) DEFAULT NULL COMMENT '权限值',`parent_id` int(11) DEFAULT NULL COMMENT '上级id',`level` int(11) DEFAULT NULL COMMENT '层级',`create_time` datetime NOT NULL COMMENT '创建时间',`update_time` datetime NOT NULL COMMENT '更新时间',`is_delete` int(1) DEFAULT '0' COMMENT '是否删除 0-否 1-是',PRIMARY KEY (`id`)
) ENGINE=InnoDB CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='菜单权限表';
CREATE TABLE `user_role_relation` (`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键',`user_id` int(11) DEFAULT NULL COMMENT '用户id',`role_id` int(11) DEFAULT NULL COMMENT '角色id',`create_time` datetime NOT NULL COMMENT '创建时间',`update_time` datetime NOT NULL COMMENT '更新时间',`is_delete` int(1) DEFAULT '0' COMMENT '是否删除 0-否 1-是',PRIMARY KEY (`id`)
) ENGINE=InnoDB CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='用户和角色对应关系表';
CREATE TABLE `menu_role_relation` (`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键',`menu_id` int(11) DEFAULT NULL COMMENT '权限id',`role_id` int(11) DEFAULT NULL COMMENT '角色名称',`create_time` datetime NOT NULL COMMENT '创建时间',`update_time` datetime NOT NULL COMMENT '更新时间',`is_delete` int(1) DEFAULT '0' COMMENT '是否删除 0-否 1-是',PRIMARY KEY (`id`)
) ENGINE=InnoDB CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='菜单权限和角色对应关系表';

数据库初始化脚本

INSERT INTO `management`.`user` (`id`, `username`, `password`, `nickname`, `avatar`, `create_time`, `update_time`, `is_delete`) VALUES (1, 'admin', '8d969eef6ecad3c29a3a629280e686cf0c3f5d5a86aff3ca12020c923adc6c92', '超级管理员', 'http://47.121.182.69:9000/management/avatar1.jpg', '2025-04-02 15:00:44', '2025-06-04 15:45:57', 0);INSERT INTO `management`.`role` (`id`, `name`, `key`, `create_time`, `update_time`, `is_delete`) VALUES (1, '老板', 'Boss', '2025-04-01 16:51:21', '2025-04-02 16:41:16', 0);INSERT INTO `management`.`user_role_relation` (`id`, `user_id`, `role_id`, `create_time`, `update_time`, `is_delete`) VALUES (1, 1, 1, '2025-04-16 15:49:13', '2025-04-16 15:49:13', 0);INSERT INTO `management`.`menu` (`id`, `name`, `acl`, `parent_id`, `level`, `create_time`, `update_time`, `is_delete`) VALUES (1, '全部数据', NULL, 0, 0, '2025-04-03 16:05:13', '2025-04-03 17:09:54', 0);
INSERT INTO `management`.`menu` (`id`, `name`, `acl`, `parent_id`, `level`, `create_time`, `update_time`, `is_delete`) VALUES (2, '权限管理', 'ACL', 1, 1, '2025-04-03 16:08:43', '2025-04-07 11:23:16', 0);
INSERT INTO `management`.`menu` (`id`, `name`, `acl`, `parent_id`, `level`, `create_time`, `update_time`, `is_delete`) VALUES (3, '用户管理', 'User', 2, 2, '2025-04-03 16:09:15', '2025-04-03 16:09:17', 0);
INSERT INTO `management`.`menu` (`id`, `name`, `acl`, `parent_id`, `level`, `create_time`, `update_time`, `is_delete`) VALUES (4, '角色管理', 'Role', 2, 2, '2025-04-03 16:09:15', '2025-04-03 16:09:17', 0);
INSERT INTO `management`.`menu` (`id`, `name`, `acl`, `parent_id`, `level`, `create_time`, `update_time`, `is_delete`) VALUES (5, '菜单管理', 'Menu', 2, 2, '2025-04-03 17:19:11', '2025-04-03 17:19:13', 0);
INSERT INTO `management`.`menu` (`id`, `name`, `acl`, `parent_id`, `level`, `create_time`, `update_time`, `is_delete`) VALUES (6, '添加用户', 'btn.addUser', 3, 3, '2025-04-07 16:26:02', '2025-04-07 16:59:09', 0);
INSERT INTO `management`.`menu` (`id`, `name`, `acl`, `parent_id`, `level`, `create_time`, `update_time`, `is_delete`) VALUES (7, '编辑用户', 'btn.editUser', 3, 3, '2025-04-08 11:24:29', '2025-04-08 11:24:33', 0);
INSERT INTO `management`.`menu` (`id`, `name`, `acl`, `parent_id`, `level`, `create_time`, `update_time`, `is_delete`) VALUES (8, '删除用户', 'btn.deleteUser', 3, 3, '2025-04-08 11:24:29', '2025-04-08 11:24:33', 0);
INSERT INTO `management`.`menu` (`id`, `name`, `acl`, `parent_id`, `level`, `create_time`, `update_time`, `is_delete`) VALUES (9, '批量删除用户', 'btn.batchDeleteUser', 3, 3, '2025-04-08 11:24:29', '2025-04-08 11:24:33', 0);
INSERT INTO `management`.`menu` (`id`, `name`, `acl`, `parent_id`, `level`, `create_time`, `update_time`, `is_delete`) VALUES (10, '分配角色', 'btn.allocateRole', 3, 3, '2025-04-07 16:26:02', '2025-04-07 16:59:09', 0);
INSERT INTO `management`.`menu` (`id`, `name`, `acl`, `parent_id`, `level`, `create_time`, `update_time`, `is_delete`) VALUES (11, '添加角色', 'btn.addRole', 4, 3, '2025-04-08 11:28:30', '2025-04-08 11:28:32', 0);
INSERT INTO `management`.`menu` (`id`, `name`, `acl`, `parent_id`, `level`, `create_time`, `update_time`, `is_delete`) VALUES (12, '编辑角色', 'btn.editRole', 4, 3, '2025-04-08 11:28:30', '2025-04-08 11:28:32', 0);
INSERT INTO `management`.`menu` (`id`, `name`, `acl`, `parent_id`, `level`, `create_time`, `update_time`, `is_delete`) VALUES (13, '删除角色', 'btn.deleteRole', 4, 3, '2025-04-08 11:28:30', '2025-04-08 11:28:32', 0);
INSERT INTO `management`.`menu` (`id`, `name`, `acl`, `parent_id`, `level`, `create_time`, `update_time`, `is_delete`) VALUES (14, '分配权限', 'btn.allocateACL', 4, 3, '2025-04-08 11:28:30', '2025-04-08 11:28:32', 0);
INSERT INTO `management`.`menu` (`id`, `name`, `acl`, `parent_id`, `level`, `create_time`, `update_time`, `is_delete`) VALUES (15, '添加菜单', 'btn.addMenu', 5, 3, '2025-04-08 11:28:30', '2025-04-08 11:28:32', 0);
INSERT INTO `management`.`menu` (`id`, `name`, `acl`, `parent_id`, `level`, `create_time`, `update_time`, `is_delete`) VALUES (16, '编辑菜单', 'btn.editMenu', 5, 3, '2025-04-08 11:28:30', '2025-04-08 11:28:32', 0);
INSERT INTO `management`.`menu` (`id`, `name`, `acl`, `parent_id`, `level`, `create_time`, `update_time`, `is_delete`) VALUES (17, '删除菜单', 'btn.deleteMenu', 5, 3, '2025-04-08 11:28:30', '2025-04-08 11:28:32', 0);
INSERT INTO `management`.`menu` (`id`, `name`, `acl`, `parent_id`, `level`, `create_time`, `update_time`, `is_delete`) VALUES (18, '查询用户', 'btn.queryUser', 3, 3, '2025-04-16 15:55:06', '2025-04-16 15:55:06', 0);
INSERT INTO `management`.`menu` (`id`, `name`, `acl`, `parent_id`, `level`, `create_time`, `update_time`, `is_delete`) VALUES (19, '查询角色', 'btn.queryRole', 4, 3, '2025-04-16 15:56:45', '2025-04-16 15:56:45', 0);
INSERT INTO `management`.`menu` (`id`, `name`, `acl`, `parent_id`, `level`, `create_time`, `update_time`, `is_delete`) VALUES (20, '查询菜单', 'btn.queryMenu', 5, 3, '2025-04-16 15:57:03', '2025-04-16 15:57:03', 0);INSERT INTO `management`.`menu_role_relation` (`id`, `menu_id`, `role_id`, `create_time`, `update_time`, `is_delete`) VALUES (1, 1, 1, '2025-04-17 09:27:45', '2025-04-17 09:27:52', 0);
INSERT INTO `management`.`menu_role_relation` (`id`, `menu_id`, `role_id`, `create_time`, `update_time`, `is_delete`) VALUES (2, 2, 1, '2025-04-17 09:27:45', '2025-04-17 09:27:52', 0);
INSERT INTO `management`.`menu_role_relation` (`id`, `menu_id`, `role_id`, `create_time`, `update_time`, `is_delete`) VALUES (3, 3, 1, '2025-04-17 09:27:45', '2025-04-17 09:27:52', 0);
INSERT INTO `management`.`menu_role_relation` (`id`, `menu_id`, `role_id`, `create_time`, `update_time`, `is_delete`) VALUES (4, 4, 1, '2025-04-17 09:27:45', '2025-04-17 09:27:52', 0);
INSERT INTO `management`.`menu_role_relation` (`id`, `menu_id`, `role_id`, `create_time`, `update_time`, `is_delete`) VALUES (5, 5, 1, '2025-04-17 09:27:45', '2025-04-17 09:27:52', 0);
INSERT INTO `management`.`menu_role_relation` (`id`, `menu_id`, `role_id`, `create_time`, `update_time`, `is_delete`) VALUES (6, 6, 1, '2025-04-17 09:27:45', '2025-04-17 09:27:52', 0);
INSERT INTO `management`.`menu_role_relation` (`id`, `menu_id`, `role_id`, `create_time`, `update_time`, `is_delete`) VALUES (7, 7, 1, '2025-04-17 09:27:45', '2025-04-17 09:27:52', 0);
INSERT INTO `management`.`menu_role_relation` (`id`, `menu_id`, `role_id`, `create_time`, `update_time`, `is_delete`) VALUES (8, 8, 1, '2025-04-17 09:27:45', '2025-04-17 09:27:52', 0);
INSERT INTO `management`.`menu_role_relation` (`id`, `menu_id`, `role_id`, `create_time`, `update_time`, `is_delete`) VALUES (9, 9, 1, '2025-04-17 09:27:45', '2025-04-17 09:27:52', 0);
INSERT INTO `management`.`menu_role_relation` (`id`, `menu_id`, `role_id`, `create_time`, `update_time`, `is_delete`) VALUES (10, 10, 1, '2025-04-17 09:27:45', '2025-04-17 09:27:52', 0);
INSERT INTO `management`.`menu_role_relation` (`id`, `menu_id`, `role_id`, `create_time`, `update_time`, `is_delete`) VALUES (11, 11, 1, '2025-04-17 09:27:45', '2025-04-17 09:27:52', 0);
INSERT INTO `management`.`menu_role_relation` (`id`, `menu_id`, `role_id`, `create_time`, `update_time`, `is_delete`) VALUES (12, 12, 1, '2025-04-17 09:27:45', '2025-04-17 09:27:52', 0);
INSERT INTO `management`.`menu_role_relation` (`id`, `menu_id`, `role_id`, `create_time`, `update_time`, `is_delete`) VALUES (13, 13, 1, '2025-04-17 09:27:45', '2025-04-17 09:27:52', 0);
INSERT INTO `management`.`menu_role_relation` (`id`, `menu_id`, `role_id`, `create_time`, `update_time`, `is_delete`) VALUES (14, 14, 1, '2025-04-17 09:27:45', '2025-04-17 09:27:52', 0);
INSERT INTO `management`.`menu_role_relation` (`id`, `menu_id`, `role_id`, `create_time`, `update_time`, `is_delete`) VALUES (15, 15, 1, '2025-04-17 09:27:45', '2025-04-17 09:27:52', 0);
INSERT INTO `management`.`menu_role_relation` (`id`, `menu_id`, `role_id`, `create_time`, `update_time`, `is_delete`) VALUES (16, 16, 1, '2025-04-17 09:27:45', '2025-04-17 09:27:52', 0);
INSERT INTO `management`.`menu_role_relation` (`id`, `menu_id`, `role_id`, `create_time`, `update_time`, `is_delete`) VALUES (17, 17, 1, '2025-04-17 09:27:45', '2025-04-17 09:27:52', 0);
INSERT INTO `management`.`menu_role_relation` (`id`, `menu_id`, `role_id`, `create_time`, `update_time`, `is_delete`) VALUES (18, 18, 1, '2025-04-17 09:27:45', '2025-04-17 09:27:52', 0);
INSERT INTO `management`.`menu_role_relation` (`id`, `menu_id`, `role_id`, `create_time`, `update_time`, `is_delete`) VALUES (19, 19, 1, '2025-04-17 09:27:45', '2025-04-17 09:27:52', 0);
INSERT INTO `management`.`menu_role_relation` (`id`, `menu_id`, `role_id`, `create_time`, `update_time`, `is_delete`) VALUES (20, 20, 1, '2025-04-17 09:27:45', '2025-04-17 09:27:52', 0);

权限认证

引入 Sa-Token

<!-- Sa-Token 权限认证 -->
<dependency><groupId>cn.dev33</groupId><artifactId>sa-token-spring-boot3-starter</artifactId><version>1.44.0</version>
</dependency>
<!-- Sa-Token 集成 Redis -->
<dependency><groupId>cn.dev33</groupId><artifactId>sa-token-redis-template</artifactId><version>1.44.0</version>
</dependency>
<dependency><groupId>org.apache.commons</groupId><artifactId>commons-pool2</artifactId>
</dependency>
<!-- Sa-Token 集成 JWT -->
<dependency><groupId>cn.dev33</groupId><artifactId>sa-token-jwt</artifactId><version>1.44.0</version>
</dependency>

配置文件

server:port: 8082
spring:datasource:url: jdbc:mysql://127.0.0.1:3306/managementusername: rootpassword: 123456driver-class-name: com.mysql.cj.jdbc.Driverdata:redis:port: 6379host: 127.0.0.1password: 123456database: 0
sa-token:# jwt 的秘钥jwt-secret-key: # token 的名字token-name: token# 是否打印日志is-log: true# 是否生成同一个 tokenis-share: true# 是否可以同时登录is-concurrent: false

通过逆向工程生成的 mybatis 文件此处省略

查询用户权限的接口

public interface UserService extends IService<User> {// 根据用户id 获取用户角色标识集合List<RoleVO> getRoleListByUserId(Integer userId);// 根据用户id 获取用户权限集合List<Menu> getACLListByUserId(Integer userId);// 菜单路径权限List<String> getRouteList(List<Menu> menuList);// 按钮权限List<String> getButtonList(List<Menu> menuList);
}
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {@Autowiredprivate RoleService roleService;@Autowiredprivate UserRoleRelationService userRoleRelationService;@Autowiredprivate MenuService menuService;@Autowiredprivate MenuRoleRelationService menuRoleRelationService;@Overridepublic List<RoleVO> getRoleListByUserId(Integer userId) {List<RoleVO> roles = new ArrayList<>();List<UserRoleRelation> userRoleRelations = userRoleRelationService.list(new LambdaQueryWrapper<UserRoleRelation>().eq(UserRoleRelation::getUserId, userId));if (CollectionUtil.isNotEmpty(userRoleRelations)) {List<Integer> roleIdList = userRoleRelations.stream().map(UserRoleRelation::getRoleId).toList();List<Role> roleList = roleService.list(new LambdaQueryWrapper<Role>().in(Role::getId, roleIdList));roles = BeanUtil.copyToList(roleList, RoleVO.class);}return roles;}@Overridepublic List<Menu> getACLListByUserId(Integer userId) {List<Menu> menuList = new ArrayList<>();// 设置 buttons routesList<UserRoleRelation> userRoleRelations = userRoleRelationService.list(new LambdaQueryWrapper<UserRoleRelation>().eq(UserRoleRelation::getUserId, userId));// 获取角色idList<Integer> roleIds = userRoleRelations.stream().map(UserRoleRelation::getRoleId).toList();if (CollectionUtil.isNotEmpty(roleIds)) {// 菜单和角色对应关系List<MenuRoleRelation> menuRoleRelations = menuRoleRelationService.list(new LambdaQueryWrapper<MenuRoleRelation>().in(MenuRoleRelation::getRoleId, roleIds));// 菜单列表List<Integer> menuIds = menuRoleRelations.stream().map(MenuRoleRelation::getMenuId).toList();if (CollectionUtil.isNotEmpty(menuIds)) {menuList = menuService.list(new LambdaQueryWrapper<Menu>().in(Menu::getId, menuIds));}}return menuList;}@Overridepublic List<String> getRouteList(List<Menu> menuList) {return menuList.stream().filter(item -> item.getLevel().equals(1) || item.getLevel().equals(2)).map(Menu::getAcl).toList();}@Overridepublic List<String> getButtonList(List<Menu> menuList) {return menuList.stream().filter(item -> item.getLevel().equals(3)).map(Menu::getAcl).toList();}
}

将权限列表加入配置

创建文件 SaTokenPermission

@Component
public class SaTokenPermission implements StpInterface {@Autowiredprivate UserService userService;@Overridepublic List<String> getPermissionList(Object loginId, String loginType) {Integer userId = Integer.parseInt(String.valueOf(loginId));List<Menu> menuList = userService.getACLListByUserId(userId);return menuList.stream().map(Menu::getAcl).toList();}@Overridepublic List<String> getRoleList(Object loginId, String loginType) {Integer userId = Integer.parseInt(String.valueOf(loginId));List<RoleVO> roleList = userService.getRoleListByUserId(userId);return roleList.stream().map(RoleVO::getTag).toList();}
}

创建文件 SaTokenConfig

@Configuration
public class SaTokenConfig implements WebMvcConfigurer {@Overridepublic void addInterceptors(InterceptorRegistry registry) {// 注册 Sa-Token 拦截器 并放行登录接口registry.addInterceptor(new SaInterceptor(handle -> StpUtil.checkLogin())).addPathPatterns("/**").excludePathPatterns("/user/login");}// Sa-Token 整合 jwt (Simple 简单模式)@Beanpublic StpLogic getStpLogicJwt() {return new StpLogicJwtForSimple();}
}

用户接口

前后端分离项目:登录后将token信息返回。

@Tag(name = "用户管理")
@RestController
@RequestMapping("user")
public class UserController {@Autowiredprivate UserService userService;@Operation(summary = "登录")@PostMapping("login")public SaResult login(@RequestBody LoginDTO loginDTO) {String username = loginDTO.getUsername();String password = loginDTO.getPassword();User user = userService.getOne(new LambdaQueryWrapper<User>().eq(User::getUsername, username).eq(User::getPassword, password));if (user != null) {Integer id = user.getId();StpUtil.login(id, new SaLoginParameter().setExtra("name", user.getNickname()).setExtra("avatar", user.getAvatar()));SaTokenInfo tokenInfo = StpUtil.getTokenInfo();return SaResult.ok("登录成功").setData(tokenInfo);}return SaResult.error("登录失败");}@Operation(summary = "下线用户")@PostMapping("logout")public SaResult logout() {Integer userId = StpUtil.getLoginIdAsInt();StpUtil.logout(userId);return SaResult.ok().setData(true);}@Operation(summary = "获取用户信息")@GetMapping("info")public SaResult userInfo() {// 判断是否登录StpUtil.checkLogin();// 获取用户信息Integer userId = StpUtil.getLoginIdAsInt();User user = userService.getById(userId);UserVO userVO = BeanUtil.copyProperties(user, UserVO.class);fillUserVO(userVO);return SaResult.ok().setData(userVO);}
}

权限注解的使用

此前通过 SaTokenPermission 已经将所有权限标识已经交给了 Sa-Token

通过 @SaCheckPermission 注解来限制接口是否能返回数据,注解的 value 为权限标识

@SaCheckPermission("btn.queryUser")
@Operation(summary = "根据id获取用户信息")
@GetMapping("getById/{id}")
public SaResult getById(@Parameter(description = "用户id")@PathVariable Integer id) {User user = userService.getById(id);UserVO userVO = BeanUtil.copyProperties(user, UserVO.class);userVO.setRoles(userService.getRoleListByUserId(id));fillUserVO(userVO);return SaResult.ok().setData(userVO);
}
@SaCheckPermission(value = {"btn.addUser", "btn.editUser"}, mode = SaMode.OR)
@Operation(summary = "新增/修改用户信息")
@PostMapping("createOrEdit")
public SaResult createOrEdit(@RequestBody UserDTO userDTO) {User user = BeanUtil.copyProperties(userDTO, User.class);boolean result = userService.saveOrUpdate(user);return SaResult.ok().setData(result);
}

全局异常处理

当权限不足时 Sa-Token 会抛出异常,定义全局异常处理捕获异常

创建 GlobalExceptionHandler

@RestControllerAdvice
public class GlobalExceptionHandler {@ExceptionHandlerpublic SaResult handlerException(Exception exception) {exception.printStackTrace();return SaResult.error(exception.getMessage());}
}

相关文章:

  • PCB设计教程【大师篇】STM32开发板原理图设计(接口部分)
  • BD202401补给
  • 统计学(第8版)——统计学基础统计抽样与抽样分布(考试用)
  • C++算法动态规划4
  • (javaSE)继承和多态:成员变量,super,子类构造方法,super和this,初始化, protected 继承方式 final关键字 继承与组合
  • RAG 处理流程
  • 家政行业数字化变革:小程序开发技术剖析与实战指南
  • Python应用大学期末考试选择练习系统
  • Spring Boot 中ConditionalOnClass、ConditionalOnMissingBean 注解详解
  • 黑马python(五)
  • LangChain MCP Adapters Quickstart
  • Linux CPU 亲和性
  • Pip Manager本地Python包管理器
  • 第五十一天打卡
  • 如何配置Dify中的MCP服务
  • 【AI News | 20250611】每日AI进展
  • MySQL之事务与读视图
  • 看板中如何管理技术债务
  • 【Java学习日记38】:C语言 fabs 与 Java abs 绝对值函数
  • Linux相关问题整理
  • 大良网站建设基本流程/百度广告竞价排名
  • java18/网站seo重庆
  • 学做网站论坛会员/seo网站推广教程
  • DW做网站下拉列表怎么做/谷歌seo搜索引擎优化
  • 做博物馆网站最重要性/青山seo排名公司
  • 做网站的网页设计用cdr吗/seo推广灰色词