做册子模板素材有哪些网站,静态门户网站源码,wordpress html插件,wordpress后台改成中文用户中心项目搭建笔记
技术栈
前端技术栈
“react”: “^18.2.0”,ant-design-pro
后端技术栈
SpringBoot 2.6.x
项目源码地址
https://gitee.com/szxio/user-center
前端项目搭建
快速搭建一个后端管理系统项目框架
初始化
antDesignPro 官网#xff1a; https://…用户中心项目搭建笔记
技术栈
前端技术栈
“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访问 后端项目搭建
初始化
使用idea开发工具自带 Spring Initializr 完成项目创建 如果Java版本无法选中8可以切换上面的 Server URL 为阿里的源 https://start.aliyun.com然后就可以选择8版本了 接着点击 Next选择常用的开发依赖下面我列出一些基本的依赖
?xml version1.0 encodingUTF-8?
project xmlnshttp://maven.apache.org/POM/4.0.0 xmlns:xsihttp://www.w3.org/2001/XMLSchema-instancexsi:schemaLocationhttp://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsdmodelVersion4.0.0/modelVersiongroupIdcom.szx/groupIdartifactIduser-center/artifactIdversion0.0.1-SNAPSHOT/versionnameuser-center/namedescriptionuser-center/descriptionpropertiesjava.version1.8/java.versionproject.build.sourceEncodingUTF-8/project.build.sourceEncodingproject.reporting.outputEncodingUTF-8/project.reporting.outputEncodingspring-boot.version2.6.13/spring-boot.version/propertiesdependenciesdependencygroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-web/artifactId/dependencydependencygroupIdorg.mybatis.spring.boot/groupIdartifactIdmybatis-spring-boot-starter/artifactIdversion2.2.2/version/dependencydependencygroupIdorg.projectlombok/groupIdartifactIdlombok/artifactIdoptionaltrue/optional/dependencydependencygroupIdmysql/groupIdartifactIdmysql-connector-java/artifactId/dependencydependencygroupIdcom.baomidou/groupIdartifactIdmybatis-plus-boot-starter/artifactIdversion3.4.2/version/dependencydependencygroupIdjunit/groupIdartifactIdjunit/artifactIdversion4.12/version/dependencydependencygroupIdcn.hutool/groupIdartifactIdhutool-all/artifactIdversion5.8.26/version/dependency!--swagger--dependencygroupIdio.springfox/groupIdartifactIdspringfox-boot-starter/artifactIdversion3.0.0/version/dependencydependencygroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-test/artifactIdscopetest/scope/dependency/dependenciesdependencyManagementdependenciesdependencygroupIdorg.springframework.boot/groupIdartifactIdspring-boot-dependencies/artifactIdversion${spring-boot.version}/versiontypepom/typescopeimport/scope/dependency/dependencies/dependencyManagementbuildpluginsplugingroupIdorg.apache.maven.plugins/groupIdartifactIdmaven-compiler-plugin/artifactIdversion3.8.1/versionconfigurationsource1.8/sourcetarget1.8/targetencodingUTF-8/encoding/configuration/pluginplugingroupIdorg.springframework.boot/groupIdartifactIdspring-boot-maven-plugin/artifactIdversion${spring-boot.version}/versionconfigurationmainClasscom.szx.usercenter.UserCenterApplication/mainClassskiptrue/skip/configurationexecutionsexecutionidrepackage/idgoalsgoalrepackage/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?useUnicodetruecharacterEncodingutf-8useSSLfalseserverTimezoneAsia/Shanghaiusername: rootpassword: abc123# 配置日期返回格式jackson:# 日期格式date-format: yyyy-MM-dd HH:mm:ss# 时区time-zone: GMT8# 非空的属性值才会被包含在结果中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.StdOutImplSwaggerUI配置
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自带的代码生成器 注意生成的文件会覆盖原有文件
统一结果返回类
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 ResponseT {private String code;private String msg;private T data;/*** title 成功消息* return*/public static T ResponseT success() {return rspMsg(ResponseEnum.SUCCESS);}/*** title 失败消息* return*/public static T ResponseT error() {return rspMsg(ResponseEnum.SERVER_INNER_ERR);}/*** title 自定义消息* return*/public static T ResponseT rspMsg(ResponseEnum responseEnum) {ResponseT message new ResponseT();message.setCode(responseEnum.getCode());message.setMsg(responseEnum.getMsg());return message;}/*** title 自定义消息* return*/public static T ResponseT rspMsg(String code , String msg) {ResponseT message new ResponseT();message.setCode(code);message.setMsg(msg);return message;}/*** title 返回数据* param data* return*/public static T ResponseT rspData(T data) {ResponseT responseData new ResponseT();responseData.setCode(ResponseEnum.SUCCESS.getCode());responseData.setData(data);return responseData;}public static T ResponseT error(T data) {ResponseT responseData new ResponseT();responseData.setCode(ResponseEnum.ERROR.getCode());responseData.setData(data);return responseData;}/*** title 返回数据-自定义code* param data* return*/public static T ResponseT rspData(String code , T data) {ResponseT responseData new ResponseT();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 ResponseListSysUser 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页面试试 至此后端项目搭建完成
打包pom通用配置
buildpluginsplugingroupIdorg.apache.maven.plugins/groupIdartifactIdmaven-compiler-plugin/artifactIdversion3.8.1/version !-- 或者你使用的版本 --configurationsource1.8/sourcetarget1.8/target/configuration/pluginplugingroupIdorg.springframework.boot/groupIdartifactIdspring-boot-maven-plugin/artifactIdversion2.3.7.RELEASE/versionexecutionsexecutiongoalsgoalrepackage/goal/goals/execution/executions/plugin/plugins
/buildSQL建表语句
可以复制若依的表来使用下面是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)
) engineinnodb auto_increment100 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)
) engineinnodb auto_increment100 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)
) engineinnodb 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)
) engineinnodb 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);}
}后端功能开发
注册逻辑
用户名不能有特殊字符并且必须超过6位数密码必须超过6位数用户名不能重复密码加密后保存到数据库中
注册接口开发
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 LambdaQueryWrapperSysUser().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(注册失败);
}登录逻辑
根据用户名获取数据库表中保存的用户信息在用传递进来的密码和表中的密码进行密码校验校验成功返回用户信息否则登录失败
登录接口开发
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 LambdaQueryWrapperSysUser().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 LambdaQueryWrapperSysUser().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 ResponseObject error(Exception e){log.error(ExceptionUtil.getMessage(e));e.printStackTrace();// 将异常转成string返回出去return Response.error(e.getMessage());}/*** 处理自定义的异常-CenterExceptionHandler* param e* return*/ExceptionHandler(CenterExceptionHandler.class)ResponseBodypublic ResponseObject 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(测试异常);
}查询接口开发
接口实现类 SysUserServiceImpl 添加方法
// 用户信息脱敏方法
Override
public SysUser getSefUser(SysUser user) {SysUser sysUser ObjUtil.clone(user);sysUser.setPassword(null);return sysUser;
}Override
public Response getPageUserList(SysUser sysUser) {PageSysUser sysUserPage new Page(sysUser.getCurrent(), sysUser.getPageSize());LambdaQueryWrapperSysUser 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);// 返回的用户信息脱敏ListSysUser 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 类型默认会出现一个类型转换错误的异常如下图 前端传递的参数 我们可以修改配置文件增加一个日期转换格式的配置
spring:# 配置日期返回格式jackson:# 日期格式date-format: yyyy-MM-dd HH:mm:ss# 时区time-zone: GMT8# 非空的属性值才会被包含在结果中default-property-inclusion: non_null重启项目再次查询就不会报错了 自动填充创建人和更新人
新建一个 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);}
}更新和创建时值自动填充 前端功能开发
登录逻辑梳理
首先找到登录页面对应的文件位置在src/pages/User/Login/index.tsx然后观察代码发现登录页面使用了 LoginForm 组件来实现的登录表单LoginForm 是从 ant-design/pro-components 中导出的ProComponents 是基于 Ant Design 而开发的模板组件提供了更高级别的抽象支持开箱即用。可以显著地提升制作 CRUD 页面的效率更加专注于页面。 loginForm组件使用文档https://pro-components.antdigital.dev/components/form
点击登录会触发onFinish钩子函数调用handleSubmit方法 login方法在src/services/ant-design-pro/api.ts 文件中声明 登录成功后调用 fetchUserInfo 方法获取用户信息 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?: PartialLayoutSettings;currentUser?: API.CurrentUser;loading?: boolean;fetchUserInfo?: () PromiseAPI.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 PartialLayoutSettings,};}return {fetchUserInfo,settings: defaultSettings as PartialLayoutSettings,};
}查看 queryCurrentUser 接口地址 找到mock中对应的接口 下面我们按照这种格式编写后端接口即可
修改响应拦截器
找到 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 {// 错误处理 umi3 的错误处理方案。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] useStatestring(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}Helmettitle{登录}- {Settings.title}/title/Helmet{/*忘记密码重置密码表单*/}{forgotPassword ForgotPasswordForm updatePasswordStatus{updatePasswordStatus} /}{!forgotPassword (div style{{ marginTop: 5% }}LoginFormcontentStyle{{minWidth: 280,maxWidth: 75vw,}}logo{img altlogo 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: 账户密码登录,},]}/ProFormTextnameusernamefieldProps{{size: large,prefix: UserOutlined /,}}placeholder{请输入用户名}rules{[{required: true,message: 用户名是必填项,},]}/ProFormText.PasswordnamepasswordfieldProps{{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 requestAPI.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 classNameant-pro-form-login-header style{{marginTop: 5%}}span classNameant-pro-form-login-logo img altlogo src/logo.svg//spanspan classNameant-pro-form-login-title 用户管理中心/span/divTabsactiveKey{account}centereditems{[{key: account,label: 重置密码,},]}/Formnamebasicstyle{{width: 328}}initialValues{{remember: false,}}layoutverticalonFinish{onFinish}autoCompleteoffForm.ItemlabelnameuserNamerules{[{required: true,message: 请输入用户名,},]}Inputsize{large}prefix{UserOutlined/}placeholder请输入用户名//Form.ItemForm.Itemlabelnamepasswordrules{[{required: true,message: 请输入密码!,},]}Input.Password size{large} prefix{LockOutlined/} placeholder请输入密码//Form.ItemForm.ItemlabelnamenewPasswordrules{[{required: true,message: 请确认密码!,},]}Input.Password size{large} prefix{LockOutlined/} placeholder请确认密码//Form.Itemdivstyle{{marginBottom: 24,}}astyle{{float: right,marginBottom: 20}}onClick{() props.updatePasswordStatus(false)}返回登录/a/divButton typeprimary htmlTypesubmit sizelarge block确认/Button/Form/div);
};export default ForgotPasswordForm;动态获取菜单
官方提供的动态菜单实现方法菜单的高级用法 - 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;这里二级菜单的图标没有官方是这样解释的
最终实现的效果先给管理员和普通和用户分配不同的菜单 切换登录不同角色的用户会显示不同的菜单 实现过程中遇到的问题以及解决方法
请问菜单从服务端获取为什么还要在 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我是子组件/divButton 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}/divButton onClick{getChildStr}调用子组件的方法/Button/);
};export default Index;效果展示 图表 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成功安装完成之后即可使用 import 或 require 进行引用
import { Line } from ant-design/charts;在需求明确的情况下也可仅引入相关子包
# 统计图表
npm install ant-design/plots --saveJava操作Word文档
poi-tl介绍
官方文档https://deepoove.com/poi-tl/
poi-tlpoi 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的APIHTML浏览器导出依赖浏览器的实现移植性较差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
dependencygroupIdcom.deepoove/groupIdartifactIdpoi-tl/artifactIdversion1.12.2/version
/dependency准备一个模板文件占位符使用双大括号占位
你好我是{{name}}今年{{age}}岁然后将模板放在 resources 目录下编写代码
Test
void test1() {// 定义模板对应的数据HashMapString, 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);}
}效果展示 加载远程模板文件
在实际业务场景中模板可能会有很多并且不会保存在本地这时就需要加载远程模板来进行处理
下面是示例代码
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();// 定义模板对应的数据HashMapString, 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) {// 定义模板对应的数据HashMapString, 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; filenameoutput.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 namename label名称 placeholder请输入名称 /ProFormDigit type{number} nameage label年龄 placeholder请输入年龄 //ProForm/);
};export default Report; 下载的文件内容 图片 图片标签以开始{{var}} Test
void test3() {// 定义模板对应的数据HashMapString, 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);}
}表格 表格标签以#开始{{#var}} // 插入表格
Test
void test4() {// 定义模板对应的数据HashMapString, 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);}
}表格行循环
我们希望根据一个集合的内容来决定表格的行数这是就用到表格行循环 货物明细需要展示所有货物{{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);ArrayListGood goods new ArrayList();goods.add(good);goods.add(good2);goods.add(good3);// 定义模板对应的数据HashMapString, 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;
}项目线上部署
Docker部署
首先编写Dockerfile
Java的Dockerfile
方式一基于已经打包的jar包编写DockerFile
从阿里镜像获取源地址以获取更快的下载速度
访问https://cr.console.aliyun.com/cn-hangzhou/instances/artifact # 可以从阿里云的容器镜像服务中 找到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.activeprod]方式二只上传代码其他都交给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.activeprod]构建镜像
将Dockerfile和源码放在平级然后运行下面命令构建镜像
docker build -t user-center:1.0.0 .启动镜像
docker run -d --nameuser-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 --registryhttps://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 --frombuilder /visualization/dist .RUN chmod x /docker-entrypoint.sh在根目录新建 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_PATHhttps://xxx.xxx/;
fiapiUrlproxy_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构建结果 运行
//方式一
// 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_PATHhttp://8g6igw.natappfree.cc/ user-center-web:1.0.0