项目介绍
目录结构
wk_crm
├── admin -- 系统管理模块和用户管理模块
├── authorization -- 鉴权模块,目前仅用于登录鉴权,后期可能有更改
├── bi -- 商业智能模块
├── core -- 通用的代码和工具类
├── crm -- 客户管理模块
├── examine -- 审批模块
├── gateway -- 网关模块
├── job -- 定时任务模块
├── oa -- OA模块
└── work -- 项目管理模块
└── hrm -- 人力资源管理模块
技术栈
| 名称 | 版本 | 说明 |
| spring-cloud-alibaba | 2.2.1.RELEASE(Hoxton.SR3) | 核心框架 |
| swagger | 2.9.2 | 接口文档 |
| mybatis-plus | 3.3.0 | ORM框架 |
| sentinel | 2.2.1.RELEASE | 断路器以及限流 |
| nacos | 1.2.1.RELEASE | 注册中心以及分布式配置管理 |
| seata | 1.2.0 | 分布式事务 |
| elasticsearch | 2.2.5.RELEASE(6.8.6) | 搜索引擎中间件 |
| jetcache | 2.6.0 | 分布式缓存框架 |
| xxl-job | 2.1.2 | 分布式定时任务框架 |
| gateway | 2.2.2.RELEASE | 微服务网关 |
| feign | 2.2.2.RELEASE | 服务调用 |
架构图

核心 core
自定义过滤器 包装请求体
解决 HTTP 请求体(Request Body)只能读取一次的问题
为什么需要这个过滤器
在 Servlet 规范中,HttpServletRequest 的请求体(InputStream)只能读取一次。一旦被读取,流就会被消耗掉,后续无法再次读取。这在某些场景下会造成问题:
- 日志记录:想要记录完整的请求内容
- 参数验证:需要验证请求参数
- 安全检查:需要检查请求内容是否存在安全风险
- 多次处理:多个过滤器或组件都需要访问请求体
过滤器的工作原理
- 拦截请求:过滤器拦截所有请求(
urlPatterns = "/*") - 检查内容类型:只处理 JSON 类型的请求(
Content-Type: application/json) - 缓存请求体:将请求体内容读取并保存到字节数组中
- 包装请求:创建一个
BodyReaderHttpServletRequestWrapper包装原始请求 - 提供可重读的流:包装器提供可以多次读取的输入流
源码
package com.kakarote.core.config;
import cn.hutool.extra.servlet.ServletUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.Ordered;
import org.springframework.stereotype.Component;
import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
@Component
@WebFilter(filterName = "bodyReaderFilter", urlPatterns = "/*")
@Slf4j
public class BodyReaderFilter implements Filter, Ordered {
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
ServletRequest requestWrapper = null;
if (servletRequest instanceof HttpServletRequest) {
HttpServletRequest request = ((HttpServletRequest) servletRequest);
if (request.getHeader("Content-Type") != null && request.getHeader("Content-Type").startsWith("application/json")) {
requestWrapper = new BodyReaderHttpServletRequestWrapper(request);
}
}
if (requestWrapper == null) {
filterChain.doFilter(servletRequest, servletResponse);
} else {
filterChain.doFilter(requestWrapper, servletResponse);
}
}
@Override
public int getOrder() {
return -9;
}
public class BodyReaderHttpServletRequestWrapper extends HttpServletRequestWrapper {
private byte[] body = null;
public BodyReaderHttpServletRequestWrapper(HttpServletRequest request) {
super(request);
body = ServletUtil.getBodyBytes(request);
}
@Override
public BufferedReader getReader() throws IOException {
return new BufferedReader(new InputStreamReader(getInputStream(), StandardCharsets.UTF_8));
}
@Override
public ServletInputStream getInputStream() throws IOException {
final ByteArrayInputStream bais = new ByteArrayInputStream(body);
return new ServletInputStream() {
@Override
public int read() throws IOException {
return bais.read();
}
@Override
public boolean isFinished() {
return bais.available() == 0;
}
@Override
public boolean isReady() {
return true;
}
@Override
public void setReadListener(ReadListener readListener) {
}
};
}
}
}
全局日期统一处理
当 Spring MVC 处理 HTTP 请求时,会自动使用这个转换器来将请求参数中的字符串转换为 Date 对象。
package com.kakarote.core.config;
import cn.hutool.core.date.DateUtil;
import org.springframework.core.convert.converter.Converter;
import org.springframework.lang.Nullable;
import org.springframework.stereotype.Component;
import java.util.Date;
/**
* 全局日期统一处理
* @author zhangzhiwei
*/
@Component
public class DateConverterConfig implements Converter<String, Date> {
@Override
public Date convert(@Nullable String dateStr) {
return DateUtil.parse(dateStr);
}
}
也有替代方案:
SpringBoot配置文件
spring:
jackson:
date-format: yyyy-MM-dd HH:mm:ss
time-zone: GMT+8
单个配置
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date birthDate;
重写ObjectMapper:不推荐,因为会完全替换
@Configuration
public class JacksonConfig {
@Bean
public ObjectMapper objectMapper() {
ObjectMapper mapper = new ObjectMapper();
mapper.setDateFormat(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
return mapper;
}
}
容器Bean工具类
解决非Bean中调用Bean对象
@Component
public class ApplicationContextHolder implements ApplicationContextAware {
private static ApplicationContext applicationContext;
@Override
public void setApplicationContext(@Nonnull ApplicationContext applicationContext) throws BeansException {
ApplicationContextHolder.applicationContext = applicationContext;
}
/**
* 全局的applicationContext对象
* @return applicationContext
*/
public static ApplicationContext getApplicationContext() {
return applicationContext;
}
@SuppressWarnings("unchecked")
public static <T> T getBean(String beanName) {
return (T) applicationContext.getBean(beanName);
}
public static <T> T getBean(Class<T> clazz) {
return applicationContext.getBean(clazz);
}
}
动态实现类
public class FileServiceFactory {
public static FileService build() {
UploadConfig uploadConfig = ApplicationContextHolder.getBean(UploadConfig.class);
// 配置类中取值
Integer config = uploadConfig.getConfig();
if (config.equals(UploadFileEnum.ALI_OSS.getConfig())) {
return new OssFileServiceImpl(uploadConfig.getOss());
} else if (config.equals(UploadFileEnum.LOCAL.getConfig())) {
return new LocalFileServiceImpl(uploadConfig.getLocal());
}else if(config.equals(UploadFileEnum.ALI_COS.getConfig())){
return new TencentFileServiceImpl(uploadConfig.getCos());
}else if(config.equals(UploadFileEnum.ALI_QNC.getConfig())){
return new QncFileServiceImpl(uploadConfig.getQnc());
}
return new LocalFileServiceImpl(uploadConfig.getLocal());
}
}
可用条件注解代替工厂类
@ConditionalOnProperty(name = "file.storage.type", havingValue = "local")
public interface LocalFileServiceImpl
网关 gateway
授权 authorization
Feign本地降级
如果有多个实现类,需要显示指定FallBack
调用Feign接口
@Component
@FeignClient(name = "admin")
public interface AdminUserService {
/**
* 通过用户名查询用户
*
* @param username 用户名
* @return 结果信息
*/
@RequestMapping(value = "/adminUser/findByUsername")
Result findByUsername(@RequestParam("username") String username);
}
如果Feign不可用,会调用本地接口
@Component
public class AdminUserServiceImpl implements AdminUserService {
/**
* 通过用户名查询用户
*
* @param username 用户名
* @return 结果信息
*/
@Override
public Result findByUsername(String username) {
return Result.error(SystemCodeEnum.SYSTEM_NO_FOUND);
}
}
ip2region
离线IP地址定位库和IP定位数据管理框架
https://gitee.com/lionsoul/ip2region
<dependency>
<groupId>org.lionsoul</groupId>
<artifactId>ip2region</artifactId>
<version>1.7.2</version>
</dependency>
private String getIpCityInfo1(String ip){
log.info("===> Login IP : {}",ip);
String dbPath = LoginLogUtil.class.getClassLoader().getResource("").getPath() + "ip2region/ip2region.db";
log.info("===> dbPath : {}",dbPath);
File file = new File(dbPath);
if (!file.exists()) {
return null;
}
try {
DbConfig config = new DbConfig();
DbSearcher searcher = new DbSearcher(config, dbPath);
Method method = searcher.getClass().getMethod("btreeSearch", String.class);
if (!Util.isIpAddress(ip)) {
return null;
}
DataBlock dataBlock = (DataBlock) method.invoke(searcher, ip);
return dataBlock.getRegion();
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
系统管理 admin
文件上传
- 上传文件,存储文件信息,返回文件id
- 前端携带token+文件id 通过流返回前端
/**
* 下载文件
*
* @param response response
* @param fileId fileId
*/
@Override
public void down(HttpServletResponse response, Long fileId) {
AdminFile adminFile = getById(fileId);
if (adminFile != null) {
if (Objects.equals(1, adminFile.getType())) {
boolean exist = FileUtil.exist(adminFile.getPath());
if (exist) {
ServletUtil.write(response, FileUtil.file(adminFile.getPath()));
}
return;
}
}
}
1万+

被折叠的 条评论
为什么被折叠?



