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

React + SpringBoot开发用户中心管理系统

用户中心项目搭建笔记

技术栈

前端技术栈

  • “react”: “^18.2.0”,
  • ant-design-pro

后端技术栈

  • SpringBoot 2.6.x

项目源码地址

  • https://gitee.com/szxio/user-center

前端项目搭建

快速搭建一个后端管理系统项目框架

初始化

antDesignPro 官网: https://pro.ant.design/zh-CN。开箱即用的中台前端/设计解决方案

我们提供了 pro-cli 来快速的初始化脚手架。

# 使用 npm
npm i @ant-design/pro-cli -g
pro create user-center
cd user-center
pnpm install

去除国际化

pnpm i18n-remove

执行这个命令可以去掉项目中的国际化配置,再次启动可能会报引用错误,把多余的引用去掉即可

启动

pnpm start

image-20240511145232628

访问

image-20240511145311283

后端项目搭建

初始化

使用idea开发工具自带 Spring Initializr 完成项目创建

image-20240511145850969

如果Java版本无法选中8,可以切换上面的 Server URL 为阿里的源 https://start.aliyun.com,然后就可以选择8版本了

image-20240511150017423

接着点击 Next选择常用的开发依赖,下面我列出一些基本的依赖

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><groupId>com.szx</groupId><artifactId>user-center</artifactId><version>0.0.1-SNAPSHOT</version><name>user-center</name><description>user-center</description><properties><java.version>1.8</java.version><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding><project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding><spring-boot.version>2.6.13</spring-boot.version></properties><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.mybatis.spring.boot</groupId><artifactId>mybatis-spring-boot-starter</artifactId><version>2.2.2</version></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><optional>true</optional></dependency><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId></dependency><dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-boot-starter</artifactId><version>3.4.2</version></dependency><dependency><groupId>junit</groupId><artifactId>junit</artifactId><version>4.12</version></dependency><dependency><groupId>cn.hutool</groupId><artifactId>hutool-all</artifactId><version>5.8.26</version></dependency><!--swagger--><dependency><groupId>io.springfox</groupId><artifactId>springfox-boot-starter</artifactId><version>3.0.0</version></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency></dependencies><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></dependencies></dependencyManagement><build><plugins><plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-compiler-plugin</artifactId><version>3.8.1</version><configuration><source>1.8</source><target>1.8</target><encoding>UTF-8</encoding></configuration></plugin><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId><version>${spring-boot.version}</version><configuration><mainClass>com.szx.usercenter.UserCenterApplication</mainClass><skip>true</skip></configuration><executions><execution><id>repackage</id><goals><goal>repackage</goal></goals></execution></executions></plugin></plugins></build></project>

配置文件

application.yml

server:port: 8080spring:application:name: user-center# 数据源配置datasource:driver-class-name: com.mysql.cj.jdbc.Driverurl: jdbc:mysql://localhost:3306/user-center?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghaiusername: rootpassword: abc123# 配置日期返回格式jackson:# 日期格式date-format: yyyy-MM-dd HH:mm:ss# 时区time-zone: GMT+8# 非空的属性值才会被包含在结果中default-property-inclusion: non_nullmvc:pathmatch:# swagger配置路径匹配规则matching-strategy: ant_path_matchermybatis-plus:mapper-locations: classpath:/mapper/**.xmlconfiguration:# 开启控制台SQL输出log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

SwaggerUI配置

package com.szx.usercenter.config;import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;/*** @author songzx* @create 2022-09-22 11:21*/
@Configuration
@EnableSwagger2
public class SwaggerConfig {@Beanpublic Docket webApiConfig(){return new Docket(DocumentationType.SWAGGER_2).groupName("webApi").apiInfo(webApiInfo()).select().paths(path -> !path.contains("/error"))  // 过滤掉SwaggerUI自带的error路径的api.build();}public ApiInfo webApiInfo(){return new ApiInfoBuilder().title("用户中心接口文档").build();}
}

MybatisPlus分页插件和自动插入当前日期

@Configuration
public class MybatisPlusConfig implements MetaObjectHandler {// 分页插件@Beanpublic MybatisPlusInterceptor mybatisPlusInterceptor() {MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));return interceptor;}// 创建日期和更新日期自动更新@Overridepublic void insertFill(MetaObject metaObject) {setFieldValByName("createTime", new Date(),metaObject);setFieldValByName("updateTime",new Date(),metaObject);}// 更新日期自动更新@Overridepublic void updateFill(MetaObject metaObject) {setFieldValByName("updateTime",new Date(),metaObject);}
}

启动类设置

package com.szx.usercenter;import lombok.extern.log4j.Log4j2;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.core.env.Environment;import java.net.InetAddress;
import java.net.UnknownHostException;@Log4j2
@SpringBootApplication
@MapperScan("com.szx.usercenter.mapper")
public class UserCenterApplication {public static void main(String[] args) throws UnknownHostException {ConfigurableApplicationContext ioc = SpringApplication.run(UserCenterApplication.class, args);Environment env = ioc.getEnvironment();String host = InetAddress.getLocalHost().getHostAddress();String port = env.getProperty("server.port");log.info("\n    ----------------------------------------------------------\n\t" +"Application '{}' 正在运行中... Access URLs:\n\t" +"Local: \t\thttp://localhost:{}\n\t" +"External: \thttp://{}:{}\n\t" +"Doc: \thttp://{}:{}/doc.html\n\t" +"SwaggerDoc: \thttp://{}:{}/swagger-ui/index.html\n\t" +"----------------------------------------------------------",env.getProperty("spring.application.name"),env.getProperty("server.port"),host, port,host, port,host, port);}}

IDEA自带的代码生成器

image-20240511151420847

image-20240511151606132

注意:生成的文件会覆盖原有文件

统一结果返回类

Response

package com.szx.usercenter.util;import com.fasterxml.jackson.annotation.JsonInclude;/*** @author songzx* @date 2023/6/4* @apiNote*/
@JsonInclude(JsonInclude.Include.NON_NULL) // 值等于null的属性不返回
public class Response<T> {private String code;private String msg;private T data;/*** @title 成功消息* @return*/public static <T> Response<T> success() {return rspMsg(ResponseEnum.SUCCESS);}/*** @title 失败消息* @return*/public static <T> Response<T> error() {return rspMsg(ResponseEnum.SERVER_INNER_ERR);}/*** @title 自定义消息* @return*/public static <T> Response<T> rspMsg(ResponseEnum responseEnum) {Response<T> message = new Response<T>();message.setCode(responseEnum.getCode());message.setMsg(responseEnum.getMsg());return message;}/*** @title 自定义消息* @return*/public static <T> Response<T> rspMsg(String code , String msg) {Response<T> message = new Response<T>();message.setCode(code);message.setMsg(msg);return message;}/*** @title 返回数据* @param data* @return*/public static <T> Response<T> rspData(T data) {Response<T> responseData = new Response<T>();responseData.setCode(ResponseEnum.SUCCESS.getCode());responseData.setData(data);return responseData;}public static <T> Response<T> error(T data) {Response<T> responseData = new Response<T>();responseData.setCode(ResponseEnum.ERROR.getCode());responseData.setData(data);return responseData;}/*** @title 返回数据-自定义code* @param data* @return*/public static <T> Response<T> rspData(String code , T data) {Response<T> responseData = new Response<T>();responseData.setCode(code);responseData.setData(data);return responseData;}public String getCode() {return code;}public void setCode(String code) {this.code = code;}public String getMsg() {return msg;}public void setMsg(String msg) {this.msg = msg;}public T getData() {return data;}public void setData(T data) {this.data = data;}
}

枚举类 ResponseEnum

package com.szx.usercenter.util;/*** @author songzx* @create 2023-12-05 14:25*/
public enum ResponseEnum {// 可以根据自己的实际需要增加状态码SUCCESS("200", "成功"),ERROR("500","系统异常"),SERVER_INNER_ERR("500","系统繁忙"),LOGIN_EXPIRED("401","登录过期"),PARAM_LACK("100" , "非法参数"),OPERATION_FAILED("101" ,"操作失败");private String code;private String msg;ResponseEnum(String code, String msg) {this.code = code;this.msg = msg;}public String getCode() {return code;}public void setCode(String code) {this.code = code;}public String getMsg() {return msg;}public void setMsg(String msg) {this.msg = msg;}
}

编写测试接口

package com.szx.usercenter.controller;import com.szx.usercenter.domain.SysUser;
import com.szx.usercenter.service.SysUserService;
import com.szx.usercenter.util.Response;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.web.bind.annotation.*;import javax.annotation.Resource;
import java.util.List;/*** @author songzx* @create 2024-05-11 10:23*/
@RestController
@RequestMapping("/sysUser")
@Api(tags = "用户管理")
public class SusUserController {@ResourceSysUserService sysUserService;/*** 获取所有用户* @return*/@GetMapping("getAllUser")@ApiOperation("获取所有用户")public Response<List<SysUser>> getUserList() {return Response.rspData(sysUserService.list());}/*** 登录* @param sysUser* @return*/@PostMapping("login")@ApiOperation("登录")public Response login(@RequestBody SysUser sysUser) {SysUser login = sysUserService.login(sysUser);if(login != null){login.setPassword(null);return Response.rspData(login);}else{return Response.error("用户名或密码错误");}}
}

重启项目,访问Swagger页面试试

image-20240511152136291

至此后端项目搭建完成

打包pom通用配置

<build><plugins><plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-compiler-plugin</artifactId><version>3.8.1</version> <!-- 或者你使用的版本 --><configuration><source>1.8</source><target>1.8</target></configuration></plugin><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId><version>2.3.7.RELEASE</version><executions><execution><goals><goal>repackage</goal></goals></execution></executions></plugin></plugins>
</build>

SQL建表语句

可以复制若依的表来使用,下面是sql地址

https://gitee.com/y_project/RuoYi-Vue/blob/master/sql/ry_20231130.sql

用户表 sys_user

drop table if exists sys_user;
create table sys_user (user_id           bigint(20)      not null auto_increment    comment '用户ID',dept_id           bigint(20)      default null               comment '部门ID',user_name         varchar(30)     not null                   comment '用户账号',nick_name         varchar(30)     not null                   comment '用户昵称',user_type         varchar(2)      default '00'               comment '用户类型(00系统用户)',email             varchar(50)     default ''                 comment '用户邮箱',phonenumber       varchar(11)     default ''                 comment '手机号码',sex               char(1)         default '0'                comment '用户性别(0男 1女 2未知)',avatar            varchar(100)    default ''                 comment '头像地址',password          varchar(100)    default ''                 comment '密码',status            char(1)         default '0'                comment '帐号状态(0正常 1停用)',del_flag          char(1)         default '0'                comment '删除标志(0代表存在 2代表删除)',login_ip          varchar(128)    default ''                 comment '最后登录IP',login_date        datetime                                   comment '最后登录时间',create_by         varchar(64)     default ''                 comment '创建者',create_time       datetime                                   comment '创建时间',update_by         varchar(64)     default ''                 comment '更新者',update_time       datetime                                   comment '更新时间',remark            varchar(500)    default null               comment '备注',primary key (user_id)
) engine=innodb auto_increment=100 comment = '用户信息表';

角色表 sys_role

drop table if exists sys_role;
create table sys_role (role_id              bigint(20)      not null auto_increment    comment '角色ID',role_name            varchar(30)     not null                   comment '角色名称',role_key             varchar(100)    not null                   comment '角色权限字符串',del_flag             char(1)         default '0'                comment '删除标志(0代表存在 1代表删除)',create_by            varchar(64)     default ''                 comment '创建者',create_time          datetime                                   comment '创建时间',update_by            varchar(64)     default ''                 comment '更新者',update_time          datetime                                   comment '更新时间',remark               varchar(500)    default null               comment '备注',primary key (role_id)
) engine=innodb auto_increment=100 comment = '角色信息表';

用户角色表 sys_user_role

drop table if exists sys_user_role;
create table sys_user_role (user_id   bigint(20) not null comment '用户ID',role_id   bigint(20) not null comment '角色ID',primary key(user_id)
) engine=innodb comment = '用户和角色关联表';

角色菜单表 sys_role_menu

drop table if exists sys_role_menu;
create table sys_role_menu (role_id   	 bigint(0) 	not null comment '角色ID',routes    	 text                comment '保存的routes数据',checked_keys   text 				 comment '选中的key',primary key(role_id)
) engine=innodb comment = '角色和菜单关联表';

密码的加密和校验

用到了hutool包中的BCrypt加密工具类

package com.szx.usercenter;import cn.hutool.crypto.digest.BCrypt;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;/*** @author songzx* @date 2024/5/12* @apiNote*/
@SpringBootTest
public class UserPasswordTest {/*** 密码加密*/@Testvoid testJbcrypt() {String passwordToHash = "abc123";// BCrypt.gensalt()会随机生成一个数作为盐,因此密码相同情况下,每次的密文是不一样String hashedPassword = BCrypt.hashpw(passwordToHash, BCrypt.gensalt());System.out.println(hashedPassword);}/*** 密码校验*/@Testvoid testJbcryptCheck() {String passwordToCheck = "abc123";String hashedPassword = "$2a$10$wpngf2ng8ynf2WQGLSgh6.ztH7q7Bn0mhsH.7x08qLevfzISmSzd2";boolean checkpw = BCrypt.checkpw(passwordToCheck, hashedPassword);System.out.println(checkpw);}
}

image-20240512170629568

image-20240512170542708

后端功能开发

注册逻辑

  1. 用户名不能有特殊字符,并且必须超过6位数
  2. 密码必须超过6位数
  3. 用户名不能重复
  4. 密码加密后保存到数据库中

注册接口开发

@RestController
@RequestMapping("/sysUser")
@Api(tags = "用户管理")
public class SusUserController {@ResourceSysUserService sysUserService;/*** 用户注册* @param username 用户名* @param password 密码* @return*/@ApiOperation("用户注册")@PostMapping("register")public Response register(String username, String password){return sysUserService.register(username,password);}
}

实现 register 方法

@Override
public Response register(String username, String password) {// 1.用户名不能有特殊字符,并且必须超过6位数if(!username.matches("^[a-zA-Z0-9_-]{6,16}$")){return Response.error("用户名必须超过6位数,并且不能有特殊字符");}// 2.密码必须超过6位数if(password.length() < 6){return Response.error("密码必须超过6位数");}// 3.用户名不能重复if(this.getOne(new LambdaQueryWrapper<SysUser>().eq(SysUser::getUserName, username)) != null){return Response.error("用户名已存在");}// 4.添加用户到表中SysUser sysUser = new SysUser();sysUser.setUserName(username);sysUser.setNickName(username);sysUser.setPassword(BCrypt.hashpw(password, BCrypt.gensalt())); // 密码加密保存boolean isOk = this.save(sysUser);return isOk ? Response.success() : Response.error("注册失败");
}

登录逻辑

  1. 根据用户名获取数据库表中保存的用户信息
  2. 在用传递进来的密码和表中的密码进行密码校验
  3. 校验成功返回用户信息
  4. 否则登录失败

登录接口开发

@RestController
@RequestMapping("/sysUser")
@Api(tags = "用户管理")
public class SusUserController {@ResourceSysUserService sysUserService;/*** 登录* @param sysUser* @return*/@PostMapping("login")@ApiOperation("登录")public Response login(@RequestBody SysUser sysUser) {return sysUserService.login(sysUser);}
}

login 方法实现

@Override
public Response login(SysUser sysUser) {// 1.获取用户填写的用户名和密码String userName = sysUser.getUserName();String password = sysUser.getPassword();// 2.校验用户名密码if(userName == null || password == null){return Response.error("用户名或密码不能为空");}SysUser one = this.getOne(new LambdaQueryWrapper<SysUser>().eq(SysUser::getUserName, userName));if(one == null){return Response.error("用户名不存在");}if(!BCrypt.checkpw(password, one.getPassword())){return Response.error("密码错误");}// 3.返回用户信息,清空返回体中的密码one.setPassword(null);return Response.rspData(one);
}

生成Token

给登录接口返回的内容中添加Token

在 login 实现方法中增加一个行代码,JwtHelper 的使用方法看的的这个文章,写的很详细

/*** 登录* @param sysUser* @return*/
@Override
public Response login(SysUser sysUser) {// 1.获取用户填写的用户名和密码String userName = sysUser.getUserName();String password = sysUser.getPassword();// 2.校验用户名密码if(userName == null || password == null){return Response.error("用户名或密码不能为空");}SysUser one = this.getOne(new LambdaQueryWrapper<SysUser>().eq(SysUser::getUserName, userName));if(one == null){return Response.error("用户名不存在");}if(!BCrypt.checkpw(password, one.getPassword())){return Response.error("密码错误");}// 3.返回用户信息,清空返回体中的密码one.setPassword(null);// 生成tokenone.setToken(JwtHelper.createToken(sysUser.getUserId(), sysUser.getUserName()));return Response.rspData(one);
}

添加Token拦截器

编写 Token 配置类

package com.szx.usercenter.config;import com.szx.usercenter.handle.TokenHandle;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;/*** @author songzx* @date 2024/5/12* @apiNote*/
@Configuration
public class TokenConfig implements WebMvcConfigurer {@Overridepublic void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(new TokenHandle()).addPathPatterns("/**").excludePathPatterns("/sysUser/login","/sysUser/register","/swagger-ui.html","/swagger-ui/index.html","/swagger-resources","/v2/api-docs","/v2/api-docs-ext","/doc.html","/swagger-resources/configuration/ui","/swagger-resources/configuration/security","/swagger-resources/configuration/ui","/webjars/**","/swagger-resources/**");}
}

TokenHandle 代码,从请求头中获取 X-Token,进行校验,如果为空或者过期,则抛出自定义全局异常。

package com.szx.usercenter.handle;import cn.hutool.core.util.StrUtil;
import com.szx.usercenter.util.JwtHelper;
import com.szx.usercenter.util.ResponseEnum;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;/*** @author songzx* @date 2024/5/12* @apiNote*/
public class TokenHandle implements HandlerInterceptor {@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {if(!(handler instanceof HandlerMethod)){return true;}// 从请求头中获取tokenString token = request.getHeader("X-Token");// 获取请求来源String referer = request.getHeader("Referer");boolean fromSwagger = referer.endsWith("swagger-ui/index.html");// 校验tokenif(!fromSwagger && (StrUtil.isEmpty(token) || JwtHelper.tokenExpired(token))){// 如果token校验失败则抛出自定义全局异常throw new CenterExceptionHandler(ResponseEnum.LOGIN_EXPIRED);}return true;}
}

自定义全局异常

新建全局异常处理类

GlobalExceptionHandler

package com.szx.usercenter.handle;import cn.hutool.core.exceptions.ExceptionUtil;
import com.szx.usercenter.util.Response;
import lombok.extern.log4j.Log4j2;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestControllerAdvice;/*** @author songzx* @date 2024/5/12* @apiNote*/
@RestControllerAdvice
@Log4j2
public class GlobalExceptionHandler {// 全局异常处理@ExceptionHandler(Exception.class)@ResponseBodypublic Response<Object> error(Exception e){log.error(ExceptionUtil.getMessage(e));e.printStackTrace();// 将异常转成string返回出去return Response.error(e.getMessage());}/*** 处理自定义的异常-CenterExceptionHandler* @param e* @return*/@ExceptionHandler(CenterExceptionHandler.class)@ResponseBodypublic Response<Object>  businessExceptionHandler(CenterExceptionHandler e){log.error("CenterExceptionHandler:" + e.getMessage(),e);return Response.rspMsg(e.getCode(),e.getMessage());}
}

新建自定义异常处理类

CenterExceptionHandler

package com.szx.usercenter.handle;import com.szx.usercenter.util.ResponseEnum;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;/*** 自定义全局异常处理类* @author songzx* @date 2024/5/12* @apiNote*/
@Data
public class CenterExceptionHandler extends RuntimeException{/*** 错误码*/private String code;/*** 业务异常** @param message     信息* @param code        错误码*/public CenterExceptionHandler(String message, String code) {super(message);this.code = code;}/*** 业务异常** @param errorCode 错误代码*/public CenterExceptionHandler(ResponseEnum errorCode) {super(errorCode.getMsg());this.code = errorCode.getCode();}/*** 默认业务异常,状态默认500** @param message 信息*/public CenterExceptionHandler(String message) {super(message);this.code = ResponseEnum.ERROR.getCode();}/*** 默认业务异常*/public CenterExceptionHandler() {super(ResponseEnum.ERROR.getMsg());this.code = ResponseEnum.ERROR.getCode();}
}

然后再任何需要抛出异常的地方直接使用即可

例如

@GetMapping("testError")
public Response testError(){throw new CenterExceptionHandler("测试异常");
}

image-20240512195337458

查询接口开发

接口实现类 SysUserServiceImpl 添加方法

// 用户信息脱敏方法
@Override
public SysUser getSefUser(SysUser user) {SysUser sysUser = ObjUtil.clone(user);sysUser.setPassword(null);return sysUser;
}@Override
public Response getPageUserList(SysUser sysUser) {Page<SysUser> sysUserPage = new Page<>(sysUser.getCurrent(), sysUser.getPageSize());LambdaQueryWrapper<SysUser> qw = new LambdaQueryWrapper<>();// 用户名称查询if (StrUtil.isNotEmpty(sysUser.getUserName())) {qw.like(SysUser::getUserName, sysUser.getUserName());}// 手机号查询if (StrUtil.isNotEmpty(sysUser.getPhonenumber())) {qw.like(SysUser::getPhonenumber, sysUser.getPhonenumber());}// 创建日期查询,查询当天内的所有数据if (ObjectUtil.isNotEmpty(sysUser.getCreateTime())) {Date startDate = DateUtil.beginOfDay(sysUser.getCreateTime()); // 将前端传来的日期转换为当天的开始时间Date endDate = DateUtil.endOfDay(startDate);  // 将结束日期设置为当天的结束时间qw.between(SysUser::getCreateTime, startDate, endDate);}this.page(sysUserPage, qw);// 返回的用户信息脱敏List<SysUser> userList = sysUserPage.getRecords();sysUserPage.setRecords(userList.stream().map(user -> getSefUser(user)).collect(Collectors.toList()));return Response.rspData(sysUserPage);
}

这里前端传递过来的日期格式是字符串类型的日期,例如:2024-05-14 17:12:47,但是后端定义的 createTime 字段类型是 Date 类型,默认会出现一个类型转换错误的异常,如下图

image-20240517171439008

前端传递的参数

image-20240517171311614

image-20240517171337859

我们可以修改配置文件,增加一个日期转换格式的配置

spring:# 配置日期返回格式jackson:# 日期格式date-format: yyyy-MM-dd HH:mm:ss# 时区time-zone: GMT+8# 非空的属性值才会被包含在结果中default-property-inclusion: non_null

重启项目,再次查询就不会报错了

image-20240517171617614

自动填充创建人和更新人

新建一个 BaseUser

package com.szx.usercenter.contance;import lombok.Data;
import org.springframework.stereotype.Component;/*** @author songzx* @date 2024/5/18* @apiNote*/
@Data
@Component // 这里注意添加@Component注解,交给Spring容器管理
public class BaseUser {public static String userName;
}

然后再token拦截器中根据当前请求头中的tokne获取当前用户名,给BaseUser的userName赋值

修改 TokenHandle

package com.szx.usercenter.handle;import cn.hutool.core.util.StrUtil;
import com.szx.usercenter.contance.BaseUser;
import com.szx.usercenter.util.JwtHelper;
import com.szx.usercenter.util.ResponseEnum;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;/*** @author songzx* @date 2024/5/12* @apiNote*/
public class TokenHandle implements HandlerInterceptor {@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {if (!(handler instanceof HandlerMethod)) {return true;}// 从请求头中获取tokenString token = request.getHeader("Authorization");// 获取请求来源String referer = request.getHeader("Referer");boolean fromSwagger = referer.endsWith("swagger-ui/index.html");// 校验tokenif (!fromSwagger && (StrUtil.isEmpty(token) || JwtHelper.tokenExpired(token))) {throw new CenterExceptionHandler(ResponseEnum.LOGIN_EXPIRED);}// 获取用户名BaseUser.userName = JwtHelper.getUserName(token);return true;}
}

修该 MybatisPlusConfig

package com.szx.usercenter.config;import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import com.szx.usercenter.contance.BaseUser;
import org.apache.ibatis.reflection.MetaObject;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;import java.util.Date;/*** @author songzx* @create 2024-05-11 10:08*/
@Configuration
public class MybatisPlusConfig implements MetaObjectHandler {// 分页插件@Beanpublic MybatisPlusInterceptor mybatisPlusInterceptor() {MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));return interceptor;}// 创建日期和更新日期自动更新@Overridepublic void insertFill(MetaObject metaObject) {setFieldValByName("createTime", new Date(), metaObject);setFieldValByName("createBy", BaseUser.userName, metaObject);setFieldValByName("updateTime", new Date(), metaObject);setFieldValByName("updateBy", BaseUser.userName, metaObject);}// 更新日期自动更新@Overridepublic void updateFill(MetaObject metaObject) {setFieldValByName("updateTime", new Date(), metaObject);setFieldValByName("updateBy", BaseUser.userName, metaObject);}
}

更新和创建时值自动填充

image-20240518111601052

前端功能开发

登录逻辑梳理

首先找到登录页面对应的文件,位置在src/pages/User/Login/index.tsx,然后观察代码,发现登录页面使用了 LoginForm 组件来实现的登录表单,LoginForm 是从 @ant-design/pro-components 中导出的,ProComponents 是基于 Ant Design 而开发的模板组件,提供了更高级别的抽象支持,开箱即用。可以显著地提升制作 CRUD 页面的效率,更加专注于页面。

image-20240512101318285

loginForm组件使用文档:https://pro-components.antdigital.dev/components/form

点击登录会触发onFinish钩子函数,调用handleSubmit方法

image-20240512101903528

image-20240511180444921

login方法在src/services/ant-design-pro/api.ts 文件中声明

image-20240512102226082

登录成功后调用 fetchUserInfo 方法获取用户信息

image-20240512102451836

useModel@umi/max 内置的数据流管理插件,它是一种基于 hooks 范式的轻量级数据管理方案,可以在 Umi 项目中管理全局的共享数据。

文档地址:https://umijs.org/docs/max/data-flow#usemodel

useModel('@@initialState') 表示读取 app.tsx 文件中的 getInitialState 方法的返回值

const {initialState, setInitialState} = useModel('@@initialState');

app.tsx 文件中的 getInitialState 代码如下

import {currentUser as queryCurrentUser} from '@/services/ant-design-pro/api';
const loginPath = '/user/login';export async function getInitialState(): Promise<{settings?: Partial<LayoutSettings>;currentUser?: API.CurrentUser;loading?: boolean;fetchUserInfo?: () => Promise<API.CurrentUser | undefined>;
}> {const fetchUserInfo = async () => {try {console.log("获取用户信息")const msg = await queryCurrentUser({skipErrorHandler: true,});return msg.data;} catch (error) {history.push(loginPath);}return undefined;};// 如果不是登录页面,执行const {location} = history;if (location.pathname !== loginPath) {const currentUser = await fetchUserInfo();return {fetchUserInfo,currentUser,settings: defaultSettings as Partial<LayoutSettings>,};}return {fetchUserInfo,settings: defaultSettings as Partial<LayoutSettings>,};
}

查看 queryCurrentUser 接口地址

image-20240512104040157

找到mock中对应的接口

image-20240512104147557

下面我们按照这种格式编写后端接口即可

修改响应拦截器

找到 src/requestErrorConfig.tss 文件,这个文件中处理请求拦截和响应拦截

需要做的功能

  • 给每个请求添加一个基础路径,配合代理完成跨域处理
  • 给每个请求中添加token请求头
  • 判断响应结果是否成功,如果不成功弹出错误提醒
import type {RequestOptions} from '@@/plugin-request/request';
import type {RequestConfig} from '@umijs/max';
import {message, notification} from 'antd';
import {getToken} from '@/utils';// 错误处理方案: 错误类型
enum ErrorShowType {SILENT = 0,WARN_MESSAGE = 1,ERROR_MESSAGE = 2,NOTIFICATION = 3,REDIRECT = 9,
}// 与后端约定的响应数据格式
interface ResponseStructure {success: boolean;data: any;errorCode?: number;errorMessage?: string;showType?: ErrorShowType;
}// 请求前缀
const baseURL = '/api';/*** @name 错误处理* pro 自带的错误处理, 可以在这里做自己的改动* @doc https://umijs.org/docs/max/request#配置*/
export const errorConfig: RequestConfig = {// 错误处理: umi@3 的错误处理方案。errorConfig: {// 错误抛出errorThrower: (res) => {const { success, data, errorCode, errorMessage, showType } =res as unknown as ResponseStructure;if (!success) {const error: any = new Error(errorMessage);error.name = 'BizError';error.info = { errorCode, errorMessage, showType, data };throw error; // 抛出自制的错误}},// 错误接收及处理errorHandler: (error: any, opts: any) => {if (opts?.skipErrorHandler) throw error;// 我们的 errorThrower 抛出的错误。if (error.name === 'BizError') {const errorInfo: ResponseStructure | undefined = error.info;if (errorInfo) {const { errorMessage, errorCode } = errorInfo;switch (errorInfo.showType) {case ErrorShowType.SILENT:// do nothingbreak;case ErrorShowType.WARN_MESSAGE:message.warning(errorMessage);break;case ErrorShowType.ERROR_MESSAGE:message.error(errorMessage);break;case ErrorShowType.NOTIFICATION:notification.open({description: errorMessage,message: errorCode,});break;case ErrorShowType.REDIRECT:// TODO: redirectbreak;default:message.error(errorMessage);}}} else if (error.response) {// Axios 的错误// 请求成功发出且服务器也响应了状态码,但状态代码超出了 2xx 的范围message.error(`Response status:${error.response.status}`);} else if (error.request) {// 请求已经成功发起,但没有收到响应// \`error.request\` 在浏览器中是 XMLHttpRequest 的实例,// 而在node.js中是 http.ClientRequest 的实例message.error('None response! Please retry.');} else {// 发送请求时出了点问题message.error(error?.data || error?.msg);}},},// 请求拦截器requestInterceptors: [(config: RequestOptions) => {// 给请求头中加一个abc参数config.headers.Authorization = getToken();// 拦截请求配置,进行个性化处理。const url = baseURL + config?.url;return { ...config, url };},],// 响应拦截器responseInterceptors: [(response) => {const sucCodes = ['200', 200];// 拦截响应数据,进行个性化处理const { data } = response as unknown as ResponseStructure;if (!sucCodes.includes(data?.code)) {// 返回错误信息交给错误处理器return Promise.reject(data);}return response;},],
};

用到的 getToken 方法

/*** 设置token* @param token*/
export function setToken(token){localStorage.setItem('token',token)
}/*** 获取token*/
export function getToken(){return localStorage.getItem('token')
}

设置代理

修改 config/proxy.ts 代码

/*** @name 代理的配置* @see 在生产环境 代理是无法生效的,所以这里没有生产环境的配置* -------------------------------* The agent cannot take effect in the production environment* so there is no configuration of the production environment* For details, please see* https://pro.ant.design/docs/deploy** @doc https://umijs.org/docs/guides/proxy*/
export default {// 如果需要自定义本地开发服务器  请取消注释按需调整dev: {'/api/edu': {target: 'http://123.60.16.27:8101',changeOrigin: true,pathRewrite: { '/api': '' },},// localhost:8000/api/** -> https://preview.pro.ant.design/api/**'/api/': {// 要代理的地址target: 'http://localhost:8080',// 配置了这个可以从 http 代理到 https// 依赖 origin 的功能可能需要这个,比如 cookiechangeOrigin: true,// 去掉真实请求地址中的/apipathRewrite: { '/api': '' },},},/*** @name 详细的代理配置* @doc https://github.com/chimurai/http-proxy-middleware*/test: {// localhost:8000/api/** -> https://preview.pro.ant.design/api/**'/api/': {target: 'https://proapi.azurewebsites.net',changeOrigin: true,pathRewrite: { '^': '' },},},pre: {'/api/': {target: 'your pre url',changeOrigin: true,pathRewrite: { '^': '' },},},
};

权限管理

找到 src/access.ts 文件

export default function access(initialState: { currentUser?: API.CurrentUser } | undefined) {const {currentUser} = initialState ?? {};return {canAdmin: currentUser && currentUser.access?.includes("admin"),};
}

access 方法的 initialState 参数就是 app.tsx 文件中的 getInitialState 方法的返回值,这里是 Umi 框架帮我们封装好的

参考文档:权限管理 - Ant Design Pro

登录功能实现

修改 src/pages/Login/index.tsx 文件代码,删除多余代码

import { Footer } from '@/components';
import { login } from '@/services/ant-design-pro/api';
import { LockOutlined, UserOutlined } from '@ant-design/icons';
import { LoginForm, ProFormText } from '@ant-design/pro-components';
import { history, useModel, Helmet } from '@umijs/max';
import { message, Tabs } from 'antd';
import Settings from '../../../config/defaultSettings';
import React, { useState } from 'react';
import { flushSync } from 'react-dom';
import { setToken } from '@/utils';
import ForgotPasswordForm from '@/pages/Login/ForgotPasswordForm';
import useStyles from './useStyles.less';const getUserRole = async () => {return ['admin'];
};const Login: React.FC = () => {const [type, setType] = useState<string>('account');const { initialState, setInitialState } = useModel('@@initialState');const [forgotPassword, setForgotPassword] = useState(false);const fetchUserInfo = async (data) => {if (data) {let roles = await getUserRole();flushSync(() => {// 更新全局保存的用户信息setInitialState((s) => ({...s,currentUser: {...data,access: roles,},}));});}};const handleSubmit = async (values) => {// 登录let { data } = await login({userName: values.username,password: values.password,});if (data.token) {setToken(data.token);const defaultLoginSuccessMessage = '登录成功!';message.success(defaultLoginSuccessMessage);await fetchUserInfo(data);const urlParams = new URL(window.location.href).searchParams;history.push(urlParams.get('redirect') || '/');}};const updatePasswordStatus = (flag) => {setForgotPassword(flag);};return (<div className={useStyles.container}><Helmet><title>{'登录'}- {Settings.title}</title></Helmet>{/*忘记密码,重置密码表单*/}{forgotPassword && <ForgotPasswordForm updatePasswordStatus={updatePasswordStatus} />}{!forgotPassword && (<div style={{ marginTop: '5%' }}><LoginFormcontentStyle={{minWidth: 280,maxWidth: '75vw',}}logo={<img alt="logo" src="/logo.svg" />}title="用户管理中心"initialValues={{autoLogin: false,username: 'admin001',password: 'Abc123',}}onFinish={async (values) => {await handleSubmit(values as API.LoginParams);}}><TabsactiveKey={type}onChange={setType}centereditems={[{key: 'account',label: '账户密码登录',},]}/><><ProFormTextname="username"fieldProps={{size: 'large',prefix: <UserOutlined />,}}placeholder={'请输入用户名'}rules={[{required: true,message: '用户名是必填项!',},]}/><ProFormText.Passwordname="password"fieldProps={{size: 'large',prefix: <LockOutlined />,}}placeholder={'请输入密码'}rules={[{required: true,message: '密码是必填项!',},]}/></><divstyle={{marginBottom: 24,}}><astyle={{float: 'right',marginBottom: 20,}}onClick={() => updatePasswordStatus(true)}>忘记密码 ?</a></div></LoginForm></div>)}<Footer /></div>);
};
export default Login;

登录接口 src/services/ant-design-pro/api.ts

import {request} from '@umijs/max';/** 登录接口  */
export async function login(body: API.LoginParams, options?: { [key: string]: any }) {return request<API.LoginResult>('/sysUser/login', {method: 'POST',headers: {'Content-Type': 'application/json',},data: body,...(options || {}),});
}

样式文件 useStyles.less

.container {display: flex;flex-direction: column;height: 100vh;overflow: auto;background-image: url(https://mdn.alipayobjects.com/yuyan_qk0oxh/afts/img/V-_oS6r-i7wAAAAAAAAAAAAAFl94AQBr);background-size: 100% 100%;
}

忘记密码

新建文件 src/pages/Login/ForgotPasswordForm.tsx

import React from "react";
import {Button, Form, Input, message, Tabs} from 'antd';
import {LockOutlined, UserOutlined} from "@ant-design/icons";
import {updatePasswordFun} from "@/services/ant-design-pro/login";/** 组件名: ForgotPasswordForm* 组件用途: 重置密码表单* 创建日期: 2024/5/14*/
const ForgotPasswordForm = (props) => {const onFinish = (values) => {if (values.password.length < 6) {message.error("密码长度至少6位")return;}// 密码必须同时包含数字和大小写字母if (!/[A-Z]/.test(values.password) || !/[a-z]/.test(values.password) || !/[0-9]/.test(values.password)) {message.error("密码必须同时包含数字和大小写字母")return;}// 两次密码必须一致if (values.password !== values.newPassword) {message.error("两次密码不一致")return;}updatePasswordFun(values.userName, values.newPassword).then(() => {message.success("更新密码成功,返回登录")props.updatePasswordStatus(false)})};return (<div className={"ant-pro-form-login-container"} style={{display: "flex",flexDirection: "column",alignItems: "center",flex: "none",height: "auto"}}><div className="ant-pro-form-login-header" style={{marginTop: "5%"}}><span className="ant-pro-form-login-logo "><img alt="logo" src="/logo.svg"/></span><span className="ant-pro-form-login-title ">用户管理中心</span></div><TabsactiveKey={"account"}centereditems={[{key: 'account',label: '重置密码',},]}/><Formname="basic"style={{width: 328}}initialValues={{remember: false,}}layout="vertical"onFinish={onFinish}autoComplete="off"><Form.Itemlabel=""name="userName"rules={[{required: true,message: '请输入用户名',},]}><Inputsize={'large'}prefix={<UserOutlined/>}placeholder="请输入用户名"/></Form.Item><Form.Itemlabel=""name="password"rules={[{required: true,message: '请输入密码!',},]}><Input.Password size={'large'} prefix={<LockOutlined/>} placeholder="请输入密码"/></Form.Item><Form.Itemlabel=""name="newPassword"rules={[{required: true,message: '请确认密码!',},]}><Input.Password size={'large'} prefix={<LockOutlined/>} placeholder="请确认密码"/></Form.Item><divstyle={{marginBottom: 24,}}><astyle={{float: 'right',marginBottom: 20}}onClick={() => props.updatePasswordStatus(false)}>返回登录</a></div><Button type="primary" htmlType="submit" size="large" block>确认</Button></Form></div>);
};export default ForgotPasswordForm;

image-20240517173734307

动态获取菜单

官方提供的动态菜单实现方法:菜单的高级用法 - Ant Design Pro

前提说明:实现动态路由时,所有的路由都必须提前在 config/routes.ts 中注册好,如果动态返回了 routes.ts 中不存在的路由信息,页面将会无法访问,具体问题可参考(Issue #11137)。只能动态返回 routes.ts 内的数据

修改 app.tsx 的 layout 方法,在配置中添加 menu 属性即可实现动态菜单

// ProLayout 支持的api https://procomponents.ant.design/components/layout
export const layout: RunTimeLayoutConfig = ({ initialState, setInitialState }) => {return {// ... 省略其他代码menu: {// 每当 initialState?.currentUser?.userid 发生修改时重新执行 requestparams: {userId: initialState?.currentUser?.userId,roleIds: initialState?.currentUser?.sysRoleList?.map((item) => item.roleId),},request: async (params, defaultMenuData) => {// 调用接口获取菜单数据let { data } = await getRoleMenuFun(params.roleIds);if (data.length > 0) {return data;}return defaultMenuData;},},// 手动映射iconmenuDataRender: (menuData) => fixMenuItemIcon(menuData),};
};

接口返回的data菜单格式和 config/routes.ts 中配置的保持一致,更多配置可以参考:Pro 的 Layout 组件 - Ant Design Pro

返回内容示例:

{path: '/user/login',layout: false, // 页面是否在layout布局样式下显示,设置成false会单独显示hideInMenu: true, // 是否隐藏菜单,这里只是设置不在左侧菜单列表中显示,仍可以访问name: '登录',component: './Login',
},
{path: '/welcome',name: '欢迎',icon: 'smile',component: './Welcome',
},
{path: 'test',name: '一级菜单',routes: [{path: 'test1',name: '二级菜单1',routes: [{path: 'test1-1',name: '三级菜单1-1',component: './Test',},{path: 'test1-2',name: '三级菜单1-2',component: './Test',},],},{path: 'test2',name: '二级菜单2',component: './Test',},],
},

当我们使用了动态返回的菜单时,图标就不出现了,这时需要手动映射icon图标,可参考这里

添加 src/utils/fixMenuItemIcon.ts 文件

import React from 'react';
import * as allIcons from '@ant-design/icons';// FIX从接口获取菜单时icon为string类型
const fixMenuItemIcon = (menus, iconType = 'Outlined') => {menus.forEach((item) => {const { icon, children } = item;if (typeof icon === 'string') {let fixIconName = icon.slice(0, 1).toLocaleUpperCase() + icon.slice(1) + iconType;console.log(fixIconName, 'fixIconName');item.icon = React.createElement(allIcons[fixIconName] || allIcons[icon]);}// eslint-disable-next-line @typescript-eslint/no-unused-expressionschildren && children.length > 0 ? (item.children = fixMenuItemIcon(children)) : null;});return menus;
};export default fixMenuItemIcon;

这里二级菜单的图标没有,官方是这样解释的

最终实现的效果,先给管理员和普通和用户分配不同的菜单

image-20240521171759248

image-20240521171823525

切换登录不同角色的用户,会显示不同的菜单

hmgif2

实现过程中遇到的问题以及解决方法

  • 请问菜单从服务端获取,为什么还要在 routes 配置好全部,否则就不能正常解析 · Issue #11137 · ant-design/ant-design-pro (github.com)
  • 动态菜单实现后,路由是全部的,可以通过url跳转到不显示的菜单项BUG] · Issue #10728 · ant-design/ant-design-pro (github.com)

实现源码

https://gitee.com/szxio/user-center

函数式组件的父子组件方法互相调用

编写子组件 Child

注意:子组件需要使用 forwardRef 函数包裹,然后使用 useImperativeHandle 暴露属性和方法

import React, {forwardRef, useImperativeHandle} from 'react';
import {Button} from 'antd';// 子组件使用forwardRef函数包裹
// forwardRef函数接收两个参数,第一个参数是props,第二个参数是ref
const Child = forwardRef((props, ref) => {// 定义方法,将来由父组件调用const getChildStr = () => {console.log('子组件的getChildStr方法被触发');return '来自子组件的返回值';};const getParentFn = () => {// 调用父组件的方法props?.parentAddCount?.();};// useImperativeHandle函数接收两个参数,第一个参数是ref,第二个参数是一个函数// 这个函数返回一个对象,这个对象中的属性和方法会被暴露给父组件useImperativeHandle(ref, () => {return {getChildStr,};});return (<div className={'p-3 bg-amber-500'}><div>我是子组件</div><Button onClick={getParentFn}>调用父组件方法</Button></div>);
});export default Child;

编写父组件

import React, { useRef } from 'react';
import Child from '@/pages/test/Child';
import { Button } from 'antd';const Index = () => {let [count, setCount] = React.useState(0);let childRef = useRef();// 提供给子组件调用的方法,子组件使用 prop.parentAddCount() 实现调用父组件的方法const addCount = () => {setCount(count + 1);};// 调用子组件方法,使用 childRef.current 获取子组件暴露的属性和方法const getChildStr = () => {let childStr = childRef.current?.getChildStr();console.log(childStr);};return (<><Child ref={childRef} parentAddCount={addCount} /><div style={{ marginTop: 20 }}>count:{count}</div><Button onClick={getChildStr}>调用子组件的方法</Button></>);
};export default Index;

效果展示

image-20240518215529255

图表 Ant Design Charts

官网地址

·可视化组件库 | AntV (antgroup.com)

快速上手

安装

我们提供了 Ant Design 的 npm 包,通过下面的命令即可完成安装:

npm install @ant-design/charts --save#yarn
yarn add @ant-design/charts --save#pnpm
pnpm add @ant-design/charts --save

成功安装完成之后,即可使用 importrequire 进行引用:

import { Line } from '@ant-design/charts';

在需求明确的情况下,也可仅引入相关子包

# 统计图表
npm install @ant-design/plots --save

Java操作Word文档

poi-tl介绍

官方文档:https://deepoove.com/poi-tl/

poi-tl(poi template language)是Word模板引擎,使用模板和数据创建很棒的Word文档

在文档的任何地方做任何事情(Do Anything Anywhere)是poi-tl的星辰大海。

方案移植性功能性易用性
Poi-tlJava跨平台Word模板引擎,基于Apache POI,提供更友好的API低代码,准备文档模板和数据即可
Apache POIJava跨平台Apache项目,封装了常见的文档操作,也可以操作底层XML结构文档不全,这里有一个教程:Apache POI Word快速入门
FreemarkerXML跨平台仅支持文本,很大的局限性不推荐,XML结构的代码几乎无法维护
OpenOffice部署OpenOffice,移植性较差-需要了解OpenOffice的API
HTML浏览器导出依赖浏览器的实现,移植性较差HTML不能很好的兼容Word的格式,样式糟糕-
Jacob、winlibWindows平台-复杂,完全不推荐使用

poi-tl是一个基于Apache POI的Word模板引擎,也是一个免费开源的Java类库,你可以非常方便的加入到你的项目中,并且拥有着让人喜悦的特性。

Word模板引擎功能描述
文本将标签渲染为文本
图片将标签渲染为图片
表格将标签渲染为表格
列表将标签渲染为列表
图表条形图(3D条形图)、柱形图(3D柱形图)、面积图(3D面积图)、折线图(3D折线图)、雷达图、饼图(3D饼图)、散点图等图表渲染
If Condition判断根据条件隐藏或者显示某些文档内容(包括文本、段落、图片、表格、列表、图表等)
Foreach Loop循环根据集合循环某些文档内容(包括文本、段落、图片、表格、列表、图表等)
Loop表格行循环复制渲染表格的某一行
Loop表格列循环复制渲染表格的某一列
Loop有序列表支持有序列表的循环,同时支持多级列表
Highlight代码高亮word中代码块高亮展示,支持26种语言和上百种着色样式
Markdown将Markdown渲染为word文档
Word批注完整的批注功能,创建批注、修改批注等
Word附件Word中插入附件
SDT内容控件内容控件内标签支持
Textbox文本框文本框内标签支持
图片替换将原有图片替换成另一张图片
书签、锚点、超链接支持设置书签,文档内锚点和超链接功能
Expression Language完全支持SpringEL表达式,可以扩展更多的表达式:OGNL, MVEL…
样式模板即样式,同时代码也可以设置样式
模板嵌套模板包含子模板,子模板再包含子模板
合并Word合并Merge,也可以在指定位置进行合并
用户自定义函数(插件)插件化设计,在文档任何位置执行函数

快速上手

Maven

<dependency><groupId>com.deepoove</groupId><artifactId>poi-tl</artifactId><version>1.12.2</version>
</dependency>

准备一个模板文件,占位符使用双大括号占位

你好,我是{{name}},今年{{age}}

然后将模板放在 resources 目录下,编写代码

@Test
void test1() {// 定义模板对应的数据HashMap<String, Object> data = new HashMap<>();data.put("name", "张三");data.put("age", 18);// 加载本地模板文件InputStream inputStream = getClass().getResourceAsStream("/演示模板1.docx");// 渲染模板XWPFTemplate template = XWPFTemplate.compile(inputStream).render(data);try {// 写出到文件template.writeAndClose(new FileOutputStream("output.docx"));} catch (IOException e) {throw new RuntimeException(e);}
}

效果展示

image-20240523094721108

加载远程模板文件

在实际业务场景中,模板可能会有很多,并且不会保存在本地,这时就需要加载远程模板来进行处理

下面是示例代码

@Test
void test2() {try {// 加载远程模板String templateUrl ="https://szx-bucket1.oss-cn-hangzhou.aliyuncs.com/picgo/%E6%BC%94%E7%A4%BA%E6%A8%A1%E6%9D%BF1.docx";URL url = new URL(templateUrl);HttpURLConnection conn = (HttpURLConnection) url.openConnection();InputStream inputStream = conn.getInputStream();// 定义模板对应的数据HashMap<String, Object> data = new HashMap<>();data.put("name", "张三");data.put("age", 18);// 渲染模板XWPFTemplate template = XWPFTemplate.compile(inputStream).render(data);// 写出到文件template.writeAndClose(new FileOutputStream("output2.docx"));} catch (Exception e) {throw new RuntimeException(e);}
}

编写接口返回处理后的文件

下面我们来实现编写一个接口,前端访问时携带参数,后端完成编译后返回文件给前端下载

@Api(tags = "模板管理")
@RestController
@RequestMapping("/word")
public class WordController {@GetMapping("getWord")public void getWord(String name, Integer age, HttpServletResponse response) {// 定义模板对应的数据HashMap<String, Object> data = new HashMap<>();data.put("name", name);data.put("age", age);// 加载本地模板文件InputStream inputStream = getClass().getResourceAsStream("/演示模板1.docx");// 渲染模板XWPFTemplate template = XWPFTemplate.compile(inputStream).render(data);// 设置响应头,指定文件类型和内容长度response.setContentType("application/vnd.openxmlformats-officedocument.wordprocessingml.document");response.setHeader("Content-Disposition", "attachment; filename=output.docx");// 将生成的文件直接写出到HTTP响应输出流OutputStream outputStream = null;try {outputStream = response.getOutputStream();template.write(outputStream);outputStream.flush();// 关闭资源template.close();outputStream.close();} catch (IOException e) {throw new RuntimeException(e);}}
}

前端代码编写

定义接口地址,并且请求中声明 responseType

import { request } from '@umijs/max';// 下载报告
export async function getWordFun(age, name) {return request(`/word/getWord?age=${age}&name=${name}`, {method: 'get',responseType: 'blob', // 使用blob下载});
}

然后响应拦截器中判断 responseType

requestErrorConfig.ts

/*** @name 错误处理* pro 自带的错误处理, 可以在这里做自己的改动* @doc https://umijs.org/docs/max/request#配置*/
export const errorConfig: RequestConfig = {// 响应拦截器responseInterceptors: [(response) => {// 拦截响应数据,进行个性化处理const res = response as unknown as ResponseStructure;// 判断流数据if (res.request.responseType === 'blob') {return response;}// 判断状态码if (!sucCodes.includes(res.data?.code)) {return Promise.reject(res.data);}return response;},],
};

编写页面代码

import React from 'react';
import { ProForm, ProFormDigit, ProFormText } from '@ant-design/pro-components';
import { getWordFun } from '@/services/ant-design-pro/reportApi';const Report = () => {const onFinish = async (values) => {let res = await getWordFun(values.age, values.name);// 接收流文件数据并下载const blob = new Blob([res], {type: res.type,});const link = document.createElement('a');link.href = URL.createObjectURL(blob);link.download = 'test.docx';link.click();};return (<><ProForm title="新建表单" onFinish={onFinish}><ProFormText name="name" label="名称" placeholder="请输入名称" /><ProFormDigit type={'number'} name="age" label="年龄" placeholder="请输入年龄" /></ProForm></>);
};export default Report;

image-20240523132308219

下载的文件内容

image-20240523101613378

图片

图片标签以@开始:{{@var}}

@Test
void test3() {// 定义模板对应的数据HashMap<String, Object> data = new HashMap<>();data.put("name", "张三");data.put("age", 18);data.put("img", Pictures.ofUrl("http://deepoove.com/images/icecream.png").size(100, 100).create());// 加载本地模板文件InputStream inputStream = getClass().getResourceAsStream("/演示模板1.docx");// 渲染模板XWPFTemplate template = XWPFTemplate.compile(inputStream).render(data);try {// 写出到文件template.writeAndClose(new FileOutputStream("output.docx"));} catch (IOException e) {throw new RuntimeException(e);}
}

image-20240523134209524

image-20240523134123298

表格

表格标签以#开始:{{#var}}

// 插入表格
@Test
void test4() {// 定义模板对应的数据HashMap<String, Object> data = new HashMap<>();data.put("name", "张三");data.put("age", 18);data.put("img", Pictures.ofUrl("http://deepoove.com/images/icecream.png").size(100, 100).create());// 第0行居中且背景为蓝色的表格RowRenderData row0 =Rows.of("学历", "时间").textColor("FFFFFF").bgColor("4472C4").center().create();RowRenderData row1 = Rows.create("本科", "2015~2019");RowRenderData row2 = Rows.create("研究生", "2019~2021");data.put("eduList", Tables.create(row0, row1, row2));// 加载本地模板文件InputStream inputStream = getClass().getResourceAsStream("/演示模板1.docx");// 渲染模板XWPFTemplate template = XWPFTemplate.compile(inputStream).render(data);try {// 写出到文件template.writeAndClose(new FileOutputStream("output.docx"));} catch (IOException e) {throw new RuntimeException(e);}
}

image-20240523135141764

image-20240523135112583

表格行循环

我们希望根据一个集合的内容来决定表格的行数,这是就用到表格行循环

货物明细需要展示所有货物,{{goods}} 是个标准的标签,将 {{goods}} 置于循环行的上一行,循环行设置要循环的标签和内容,注意此时的标签应该使用 [] ,以此来区别poi-tl的默认标签语法。

示例代码

// 循环行表格
@Test
void test5() {Good good = new Good();good.setName("小米14");good.setPrice("4599");good.setColor("黑色");good.setTime("2024-05-23");Good good2 = new Good();good2.setName("苹果15");good2.setPrice("7599");good2.setColor("黑色");good2.setTime("2024-05-23");Good good3 = new Good();good3.setName("华为Meta60");good3.setPrice("7999");good3.setColor("白色");good3.setTime("2024-05-23");ArrayList<Good> goods = new ArrayList<>();goods.add(good);goods.add(good2);goods.add(good3);// 定义模板对应的数据HashMap<String, Object> data = new HashMap<>();data.put("name", "张三");data.put("age", 18);data.put("img", Pictures.ofUrl("http://deepoove.com/images/icecream.png").size(100, 100).create());// 第0行居中且背景为蓝色的表格RowRenderData row0 =Rows.of("学历", "时间").textColor("FFFFFF").bgColor("4472C4").center().create();RowRenderData row1 = Rows.create("本科", "2015~2019");RowRenderData row2 = Rows.create("研究生", "2019~2021");data.put("eduList", Tables.create(row0, row1, row2));// 添加采购列表数据data.put("goods", goods);// 加载本地模板文件InputStream inputStream = getClass().getResourceAsStream("/演示模板1.docx");// 定义行循环插件LoopRowTableRenderPolicy policy = new LoopRowTableRenderPolicy();// 绑定插件Configure config = Configure.builder().bind("goods", policy).build();// 渲染模板XWPFTemplate template = XWPFTemplate.compile(inputStream, config).render(data);try {// 写出到文件template.writeAndClose(new FileOutputStream("output.docx"));} catch (IOException e) {throw new RuntimeException(e);}
}@Data
public class Good {private String name;private String price;private String color;private String time;
}

image-20240523142219092

项目线上部署

Docker部署

首先编写Dockerfile

Java的Dockerfile

方式一:基于已经打包的jar包编写DockerFile

从阿里镜像获取源地址,以获取更快的下载速度

访问:https://cr.console.aliyun.com/cn-hangzhou/instances/artifact

image-20240525171839394

image-20240525171950744

# 可以从阿里云的容器镜像服务中 找到openjdk,选择相对应的版本
FROM anolis-registry.cn-zhangjiakou.cr.aliyuncs.com/openanolis/openjdk:8-8.6# 这里就是进入创建好的目录
WORKDIR /app# 将打包后的jar包复制到指定目录(这里我是复制到了创建好的工作目录)下,并重命名
COPY ./user-center-0.0.1-SNAPSHOT.jar ./user-center-0.0.1-SNAPSHOT.jar# 运行命令
CMD ["java","-jar","/app/user-center-0.0.1-SNAPSHOT.jar","--spring.profiles.active=prod"]

方式二:只上传代码,其他都交给Docker

FROM maven:3.8.1-jdk-8-slim as builderWORKDIR /app# 复制代码到容器
COPY pom.xml .
COPY src ./src# 打包并跳过Test检查
RUN mvn package -DskipTestsCMD ["java","-jar","/app/target/user-center-0.0.1-SNAPSHOT.jar","--spring.profiles.active=prod"]

构建镜像

将Dockerfile和源码放在平级,然后运行下面命令构建镜像

docker build -t user-center:1.0.0 .

image-20240525181335994

启动镜像

docker run -d --name=user-center -p 8080:8080 user-center:1.0.0
前端Dockerfile

方式一:在镜像中进行打包

参考文章:https://blog.51cto.com/u_16099258/10476241

编写 Dockerfile

# 第一阶段:构建前端产出物
FROM node:20.11.1 AS builderWORKDIR /visualization
COPY . .
RUN npm install -g pnpm --registry=https://registry.npmmirror.com/
RUN pnpm install && pnpm run build# 第二阶段:生成最终容器映像
FROM nginxCOPY docker/nginx.conf /etc/nginx/conf.d/default.conf
COPY docker/docker-entrypoint.sh /docker-entrypoint.shWORKDIR /home/visualization
COPY --from=builder /visualization/dist .RUN chmod +x /docker-entrypoint.sh

image-20240526112246307

在根目录新建 docker 文件夹,放两个文件

1、新建nginx.conf文件,用于配置前端项目访问nginx配置文件
2、新建docker-entrypoint.sh文件,执行脚本动态修改nginx.conf中的代理请求地址

nginx.conf内容
~根据项目情况做出修改,gzip配置前端无则可删除
~ /dev是前端代理跨域的基准地址,要保持统一,代理到后端的地址,做代理的目的是后面可以根据容器run动态改变proxy_pass地址
~如果项目无https则可删除443监听

~有https则需要配置证书ssl_certificate、ssl_certificate_key,此文件的路径为后面 运行容器时(run) -v将宿主机的目录映射至容器,就是容器的目录

新建nginx.conf文件

server {listen 80;server_name  localhost;# gzip config
#     gzip off;
#     gzip_min_length 1k;
#     gzip_comp_level 9;
#     gzip_types text/plain text/css text/javascript application/json application/javascript application/x-javascript application/xml;
#     gzip_vary off;
#     gzip_disable "MSIE [1-6]\.";#location / {root   /home/visualization;index  index.html index.htm;try_files $uri $uri/ /index.html;}location ^~/api/ {# 代理proxy_pass http://cx5k97.natappfree.cc/;access_log /var/log/nginx/dev_access.log;error_log /var/log/nginx/dev_error.log;}
}

新建docker-entrypoint.sh文件

#!/usr/bin/env bashAPI_BASE_PATH=$API_BASE_PATH;
if [ -z "$API_BASE_PATH" ]; thenAPI_BASE_PATH="https://xxx.xxx/";
fiapiUrl="proxy_pass  $API_BASE_PATH;"
sed -i '22c '"$apiUrl"'' /etc/nginx/conf.d/default.conf
sed -i '75c '"$apiUrl"'' /etc/nginx/conf.d/default.conf# 变量CERT判断是否需要证书https, $CERT存在则不需要
certOr="#"
if [ -n "$CERT" ]; thensed -i '45c '"$certOr"'' /etc/nginx/conf.d/default.confsed -i '46c '"$certOr"'' /etc/nginx/conf.d/default.confsed -i '60c '"$certOr"'' /etc/nginx/conf.d/default.confsed -i '61c '"$certOr"'' /etc/nginx/conf.d/default.conf
finginx -g "daemon off;"

然后在根目录新建 .dockerignore,忽略文件

# Dependency directory
# https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git
node_modules
.DS_Store
dist# node-waf configuration
.lock-wscript# Compiled binary addons (http://nodejs.org/api/addons.html)
build/Release
.dockerignore
Dockerfile
*docker-compose*# Logs
logs
*.log# Runtime data
.idea
.vscode
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw*
pids
*.pid
*.seed
.git
.hg
.svn

构建结果

image-20240526093128275

运行

//方式一:
// contanier_hello为容器名称
// -p 9090:80  将容器里面的80端口映射到宿主机的8080端口,80端口就是nginx里面配置,多个端口多个配置,必须确保服务器已经开了此端口
docker run -d --name user-center-web -p 8000:80 user-center-web:1.0.0//方式二:
// 运行容器的时候改变nginx代理地址
// -e API_BASE_PATH就是上面sh文件中定义的变量 把nginx的后端接口地址改为http://www.baidu.com,这个地址一定不要格式错误,不然nginx会解析不出来docker run -d --name user-center-web -p 80:80 -e "API_BASE_PATH=http://8g6igw.natappfree.cc/" user-center-web:1.0.0

相关文章:

React + SpringBoot开发用户中心管理系统

用户中心项目搭建笔记 技术栈 前端技术栈 “react”: “^18.2.0”,ant-design-pro 后端技术栈 SpringBoot 2.6.x 项目源码地址 https://gitee.com/szxio/user-center 前端项目搭建 快速搭建一个后端管理系统项目框架 初始化 antDesignPro 官网&#xff1a; https://…...

移动机器人定位与导航实训记录

本次实训主要学习ros-tf的使用、slam使用、机器人自主导航&#xff0c;我先简单发出来&#xff0c;等我整理完再重新编辑一边。...

彩灯控制器设计 74ls160+ne555实现

一、选题背景 数字电子技术在我们生活中的应用非常之广泛,不论是在各个方面都会涉及到它,小到家用电器的自动控制,大到神舟九号和天空一号航天器的设计,都无可避免的要运用它。并且鉴于以理论推动实践及理论实践相结合为指导思想,特此用我们所学的理论知识来实践这次课程设…...

Windows API 速查

Windows API 函数大全 (推荐)&#xff1a;https://blog.csdn.net/xiao_yi_xiao/article/details/121604742Windows API 在线参考手册&#xff1a;http://www.office-cn.net/t/api/index.html?web.htmWindows 开发文档 (官方)&#xff1a;https://learn.microsoft.com/zh-cn/wi…...

智能名片小程序源码系统平台版 人人可创建属于自己的名片 前后端分离 带完整的源代码以及搭建教程

系统概述 智能名片小程序源码系统平台版是一款基于微信小程序的个性化名片搭建平台。该平台采用前后端分离的设计架构&#xff0c;前端提供丰富的界面元素和灵活的布局方式&#xff0c;后端则提供强大的数据支持和功能扩展能力。用户无需具备专业的编程知识&#xff0c;只需按…...

香橙派OrangePI AiPro测评 【运行qt,编解码,xfreeRDP】

实物 为AI而生 打开盒子 配置 扛把子的 作为业界首款基于昇腾深度研发的AI开发板&#xff0c;Orange Pi AIpro无论在外观上、性能上还是技术服务支持上都非常优秀。采用昇腾AI技术路线&#xff0c;集成图形处理器&#xff0c;拥有8GB/16GB LPDDR4X&#xff0c;可以外接32…...

重生之我要精通JAVA--第七周笔记

文章目录 IO流字符流字符流原理解析flush和close方法 文件拷贝代码文件加密解密修改文件中的数据 缓冲流字节缓冲流字符缓冲流例题 转换流序列化流序列化流/对象操作输出流 反序列化流序列化流/反序列化流的细节汇总打印流字节打印流字符打印流 解压缩流压缩流Commons-io常见方…...

MySQL—函数—数值函数(基础)

一、引言 首先了解一下常见的数值函数哪些&#xff1f;并且直到它们的作用&#xff0c;并且演示这些函数的使用。 二、数值函数 常见的数值函数如下&#xff1a; 注意&#xff1a; 1、ceil(x)、floor(x) &#xff1a;向上、向下取整。 2、mod(x,y)&#xff1a;模运算&#x…...

fintuning chatglm3

chatglm3介绍 ChatGLM3-6B 是 ChatGLM 系列最新一代的开源模型&#xff0c;在保留了前两代模型对话流畅、部署门槛低等众多优秀特性的基础上&#xff0c;ChatGLM3-6B 引入了如下特性&#xff1a; 更强大的基础模型&#xff1a; ChatGLM3-6B 的基础模型 ChatGLM3-6B-Base 采用…...

草台班子啊草台班子:共享电源导致的BUG(供电不足)

某日吧&#xff08;其实就是今日&#xff0c;不过什么时候我又删帖重发也不一定啊&#xff09;&#xff0c;下工厂干活&#xff0c;机器里面没多的插座&#xff08;其实一个插座都没有&#xff0c;但是有一个24V电源的的设备&#xff09;&#xff0c;于是带队的下令并着接&…...

java递归计算文件夹和文件大小

背景 背景发现电脑c盘占用过高,然而我却不清楚是哪些文件占用了磁盘空间,于是我希望用程序来帮我完成这件事。小插曲:开始的时候,我使用python来做的,结果发现效率实在是太低,最后用java重写了一波。有需要的同学可以拿去修改一些。 代码 import java.io.File; import ja…...

硬币检测电路设计

一、来源&#xff1a;凡亿教育 第一场&#xff1a;硬币检测装置原理分析、电路设计以及器件选型_哔哩哔哩_bilibilihttps://www.bilibili.com/video/BV1Zh4y1V7Px/?p1&vd_source43eb1cb50ad3175d7f3b9385905cd88f 二、开发软件&#xff1a;KEIL MDK 三、主控芯片&#…...

3.基础光照

从宏观上讲渲染包含了两大部分&#xff1a;决定一个像素的可见性&#xff0c;决定一个像素的光照计算 而光照模型就是用于决定在一个像素上决定怎样的渲染光照计算。 一、我们是如何看到这个世界的 1.光源 实时渲染中&#xff0c;我们通常把光源当成一个没有体积的点&#…...

Image Search-这是你的图像搜索

Image Search-这是你的图像搜索 什么是图像搜索图像搜索开通图像搜索商品图片搜索图片搜索图片新增批量操作OSS-创建bucket上传文件创建increment.meta并上传元信息导出 体验感受 什么是图像搜索 在接触一个新的产品时&#xff0c;我们首先要知道这款产品是什么&#xff1f;那…...

对GPT-4o的评价:技术革新与未来展望

目录 引言一、GPT-4o的技术背景1.1 GPT系列的发展历程1.2 GPT-4o的技术特点 二、版本间的对比分析2.1 GPT-3与GPT-4的对比2.2 GPT-4与GPT-4o的对比 三、GPT-4o的技术能力3.1 自然语言处理3.2 多模态处理3.3 任务定制化 四、个人整体感受4.1 交互体验4.2 应用场景4.3 未来展望 五…...

【TB作品】msp430f5529单片机,dht22,烟雾传感器

功能 //硬件&#xff1a;msp430f5529、dht22、LCD1602、蜂鸣器、烟雾传感器、蓝牙模块。 //功能&#xff1a;读取温湿度、烟雾浓度显示到屏幕&#xff1b; //按键调节三个报警数值&#xff1b; //温度、湿度、烟雾浓度&#xff0c;任意一个大于报警数值就蜂鸣器报警&#xff1…...

uni-app全局弹窗的实现方案

背景 为了解决uni-app 任意位置出现弹窗 解决方案 一、最初方案 受限于uni-app 调用组件需要每个页面都引入注册才可以使用&#xff0c;此方案繁琐&#xff0c;每个页面都要写侵入性比较强 二、改进方案 app端&#xff1a;新建一个页面进行跳转&#xff0c;可以实现伪弹窗…...

Love-Yi情侣网站3.0存在SQL注入漏洞

目录 1. 前言 2. 网站简介 3. 寻找特征点 3.1 第一次尝试 3.2 第二次尝试 4.资产搜索 5.漏洞复现 5.1 寻找漏洞点 5.2 进行进一步测试 5.2.1 手动测试 1.寻找字段 2.寻找回显位 3.查询当前用户 5.2.2 sqlmap去跑 6.总结 1. 前言 朋友说自己建了一个情侣网站,看到…...

自然语言处理(NLP)—— 神经网络语言处理

1. 总体原则 1.1 深度神经网络&#xff08;Deep Neural Network&#xff09;的训练过程 下图展示了自然语言处理&#xff08;NLP&#xff09;领域内使用的深度神经网络&#xff08;Deep Neural Network&#xff09;的训练过程的简化图。 在神经网络的NLP领域&#xff1a; 语料…...

SHA256计算原理

标签: SHA256计算原理;SHA256;SHA-2; SHA-256计算原理 SHA-256(Secure Hash Algorithm 256-bit)是SHA-2系列中的一种哈希算法,它由美国国家安全局(NSA)设计,并由美国国家标准与技术研究院(NIST)发布。SHA-256主要用于数据完整性验证和数字签名等领域。以下是SHA-…...

树莓派超全系列教程文档--(62)使用rpicam-app通过网络流式传输视频

使用rpicam-app通过网络流式传输视频 使用 rpicam-app 通过网络流式传输视频UDPTCPRTSPlibavGStreamerRTPlibcamerasrc GStreamer 元素 文章来源&#xff1a; http://raspberry.dns8844.cn/documentation 原文网址 使用 rpicam-app 通过网络流式传输视频 本节介绍来自 rpica…...

什么是EULA和DPA

文章目录 EULA&#xff08;End User License Agreement&#xff09;DPA&#xff08;Data Protection Agreement&#xff09;一、定义与背景二、核心内容三、法律效力与责任四、实际应用与意义 EULA&#xff08;End User License Agreement&#xff09; 定义&#xff1a; EULA即…...

ardupilot 开发环境eclipse 中import 缺少C++

目录 文章目录 目录摘要1.修复过程摘要 本节主要解决ardupilot 开发环境eclipse 中import 缺少C++,无法导入ardupilot代码,会引起查看不方便的问题。如下图所示 1.修复过程 0.安装ubuntu 软件中自带的eclipse 1.打开eclipse—Help—install new software 2.在 Work with中…...

[Java恶补day16] 238.除自身以外数组的乘积

给你一个整数数组 nums&#xff0c;返回 数组 answer &#xff0c;其中 answer[i] 等于 nums 中除 nums[i] 之外其余各元素的乘积 。 题目数据 保证 数组 nums之中任意元素的全部前缀元素和后缀的乘积都在 32 位 整数范围内。 请 不要使用除法&#xff0c;且在 O(n) 时间复杂度…...

Android Bitmap治理全解析:从加载优化到泄漏防控的全生命周期管理

引言 Bitmap&#xff08;位图&#xff09;是Android应用内存占用的“头号杀手”。一张1080P&#xff08;1920x1080&#xff09;的图片以ARGB_8888格式加载时&#xff0c;内存占用高达8MB&#xff08;192010804字节&#xff09;。据统计&#xff0c;超过60%的应用OOM崩溃与Bitm…...

selenium学习实战【Python爬虫】

selenium学习实战【Python爬虫】 文章目录 selenium学习实战【Python爬虫】一、声明二、学习目标三、安装依赖3.1 安装selenium库3.2 安装浏览器驱动3.2.1 查看Edge版本3.2.2 驱动安装 四、代码讲解4.1 配置浏览器4.2 加载更多4.3 寻找内容4.4 完整代码 五、报告文件爬取5.1 提…...

分布式增量爬虫实现方案

之前我们在讨论的是分布式爬虫如何实现增量爬取。增量爬虫的目标是只爬取新产生或发生变化的页面&#xff0c;避免重复抓取&#xff0c;以节省资源和时间。 在分布式环境下&#xff0c;增量爬虫的实现需要考虑多个爬虫节点之间的协调和去重。 另一种思路&#xff1a;将增量判…...

网站指纹识别

网站指纹识别 网站的最基本组成&#xff1a;服务器&#xff08;操作系统&#xff09;、中间件&#xff08;web容器&#xff09;、脚本语言、数据厍 为什么要了解这些&#xff1f;举个例子&#xff1a;发现了一个文件读取漏洞&#xff0c;我们需要读/etc/passwd&#xff0c;如…...

脑机新手指南(七):OpenBCI_GUI:从环境搭建到数据可视化(上)

一、OpenBCI_GUI 项目概述 &#xff08;一&#xff09;项目背景与目标 OpenBCI 是一个开源的脑电信号采集硬件平台&#xff0c;其配套的 OpenBCI_GUI 则是专为该硬件设计的图形化界面工具。对于研究人员、开发者和学生而言&#xff0c;首次接触 OpenBCI 设备时&#xff0c;往…...

在 Spring Boot 项目里,MYSQL中json类型字段使用

前言&#xff1a; 因为程序特殊需求导致&#xff0c;需要mysql数据库存储json类型数据&#xff0c;因此记录一下使用流程 1.java实体中新增字段 private List<User> users 2.增加mybatis-plus注解 TableField(typeHandler FastjsonTypeHandler.class) private Lis…...