各位爷,完整项目gitee如下,求star
heima-leadnews-master: 《黑马头条》项目采用的是SpringBoot+springcloud当下最流行的微服务为项目架构,配合spring cloud alibaba nacos作为项目的注册和配置中心。新课程采用快速开发的模式,主要解决真实企业开发的一些应用场景。详情请看博客:https://blog.csdn.net/m0_67184231/article/details/131439819
随着智能手机的普及,人们更加习惯于通过手机来看新闻。由于生活节奏的加快,很多人只能利用碎片时间来获取信息,因此,对于移动资讯客户端的需求也越来越高。
黑马头条项目正是在这样背景下开发出来。黑马头条项目采用当下火热的微服务+大数据技术架构实现。本项目主要着手于获取最新最热新闻资讯,通过大数据分析用户喜好精确推送咨询新闻
项目演示地址:
平台管理:http://heima-admin-java.research.itcast.cn
自媒体:http://heime-media-java.research.itcast.cn
app端:http://heima-app-java.research.itcast.cn
平台管理与自媒体为PC端,用电脑浏览器打开即可。
其中app端为移动端,打开方式有两种:
谷歌浏览器打开,调成移动端模式
手机浏览器打开或扫描右侧二维码
Spring-Cloud-Gateway : 微服务之前架设的网关服务,实现服务注册中的API请求路由,以及控制流速控制和熔断处理都是常用的架构手段,而这些功能Gateway天然支持
运用Spring Boot快速开发框架,构建项目工程;并结合Spring Cloud全家桶技术,实现后端个人中心、自媒体、管理中心等微服务。
运用Spring Cloud Alibaba Nacos作为项目中的注册中心和配置中心
运用mybatis-plus作为持久层提升开发效率
运用Kafka完成内部系统消息通知;与客户端系统消息通知;以及实时数据计算
运用Redis缓存技术,实现热数据的计算,提升系统性能指标
使用Mysql存储用户数据,以保证上层数据查询的高性能
使用Mongo存储用户热数据,以保证用户热数据高扩展和高性能指标
使用FastDFS作为静态资源存储器,在其上实现热静态资源缓存、淘汰等功能
运用Hbase技术,存储系统中的冷数据,保证系统数据的可靠性
运用ES搜索技术,对冷数据、文章数据建立索引,以保证冷数据、文章查询性能
运用AI技术,来完成系统自动化功能,以提升效率及节省成本。比如实名认证自动化
PMD&P3C : 静态代码扫描工具,在项目中扫描项目代码,检查异常点、优化点、代码规范等,为开发团队提供规范统一,提升项目代码质量
1)打开当天资料文件中的镜像,拷贝到一个地方,然后解压
2)解压后,双击ContOS7-hmtt.vmx文件,前提是电脑上已经安装了VMware
修改虚拟网络地址(NAT)
①,选中VMware中的编辑
②,选择虚拟网络编辑器
③,找到NAT网卡,把网段改为200(当前挂载的虚拟机已固定ip地址)
4)修改虚拟机的网络模式为NAT
5)启动虚拟机,用户名:root 密码:itcast,当前虚拟机的ip已手动固定(静态IP), 地址为:192.168.200.130
6)使用FinalShell客户端链接
①:docker拉取镜像
docker pull nacos/nacos-server:1.2.0
②:创建容器
docker run --env MODE=standalone --name nacos --restart=always -d -p 8848:8848 nacos/nacos-server:1.2.0
MODE=standalone 单机版
--restart=always 开机启动
-p 8848:8848 映射端口
-d 创建一个守护式容器在后台运行
③:访问地址:http://192.168.200.130:8848/nacos
①:项目依赖环境(需提前安装好)
JDK1.8
Intellij Idea
maven-3.6.1
Git
②:在当天资料中解压heima-leadnews.zip文件,拷贝到 没有中文和空格的目录,使用idea打开即可
③:IDEA开发工具配置
设置本地仓库,建议使用资料中提供好的仓库
④:设置项目编码格式
户点击开始使用
登录后的用户权限较大,可以查 看,也可以操作(点赞,关注,评论)
用户点击不登录,先看看
游客只有查看的权限
关于app端用户相关的内容较多,可以单独设置一个库leadnews_user
表名称 | 说明 |
ap_user | APP用户信息表 |
ap_user_fan | APP用户粉丝信息表 |
ap_user_follow | APP用户关注信息表 |
ap_user_realname | APP实名认证信息表 |
从当前资料中找到对应数据库并导入到mysql中
登录需要用到的是ap_user表,表结构如下:
项目中的持久层使用的mybatis-plus,一般都使用mybais-plus逆向生成对应的实体类
app_user表对应的实体类如下:
package com.heima.model.user.pojos; import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import lombok.Data; import java.io.Serializable; import java.util.Date; /** ** APP用户信息表 *
* * @author itheima */ @Data @TableName("ap_user") public class ApUser implements Serializable { private static final long serialVersionUID = 1L; /** * 主键 */ @TableId(value = "id", type = IdType.AUTO) private Integer id; /** * 密码、通信等加密盐 */ @TableField("salt") private String salt; /** * 用户名 */ @TableField("name") private String name; /** * 密码,md5加密 */ @TableField("password") private String password; /** * 手机号 */ @TableField("phone") private String phone; /** * 头像 */ @TableField("image") private String image; /** * 0 男 1 女 2 未知 */ @TableField("sex") private Boolean sex; /** * 0 未 1 是 */ @TableField("is_certification") private Boolean certification; /** * 是否身份认证 */ @TableField("is_identity_authentication") private Boolean identityAuthentication; /** * 0正常 1锁定 */ @TableField("status") private Boolean status; /** * 0 普通用户 1 自媒体人 2 大V */ @TableField("flag") private Short flag; /** * 注册时间 */ @TableField("created_time") private Date createdTime; }
手动加密(md5+随机字符串)
md5是不可逆加密,md5相同的密码每次加密都一样,不太安全。在md5的基础上手动加盐(salt)处理
注册->生成盐
登录->使用盐来配合验证
1,用户输入了用户名和密码进行登录,校验成功后返回jwt(基于当前用户的id生成)
2,用户游客登录,生成jwt返回(基于默认值0生成)‘
在heima-leadnews-service下创建工程heima-leadnews-user
引导类
package com.heima.user; import org.mybatis.spring.annotation.MapperScan; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.client.discovery.EnableDiscoveryClient; @SpringBootApplication @EnableDiscoveryClient//集成当前注册中心 @MapperScan("com.heima.user.mapper")//扫描mapper public class UserApplication { public static void main(String[] args) { SpringApplication.run(UserApplication.class,args); } }
bootstrap.yml
server: port: 51801 spring: application: name: leadnews-user cloud: nacos: discovery: server-addr: 192.168.200.130:8848 config: server-addr: 192.168.200.130:8848 file-extension: yml
在nacos中创建配置文件
spring: datasource: driver-class-name: com.mysql.jdbc.Driver url: jdbc:mysql://localhost:3306/leadnews_user?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC username: root password: root # 设置Mapper接口所对应的XML文件位置,如果你在Mapper接口中有自定义方法,需要进行该配置 mybatis-plus: mapper-locations: classpath*:mapper/*.xml # 设置别名包扫描路径,通过该属性可以给包中的类注册别名 type-aliases-package: com.heima.model.user.pojos
The last packet successfully received from the server was 523 milliseconds ago. The last packet sent successfully to the server was 518 milliseconds ago.at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
at java.lang.reflect.Constructor.newInstance(Constructor.java:423)
at com.mysql.jdbc.Util.handleNewInstance(Util.java:425)
可以把nacos的mysqlurl改成下面
url: jdbc:mysql://localhost:3306/leadnews_user?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC&useSSL=false
logback.xml
%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n utf8 ${LOG_HOME}/leadnews.%d{yyyy-MM-dd}.log %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n 0 512
①:接口定义
@RestController @RequestMapping("/api/v1/login") public class ApUserLoginController { @PostMapping("/login_auth") public ResponseResult login(@RequestBody LoginDto dto) { return null; } }
②:持久层mapper
package com.heima.user.mapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.heima.model.user.pojos.ApUser; import org.apache.ibatis.annotations.Mapper; @Mapper public interface ApUserMapper extends BaseMapper { }
③:业务层service
package com.heima.user.service; import com.baomidou.mybatisplus.extension.service.IService; import com.heima.model.common.dtos.ResponseResult; import com.heima.model.user.dtos.LoginDto; import com.heima.model.user.pojos.ApUser; public interface ApUserService extends IService{ /** * app端登录 * @param dto * @return */ public ResponseResult login(LoginDto dto); }
实现类:
package com.heima.user.service.impl; import com.baomidou.mybatisplus.core.toolkit.Wrappers; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.heima.model.common.dtos.ResponseResult; import com.heima.model.common.enums.AppHttpCodeEnum; import com.heima.model.user.dtos.LoginDto; import com.heima.model.user.pojos.ApUser; import com.heima.user.mapper.ApUserMapper; import com.heima.user.service.ApUserService; import com.heima.utils.common.AppJwtUtil; import org.apache.commons.lang3.StringUtils; import org.springframework.stereotype.Service; import org.springframework.util.DigestUtils; import java.util.HashMap; import java.util.Map; @Service public class ApUserServiceImpl extends ServiceImpl implements ApUserService { @Override public ResponseResult login(LoginDto dto) { //1.正常登录(手机号+密码登录) if (!StringUtils.isBlank(dto.getPhone()) && !StringUtils.isBlank(dto.getPassword())) { //1.1查询用户 ApUser apUser = getOne(Wrappers.lambdaQuery().eq(ApUser::getPhone, dto.getPhone())); if (apUser == null) { return ResponseResult.errorResult(AppHttpCodeEnum.DATA_NOT_EXIST,"用户不存在"); } //1.2 比对密码 String salt = apUser.getSalt(); String pswd = dto.getPassword(); pswd = DigestUtils.md5DigestAsHex((pswd + salt).getBytes()); if (!pswd.equals(apUser.getPassword())) { return ResponseResult.errorResult(AppHttpCodeEnum.LOGIN_PASSWORD_ERROR); } //1.3 返回数据 jwt Mapmap = new HashMap<>(); map.put("token", AppJwtUtil.getToken(apUser.getId().longValue())); //这两个值 置空 再返回对象 apUser.setSalt(""); apUser.setPassword(""); map.put("user", apUser); return ResponseResult.okResult(map); } else { //2.游客 同样返回token id = 0 Map map = new HashMap<>(); map.put("token", AppJwtUtil.getToken(0l)); return ResponseResult.okResult(map); } } }
④:控制层controller
package com.heima.user.controller.v1; import com.heima.model.common.dtos.ResponseResult; import com.heima.model.user.dtos.LoginDto; import com.heima.user.service.ApUserService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping("/api/v1/login") public class ApUserLoginController { @Autowired private ApUserService apUserService; @PostMapping("/login_auth") public ResponseResult login(@RequestBody LoginDto dto) { return apUserService.login(dto); } }
docker exec -it redis redis-cli这个进入服务器得redis界面 你再输入docker ps就能看到服务起来了;docker 中的redis没密码,要把nacos的密码去掉;
报错的兄弟们先创建redis容器a然后把nacos配置中redis密码那一行注掉就行了
docker run --name redis -p 6379:6379 -d redis
Postman是一款功能强大的网页调试与发送网页HTTP请求的Chrome插件。postman被500万开发者和超100,000家公司用于每月访问1.3亿个API。
官方网址:Postman
解压资料文件夹中的软件,安装即可
通常的接口测试查看请求和响应,下面是登录请求的测试
http://localhost:51801/api/v1/login/login_auth
{"phone":"13511223456","password":"admin"}
(1)简介
Swagger 是一个规范和完整的框架,用于生成、描述、调用和可视化 RESTful 风格的 Web 服务(API Documentation & Design Tools for Teams | Swagger)。 它的主要作用是:
使得前后端分离开发更加方便,有利于团队协作
接口的文档在线自动生成,降低后端开发人员编写接口文档的负担
功能测试
Spring已经将Swagger纳入自身的标准,建立了Spring-swagger项目,现在叫Springfox。
通过在项目中引入Springfox ,即可非常简单快捷的使用Swagger。
(2)SpringBoot集成Swagger
引入依赖,在heima-leadnews-model和heima-leadnews-common模块中引入该依赖
io.springfox springfox-swagger2io.springfox springfox-swagger-ui
只需要在heima-leadnews-common中进行配置即可,因为其他微服务工程都直接或间接依赖即可。
在heima-leadnews-common工程中添加一个配置类
新增:com.heima.common.swagger.SwaggerConfiguration
package com.heima.common.swagger; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import springfox.documentation.builders.ApiInfoBuilder; import springfox.documentation.builders.PathSelectors; import springfox.documentation.builders.RequestHandlerSelectors; import springfox.documentation.service.ApiInfo; import springfox.documentation.service.Contact; import springfox.documentation.spi.DocumentationType; import springfox.documentation.spring.web.plugins.Docket; import springfox.documentation.swagger2.annotations.EnableSwagger2; @Configuration @EnableSwagger2 public class SwaggerConfiguration { @Bean public Docket buildDocket() { return new Docket(DocumentationType.SWAGGER_2) .apiInfo(buildApiInfo()) .select() // 要扫描的API(Controller)基础包 .apis(RequestHandlerSelectors.basePackage("com.heima")) .paths(PathSelectors.any()) .build(); } private ApiInfo buildApiInfo() { Contact contact = new Contact("黑马程序员","",""); return new ApiInfoBuilder() .title("黑马头条-平台管理API文档") .description("黑马头条后台api") .contact(contact) .version("1.0.0").build(); } }
在heima-leadnews-common模块中的resources目录中新增以下目录和文件
文件:resources/META-INF/Spring.factories
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ com.heima.common.swagger.SwaggerConfiguration
(3)Swagger常用注解
在Java类中添加Swagger的注解即可生成Swagger接口文档,常用Swagger注解如下:
@Api:修饰整个类,描述Controller的作用
@ApiOperation:描述一个类的一个方法,或者说一个接口
@ApiParam:单个参数的描述信息
@ApiModel:用对象来接收参数
@ApiModelProperty:用对象接收参数时,描述对象的一个字段
@ApiResponse:HTTP响应其中1个描述
@ApiResponses:HTTP响应整体描述
@ApiIgnore:使用该注解忽略这个API
@ApiError :发生错误返回的信息
@ApiImplicitParam:一个请求参数
@ApiImplicitParams:多个请求参数的描述信息
@ApiImplicitParam属性:
属性 | 取值 | 作用 |
paramType | 查询参数类型 | |
path | 以地址的形式提交数据 | |
query | 直接跟参数完成自动映射赋值 | |
body | 以流的形式提交 仅支持POST | |
header | 参数在request headers 里边提交 | |
form | 以form表单的形式提交 仅支持POST | |
dataType | 参数的数据类型 只作为标志说明,并没有实际验证 | |
Long | ||
String | ||
name | 接收参数名 | |
value | 接收参数的意义描述 | |
required | 参数是否必填 | |
true | 必填 | |
false | 非必填 | |
defaultValue | 默认值 |
我们在ApUserLoginController中添加Swagger注解,代码如下所示:
@RestController @RequestMapping("/api/v1/login") @Api(value = "app端用户登录", tags = "ap_user", description = "app端用户登录API") public class ApUserLoginController { @Autowired private ApUserService apUserService; @PostMapping("/login_auth") @ApiOperation("用户登录") public ResponseResult login(@RequestBody LoginDto dto){ return apUserService.login(dto); } }
LoginDto
@Data public class LoginDto { /** * 手机号 */ @ApiModelProperty(value="手机号",required = true) private String phone; /** * 密码 */ @ApiModelProperty(value="密码",required = true) private String password; }
启动user微服务,访问地址:http://localhost:51801/swagger-ui.html
(1)简介
knife4j是为Java MVC框架集成Swagger生成Api文档的增强解决方案,前身是swagger-bootstrap-ui,取名kni4j是希望它能像一把匕首一样小巧,轻量,并且功能强悍!
gitee地址:knife4j: Knife4j是一个集Swagger2 和 OpenAPI3为一体的增强解决方案
官方文档:Knife4j · 集Swagger2及OpenAPI3为一体的增强解决方案. | Knife4j
效果演示:http://knife4j.xiaominfo.com/doc.html
(2)核心功能
该UI增强包主要包括两大核心功能:文档说明 和 在线调试
文档说明:根据Swagger的规范说明,详细列出接口文档的说明,包括接口地址、类型、请求示例、请求参数、响应示例、响应参数、响应码等信息,使用swagger-bootstrap-ui能根据该文档说明,对该接口的使用情况一目了然。
在线调试:提供在线接口联调的强大功能,自动解析当前接口参数,同时包含表单验证,调用参数可返回接口响应内容、headers、Curl请求命令实例、响应时间、响应状态码等信息,帮助开发者在线调试,而不必通过其他测试工具测试接口是否正确,简介、强大。
个性化配置(Swaager无):通过个性化ui配置项,可自定义UI的相关显示信息
离线文档(Swaager无):根据标准规范,生成的在线markdown离线文档,开发者可以进行拷贝生成markdown接口文档,通过其他第三方markdown转换工具转换成html或pdf,这样也可以放弃swagger2markdown组件
接口排序:自1.8.5后,ui支持了接口排序功能,
例如一个注册功能主要包含了多个步骤,可以根据swagger-bootstrap-ui提供的接口排序规则实现接口的排序,step化接口操作,方便其他开发者进行接口对接
(3)快速集成
在heima-leadnews-common模块中的pom.xml文件中引入knife4j的依赖,如下:
com.github.xiaoymin knife4j-spring-boot-starter
创建Swagger配置文件
在heima-leadnews-common模块中新建配置类
新建Swagger的配置文件SwaggerConfiguration.java文件,创建springfox提供的Docket分组对象,代码如下:
package com.heima.common.knife4j; import com.github.xiaoymin.knife4j.spring.annotations.EnableKnife4j; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; import springfox.bean.validators.configuration.BeanValidatorPluginsConfiguration; import springfox.documentation.builders.ApiInfoBuilder; import springfox.documentation.builders.PathSelectors; import springfox.documentation.builders.RequestHandlerSelectors; import springfox.documentation.service.ApiInfo; import springfox.documentation.spi.DocumentationType; import springfox.documentation.spring.web.plugins.Docket; import springfox.documentation.swagger2.annotations.EnableSwagger2; @Configuration @EnableSwagger2 @EnableKnife4j @Import(BeanValidatorPluginsConfiguration.class) public class Swagger2Configuration { @Bean(value = "defaultApi2") public Docket defaultApi2() { Docket docket=new Docket(DocumentationType.SWAGGER_2) .apiInfo(apiInfo()) //分组名称 .groupName("1.0") .select() //这里指定Controller扫描包路径 .apis(RequestHandlerSelectors.basePackage("com.heima")) .paths(PathSelectors.any()) .build(); return docket; } private ApiInfo apiInfo() { return new ApiInfoBuilder() .title("黑马头条API文档") .description("黑马头条API文档") .version("1.0") .build(); } }
以上有两个注解需要特别说明,如下表:
注解 | 说明 |
@EnableSwagger2 | 该注解是Springfox-swagger框架提供的使用Swagger注解,该注解必须加 |
@EnableKnife4j | 该注解是knife4j提供的增强注解,Ui提供了例如动态参数、参数过滤、接口排序等增强功能,如果你想使用这些增强功能就必须加该注解,否则可以不用加 |
添加配置
在Spring.factories中新增配置
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ com.heima.common.swagger.Swagger2Configuration, \ com.heima.common.swagger.SwaggerConfiguration
访问
在浏览器输入地址:http://host:port/doc.html;http://localhost:51801/doc.html
(1)在heima-leadnews-gateway导入以下依赖
pom文件
org.springframework.cloud spring-cloud-starter-gatewaycom.alibaba.cloud spring-cloud-starter-alibaba-nacos-discoverycom.alibaba.cloud spring-cloud-starter-alibaba-nacos-configio.jsonwebtoken jjwt
(2)在heima-leadnews-gateway下创建heima-leadnews-app-gateway微服务
引导类:
package com.heima.app.gateway; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.client.discovery.EnableDiscoveryClient; @SpringBootApplication @EnableDiscoveryClient //开启注册中心 public class AppGatewayApplication { public static void main(String[] args) { SpringApplication.run(AppGatewayApplication.class,args); } }
bootstrap.yml
server: port: 51601 spring: application: name: leadnews-app-gateway cloud: nacos: discovery: server-addr: 192.168.200.130:8848 config: server-addr: 192.168.200.130:8848 file-extension: yml
在nacos的配置中心创建dataid为leadnews-app-gateway的yml配置
spring: cloud: gateway: globalcors: add-to-simple-url-handler-mapping: true corsConfigurations: '[/**]': allowedHeaders: "*" allowedOrigins: "*" allowedMethods: - GET - POST - DELETE - PUT - OPTION routes: # 平台管理 - id: user uri: lb://leadnews-user predicates: - Path=/user/** filters: - StripPrefix= 1
环境搭建完成以后,启动项目网关和用户两个服务,使用postman进行测试
请求地址:http://localhost:51601/user/api/v1/login/login_auth
思路分析:
用户进入网关开始登陆,网关过滤器进行判断,如果是登录,则路由到后台管理微服务进行登录
用户登录成功,后台管理微服务签发JWT TOKEN信息返回给用户
用户再次进入网关开始访问,网关过滤器接收用户携带的TOKEN
网关过滤器解析TOKEN ,判断是否有权限,如果有,则放行,如果没有则返回未认证错误
具体实现:
第一:
在认证过滤器中需要用到jwt的解析,所以需要把工具类拷贝一份到网关微服务
第二:
在网关微服务中新建全局过滤器:
package com.heima.app.gateway.filter; import com.heima.app.gateway.util.AppJwtUtil; import io.jsonwebtoken.Claims; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang.StringUtils; import org.springframework.cloud.gateway.filter.GatewayFilterChain; import org.springframework.cloud.gateway.filter.GlobalFilter; import org.springframework.core.Ordered; import org.springframework.http.HttpStatus; import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.http.server.reactive.ServerHttpResponse; import org.springframework.stereotype.Component; import org.springframework.web.server.ServerWebExchange; import reactor.core.publisher.Mono; @Component @Slf4j public class AuthorizeFilter implements Ordered, GlobalFilter { @Override public Monofilter(ServerWebExchange exchange, GatewayFilterChain chain) { //1.获取request和response对象 ServerHttpRequest request = exchange.getRequest(); ServerHttpResponse response = exchange.getResponse(); //2.判断是否是登录 if(request.getURI().getPath().contains("/login")){ //放行 return chain.filter(exchange); } //3.获取token String token = request.getHeaders().getFirst("token"); //4.判断token是否存在 if(StringUtils.isBlank(token)){ response.setStatusCode(HttpStatus.UNAUTHORIZED); return response.setComplete(); } //5.判断token是否有效 try { Claims claimsBody = AppJwtUtil.getClaimsBody(token); //是否是过期 int result = AppJwtUtil.verifyToken(claimsBody); if(result == 1 || result == 2){ response.setStatusCode(HttpStatus.UNAUTHORIZED); return response.setComplete(); } }catch (Exception e){ e.printStackTrace(); response.setStatusCode(HttpStatus.UNAUTHORIZED); return response.setComplete(); } //6.放行 return chain.filter(exchange); } /** * 优先级设置 值越小 优先级越高 * @return */ @Override public int getOrder() { return 0; } }
测试:
启动user服务,继续访问其他微服务,会提示需要认证才能访问,这个时候需要在heads中设置设置token才能正常访问。
通过nginx来进行配置,功能如下
通过nginx的反向代理功能访问后台的网关资源
通过nginx的静态服务器功能访问前端静态页面
①:解压资料文件夹中的压缩包nginx-1.18.0.zip
路径cmd,键入nginx,启动
②:解压资料文件夹中的前端项目app-web.zip
③:配置nginx.conf文件
在nginx安装的conf目录下新建一个文件夹leadnews.conf,在当前文件夹中新建heima-leadnews-app.conf文件
heima-leadnews-app.conf配置如下:
upstream heima-app-gateway{ server localhost:51601; } server { listen 8801; location / { root D:/workspace/app-web/; index index.html; } location ~/app/(.*) { proxy_pass http://heima-app-gateway/; proxy_set_header HOST $host; # 不改变源请求头的值 proxy_pass_request_body on; #开启获取请求体 proxy_pass_request_headers on; #开启获取请求头 proxy_set_header X-Real-IP $remote_addr; # 记录真实发出请求的客户端IP proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; #记录代理信息 } }
nginx.conf 把里面注释的内容和静态资源配置相关删除,引入heima-leadnews-app.conf文件加载
#user nobody; worker_processes 1; events { worker_connections 1024; } http { include mime.types; default_type application/octet-stream; sendfile on; keepalive_timeout 65; # 引入自定义配置文件 include leadnews.conf/*.conf; }
④ :启动nginx
在nginx安装包中使用命令提示符打开,输入命令nginx启动项目
可查看进程,检查nginx是否启动
重新加载配置文件:nginx -s reload
使用windows版本的nginx启动时遇到(1113: No mapping for the Unicode character exists in the target multi-byte code page)这个错误
把nginx的版本升高了,依旧报错
后来查阅发现是因为解压的路径里面包含有中文的缘故,只要把解压后的文件剪切到没有包含中文的目录即可解决问题
在运行reload的时候报错:CreateFile() "D:\DevelopCode\JavaCode\Springcloud\leadnews\nginx-1.18.0/logs/nginx.pid" failed (2: The system cannot find the file specified)
加pid的文件,然后里面没东西运行失败,就start nginx,计算机自己在pid加了东西,成功reload!
打开8801 报错:500 Internal Server Error
这个错误通常表示在Windows操作系统上,Nginx无法识别路径中含有非英文字符的文件或文件夹。这可能是因为Nginx默认将其配置文件和日志文件等保存在UTF-8编码下,而Windows默认的编码方式是ANSI。要解决这个问题,可以尝试以下方法:
将Nginx配置文件及相关路径修改为英文字符,避免包含非英文字符的路径。
修改Nginx的配置文件编码为 ANSI :在Nginx的配置文件中,找到 http 块,并在该块中添加以下指令:
plaintext复制代码http {
# ...
charset utf-8;
charset_types text/plain text/css application/javascript application/json text/javascript;
charset utf-8 off;
# ...
}
保存并重新启动Nginx服务。
⑤:打开前端项目进行测试 -- > http://localhost:8801
用谷歌浏览器打开,调试移动端模式进行访问
文章布局展示
ap_article 文章基本信息表
ap_article_config 文章配置表
ap_article_content 文章内容表
三张表关系分析
查看当天资料文件夹,在数据库连接工具中执行leadnews_article.sql
ap_article文章表对应实体
package com.heima.model.article.pojos; import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import lombok.Data; import java.io.Serializable; import java.util.Date; /** ** 文章信息表,存储已发布的文章 *
* * @author itheima */ @Data @TableName("ap_article") public class ApArticle implements Serializable { @TableId(value = "id",type = IdType.ID_WORKER) private Long id; /** * 标题 */ private String title; /** * 作者id */ @TableField("author_id") private Long authorId; /** * 作者名称 */ @TableField("author_name") private String authorName; /** * 频道id */ @TableField("channel_id") private Integer channelId; /** * 频道名称 */ @TableField("channel_name") private String channelName; /** * 文章布局 0 无图文章 1 单图文章 2 多图文章 */ private Short layout; /** * 文章标记 0 普通文章 1 热点文章 2 置顶文章 3 精品文章 4 大V 文章 */ private Byte flag; /** * 文章封面图片 多张逗号分隔 */ private String images; /** * 标签 */ private String labels; /** * 点赞数量 */ private Integer likes; /** * 收藏数量 */ private Integer collection; /** * 评论数量 */ private Integer comment; /** * 阅读数量 */ private Integer views; /** * 省市 */ @TableField("province_id") private Integer provinceId; /** * 市区 */ @TableField("city_id") private Integer cityId; /** * 区县 */ @TableField("county_id") private Integer countyId; /** * 创建时间 */ @TableField("created_time") private Date createdTime; /** * 发布时间 */ @TableField("publish_time") private Date publishTime; /** * 同步状态 */ @TableField("sync_status") private Boolean syncStatus; /** * 来源 */ private Boolean origin; /** * 静态页面地址 */ @TableField("static_url") private String staticUrl; }
ap_article_config文章配置对应实体类
package com.heima.model.article.pojos; import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import lombok.Data; import java.io.Serializable; /** ** APP已发布文章配置表 *
* * @author itheima */ @Data @TableName("ap_article_config") public class ApArticleConfig implements Serializable { @TableId(value = "id",type = IdType.ID_WORKER) private Long id; /** * 文章id */ @TableField("article_id") private Long articleId; /** * 是否可评论 * true: 可以评论 1 * false: 不可评论 0 */ @TableField("is_comment") private Boolean isComment; /** * 是否转发 * true: 可以转发 1 * false: 不可转发 0 */ @TableField("is_forward") private Boolean isForward; /** * 是否下架 * true: 下架 1 * false: 没有下架 0 */ @TableField("is_down") private Boolean isDown; /** * 是否已删除 * true: 删除 1 * false: 没有删除 0 */ @TableField("is_delete") private Boolean isDelete; }
ap_article_content 文章内容对应的实体类
package com.heima.model.article.pojos; import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import lombok.Data; import java.io.Serializable; @Data @TableName("ap_article_content") public class ApArticleContent implements Serializable { @TableId(value = "id",type = IdType.ID_WORKER) private Long id; /** * 文章id */ @TableField("article_id") private Long articleId; /** * 文章内容 */ private String content; }
1,在默认频道展示10条文章信息
2,可以切换频道查看不同种类文章
3,当用户下拉可以加载最新的文章(分页)本页文章列表中发布时间为最大的时间为依据
4,当用户上拉可以加载更多的文章信息(按照发布时间)本页文章列表中发布时间最小的时间为依据
5,如果是当前频道的首页,前端传递默认参数:
maxBehotTime:0(毫秒)
minBehotTime:20000000000000(毫秒)--->2063年
加载首页 | 加载更多 | 加载最新 | |
接口路径 | /api/v1/article/load | /api/v1/article/loadmore | /api/v1/article/loadnew |
请求方式 | POST | POST | POST |
参数 | ArticleHomeDto | ArticleHomeDto | ArticleHomeDto |
响应结果 | ResponseResult | ResponseResult | ResponseResult |
ArticleHomeDto
package com.heima.model.article.dtos; import lombok.Data; import java.util.Date; @Data public class ArticleHomeDto { // 最大时间 Date maxBehotTime; // 最小时间 Date minBehotTime; // 分页size Integer size; // 频道ID String tag; }
注意:需要在heima-leadnews-service的pom文件夹中添加子模块信息,如下:
heima-leadnews-user heima-leadnews-article
在idea中的maven中更新一下,如果工程还是灰色的,需要在重新添加文章微服务的pom文件,操作步骤如下:
需要在nacos中添加对应的配置
spring: datasource: driver-class-name: com.mysql.jdbc.Driver url: jdbc:mysql://localhost:3306/leadnews_article?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC username: root password: root # 设置Mapper接口所对应的XML文件位置,如果你在Mapper接口中有自定义方法,需要进行该配置 mybatis-plus: mapper-locations: classpath*:mapper/*.xml # 设置别名包扫描路径,通过该属性可以给包中的类注册别名 type-aliases-package: com.heima.model.article.pojos
package com.heima.article.controller.v1; import com.heima.model.article.dtos.ArticleHomeDto; import com.heima.model.common.dtos.ResponseResult; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping("/api/v1/article") public class ArticleHomeController { @PostMapping("/load") public ResponseResult load(@RequestBody ArticleHomeDto dto) { return null; } @PostMapping("/loadmore") public ResponseResult loadMore(@RequestBody ArticleHomeDto dto) { return null; } @PostMapping("/loadnew") public ResponseResult loadNew(@RequestBody ArticleHomeDto dto) { return null; } }
mybatisPlus对多表查询不太友好,所以用mybatis自定义mapper查询
package com.heima.article.mapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.heima.model.article.dtos.ArticleHomeDto; import com.heima.model.article.pojos.ApArticle; import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Param; import java.util.List; @Mapper public interface ApArticleMapper extends BaseMapper { public List loadArticleList(@Param("dto") ArticleHomeDto dto, @Param("type") Short type); }
对应的映射文件
在resources中新建mapper/ApArticleMapper.xml 如下配置:
package com.heima.article.service; import com.baomidou.mybatisplus.extension.service.IService; import com.heima.model.article.dtos.ArticleHomeDto; import com.heima.model.article.pojos.ApArticle; import com.heima.model.common.dtos.ResponseResult; import java.io.IOException; public interface ApArticleService extends IService { /** * 根据参数加载文章列表 * @param loadtype 1为加载更多 2为加载最新 * @param dto * @return */ ResponseResult load(Short loadtype, ArticleHomeDto dto); }
实现类:
package com.heima.article.service.impl; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.heima.article.mapper.ApArticleMapper; import com.heima.article.service.ApArticleService; import com.heima.common.constants.ArticleConstants; import com.heima.model.article.dtos.ArticleHomeDto; import com.heima.model.article.pojos.ApArticle; import com.heima.model.common.dtos.ResponseResult; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.util.Date; import java.util.List; @Service @Transactional @Slf4j public class ApArticleServiceImpl extends ServiceImpl implements ApArticleService { // 单页最大加载的数字 private final static short MAX_PAGE_SIZE = 50; @Autowired private ApArticleMapper apArticleMapper; /** * 根据参数加载文章列表 * @param loadtype 1为加载更多 2为加载最新 * @param dto * @return */ @Override public ResponseResult load(Short loadtype, ArticleHomeDto dto) { //1.校验参数 Integer size = dto.getSize(); if(size == null || size == 0){ size = 10; } size = Math.min(size,MAX_PAGE_SIZE); dto.setSize(size); //类型参数检验 if(!loadtype.equals(ArticleConstants.LOADTYPE_LOAD_MORE)&&!loadtype.equals(ArticleConstants.LOADTYPE_LOAD_NEW)){ loadtype = ArticleConstants.LOADTYPE_LOAD_MORE; } //文章频道校验 if(StringUtils.isEmpty(dto.getTag())){ dto.setTag(ArticleConstants.DEFAULT_TAG); } //时间校验 if(dto.getMaxBehotTime() == null) dto.setMaxBehotTime(new Date()); if(dto.getMinBehotTime() == null) dto.setMinBehotTime(new Date()); //2.查询数据 List apArticles = apArticleMapper.loadArticleList(dto, loadtype); //3.结果封装 ResponseResult responseResult = ResponseResult.okResult(apArticles); return responseResult; } }
定义常量类
package com.heima.common.constants; public class ArticleConstants { public static final Short LOADTYPE_LOAD_MORE = 1; public static final Short LOADTYPE_LOAD_NEW = 2; public static final String DEFAULT_TAG = "__all__"; }
package com.heima.article.controller.v1; import com.heima.article.service.ApArticleService; import com.heima.common.constants.ArticleConstants; import com.heima.model.article.dtos.ArticleHomeDto; import com.heima.model.common.dtos.ResponseResult; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping("/api/v1/article") public class ArticleHomeController { @Autowired private ApArticleService apArticleService; @PostMapping("/load") public ResponseResult load(@RequestBody ArticleHomeDto dto) { return apArticleService.load(ArticleConstants.LOADTYPE_LOAD_MORE,dto); } @PostMapping("/loadmore") public ResponseResult loadMore(@RequestBody ArticleHomeDto dto) { return apArticleService.load(ArticleConstants.LOADTYPE_LOAD_MORE,dto); } @PostMapping("/loadnew") public ResponseResult loadNew(@RequestBody ArticleHomeDto dto) { return apArticleService.load(ArticleConstants.LOADTYPE_LOAD_NEW,dto); } }
第一:在app网关的微服务的nacos的配置中心添加文章微服务的路由,完整配置如下:
spring: cloud: gateway: globalcors: cors-configurations: '[/**]': # 匹配所有请求 allowedOrigins: "*" #跨域处理 允许所有的域 allowedMethods: # 支持的方法 - GET - POST - PUT - DELETE routes: # 用户微服务 - id: user uri: lb://leadnews-user predicates: - Path=/user/** filters: - StripPrefix= 1 # 文章微服务 - id: article uri: lb://leadnews-article predicates: - Path=/article/** filters: - StripPrefix= 1
第二:启动nginx,直接使用前端项目测试,启动文章微服务,用户微服务、app网关微服务
FreeMarker 是一款 模板引擎: 即一种基于模板和要改变的数据, 并用来生成输出文本(HTML网页,电子邮件,配置文件,源代码等)的通用工具。 它不是面向最终用户的,而是一个Java类库,是一款程序员可以嵌入他们所开发产品的组件。
模板编写为FreeMarker Template Language (FTL)。它是简单的,专用的语言, 不是 像PHP那样成熟的编程语言。 那就意味着要准备数据在真实编程语言中来显示,比如数据库查询和业务运算, 之后模板显示已经准备好的数据。在模板中,你可以专注于如何展现数据, 而在模板之外可以专注于要展示什么数据。
常用的java模板引擎还有哪些?
Jsp、Freemarker、Thymeleaf 、Velocity 等。
1.Jsp 为 Servlet 专用,不能单独进行使用。 已淘汰
2.Thymeleaf 为新技术,功能较为强大,但是执行的效率比较低。
3.Velocity从2010年更新完 2.0 版本后,便没有在更新。Spring Boot 官方在 1.4 版本后对此也不在支持,虽然 Velocity 在 2017 年版本得到迭代,但为时已晚。
freemarker作为springmvc一种视图格式,默认情况下SpringMVC支持freemarker视图格式。
需要创建Spring Boot+Freemarker工程用于测试模板。
创建一个freemarker-demo 的测试工程专门用于freemarker的功能测试与模板的测试。
pom.xml如下
heima-leadnews-test com.heima 1.0-SNAPSHOT 4.0.0 freemarker-demo8 8 org.springframework.boot spring-boot-starter-weborg.springframework.boot spring-boot-starter-freemarkerorg.springframework.boot spring-boot-starter-testorg.projectlombok lombokorg.apache.commons commons-io1.3.2
配置application.yml
server: port: 8881 #服务端口 spring: application: name: freemarker-demo #指定服务名 freemarker: cache: false #关闭模板缓存,方便测试 settings: template_update_delay: 0 #检查模板更新延迟时间,设置为0表示立即检查,如果时间大于0会有缓存不方便进行模板测试 suffix: .ftl #指定Freemarker模板文件的后缀名
在freemarker的测试工程下创建模型类型用于测试
package com.heima.freemarker.entity; import lombok.Data; import java.util.Date; @Data public class Student { private String name;//姓名 private int age;//年龄 private Date birthday;//生日 private Float money;//钱包 }
在resources下创建templates,此目录为freemarker的默认模板存放目录。
在templates下创建模板文件 01-basic.ftl ,模板中的插值表达式最终会被freemarker替换成具体的数据。
Hello World! 普通文本 String 展示:
Hello ${name}
对象Student中的数据展示:
姓名:${stu.name}
年龄:${stu.age}
创建Controller类,向Map中添加name,最后返回模板文件。
package com.xuecheng.test.freemarker.controller; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.client.RestTemplate; import java.util.Map; @Controller public class HelloController { @GetMapping("/basic") public String test(Model model) { //1.纯文本形式的参数 model.addAttribute("name", "freemarker"); //2.实体类相关的参数 Student student = new Student(); student.setName("小明"); student.setAge(18); model.addAttribute("stu", student); return "01-basic"; } }
01-basic.ftl,使用插值表达式填充数据
Hello World! 普通文本 String 展示:
Hello ${name}
对象Student中的数据展示:
姓名:${stu.name}
年龄:${stu.age}
package com.heima.freemarker; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class FreemarkerDemotApplication { public static void main(String[] args) { SpringApplication.run(FreemarkerDemotApplication.class,args); } }
请求:http://localhost:8881/basic
<#--我是一个freemarker注释-->
Hello ${name}
3、FTL指令:和HTML标记类似,名字前加#予以区分,Freemarker会解析标签中的表达式或逻辑。
<# >FTL指令#>
4、文本,仅文本信息,这些不是freemarker的注释、插值、FTL指令的内容会被freemarker忽略解析,直接输出内容。
<#--freemarker中的普通文本--> 我是一个普通的文本
1、数据模型:
在HelloController中新增如下方法:
@GetMapping("/list") public String list(Model model){ //------------------------------------ Student stu1 = new Student(); stu1.setName("小强"); stu1.setAge(18); stu1.setMoney(1000.86f); stu1.setBirthday(new Date()); //小红对象模型数据 Student stu2 = new Student(); stu2.setName("小红"); stu2.setMoney(200.1f); stu2.setAge(19); //将两个对象模型数据存放到List集合中 Liststus = new ArrayList<>(); stus.add(stu1); stus.add(stu2); //向model中存放List集合数据 model.addAttribute("stus",stus); //------------------------------------ //创建Map数据 HashMap stuMap = new HashMap<>(); stuMap.put("stu1",stu1); stuMap.put("stu2",stu2); // 3.1 向model中存放Map数据 model.addAttribute("stuMap", stuMap); return "02-list"; }
2、模板:
在templates中新增02-list.ftl文件
Hello World! <#-- list 数据的展示 --> 展示list中的stu数据:
序号 | 姓名 | 年龄 | 钱包 |
序号 | 姓名 | 年龄 | 钱包 |
实例代码:
Hello World! <#-- list 数据的展示 --> 展示list中的stu数据:
序号 | 姓名 | 年龄 | 钱包 |
${stu_index+1} | ${stu.name} | ${stu.age} | ${stu.money} |
序号 | 姓名 | 年龄 | 钱包 |
${key_index} | ${stuMap[key].name} | ${stuMap[key].age} | ${stuMap[key].money} |
👆上面代码解释:
${k_index}:
index:得到循环的下标,使用方法是在stu后边加"_index",它的值是从0开始
if 指令即判断指令,是常用的FTL指令,freemarker在解析时遇到if会进行判断,条件为真则输出if中间的内容,否则跳过内容不再输出。
指令格式
<#if >
1、数据模型:
使用list指令中测试数据模型,判断名称为小红的数据字体显示为红色。
2、模板:
姓名 | 年龄 | 钱包 |
${stu.name} | ${stu.age} | ${stu.mondy} |
实例代码:
姓名 | 年龄 | 钱包 | |
${stu_index} | ${stu.name} | ${stu.age} | ${stu.money} |
${stu_index} | ${stu.name} | ${stu.age} | ${stu.money} |
3、输出:
姓名为“小强”则字体颜色显示为红色。
1、算数运算符
FreeMarker表达式中完全支持算术运算,FreeMarker支持的算术运算符包括:
加法: +
减法: -
乘法: *
除法: /
求模 (求余): %
模板代码
算数运算符
100+5 运算: ${100 + 5 }
100 - 5 * 5运算:${100 - 5 * 5}
5 / 2运算:${5 / 2}
12 % 10运算:${12 % 10}
除了 + 运算以外,其他的运算只能和 number 数字类型的计算。
2、比较运算符
=或者==:判断两个值是否相等.
!=:判断两个值是否不等.
>或者gt:判断左边值是否大于右边值
>=或者gte:判断左边值是否大于等于右边值
<或者lt:判断左边值是否小于右边值
<=或者lte:判断左边值是否小于等于右边值
= 和 == 模板代码
Hello World! 比较运算符
Controller 的 数据模型代码
@GetMapping("operation") public String testOperation(Model model) { //构建 Date 数据 Date now = new Date(); model.addAttribute("date1", now); model.addAttribute("date2", now); return "03-operation"; }
比较运算符注意
=和!=可以用于字符串、数值和日期来比较是否相等
=和!=两边必须是相同类型的值,否则会产生错误
字符串 "x" 、"x " 、"X"比较是不等的.因为FreeMarker是精确比较
其它的运行符可以作用于数字和日期,但不能作用于字符串
使用gt等字母运算符代替>会有更好的效果,因为 FreeMarker会把> 解释成FTL标签的结束字符
可以使用括号来避免这种情况,如:<#if (x>y)>
3、逻辑运算符
逻辑与:&&
逻辑或:||
逻辑非:!
逻辑运算符只能作用于布尔值,否则将产生错误 。
模板代码
逻辑运算符
<#if (10 lt 12 )&&( 10 gt 5 ) > (10 lt 12 )&&( 10 gt 5 ) 显示为 true #if>
<#if !false> false 取反为true #if>
1、判断某变量是否存在使用 “??”
用法为:variable??,如果该变量存在,返回true,否则返回false
例:为防止stus为空报错可以加上判断如下:
<#if stus??> <#list stus as stu> ...... #list> #if>
2、缺失变量默认值使用 “!”
使用!要以指定一个默认值,当变量为空时显示默认值
例: ${name!''}表示如果name为空显示空字符串。
如果是嵌套对象则建议使用()括起来
例: ${(stu.bestFriend.name)!''}表示,如果stu或bestFriend或name为空默认显示空字符串。
内建函数语法格式: 变量+?+函数名称
1、和到某个集合的大小
${集合名?size}
2、日期格式化
显示年月日: ${today?date}
显示时分秒:${today?time}
显示日期+时间:${today?datetime}
自定义格式化: ${today?string("yyyy年MM月")}
3、内建函数c
model.addAttribute("point", 102920122);
point是数字型,使用${point}会显示这个数字的值,每三位使用逗号分隔。
如果不想显示为每三位分隔的数字,可以使用c函数将数字型转成字符串输出
${point?c}
4、将json字符串转成对象
一个例子:
其中用到了 assign标签,assign的作用是定义一个变量。
<#assign text="{'bank':'工商银行','account':'10101920201920212'}" /> <#assign data=text?eval /> 开户行:${data.bank} 账号:${data.account}
模板代码:
inner Function 获得集合大小
集合大小:
获得日期
显示年月日:
显示时分秒:
显示日期+时间:
自定义格式化:
内建函数C
没有C函数显示的数值:
有C函数显示的数值:
声明变量assign
内建函数模板页面:
inner Function 获得集合大小
集合大小:${stus?size}
获得日期
显示年月日: ${today?date}
显示时分秒:${today?time}
显示日期+时间:${today?datetime}
自定义格式化: ${today?string("yyyy年MM月")}
内建函数C
没有C函数显示的数值:${point}
有C函数显示的数值:${point?c}
声明变量assign
<#assign text="{'bank':'工商银行','account':'10101920201920212'}" /> <#assign data=text?eval /> 开户行:${data.bank} 账号:${data.account}
内建函数Controller数据模型:
@GetMapping("innerFunc") public String testInnerFunc(Model model) { //1.1 小强对象模型数据 Student stu1 = new Student(); stu1.setName("小强"); stu1.setAge(18); stu1.setMoney(1000.86f); stu1.setBirthday(new Date()); //1.2 小红对象模型数据 Student stu2 = new Student(); stu2.setName("小红"); stu2.setMoney(200.1f); stu2.setAge(19); //1.3 将两个对象模型数据存放到List集合中 Liststus = new ArrayList<>(); stus.add(stu1); stus.add(stu2); model.addAttribute("stus", stus); // 2.1 添加日期 Date date = new Date(); model.addAttribute("today", date); // 3.1 添加数值 model.addAttribute("point", 102920122); return "04-innerFunc"; }
之前的测试都是SpringMVC将Freemarker作为视图解析器(ViewReporter)来集成到项目中,工作中,有的时候需要使用Freemarker原生Api来生成静态内容,下面一起来学习下原生Api生成文本文件
使用freemarker原生Api将页面生成html文件,本节测试html文件生成的方法:
根据模板文件生成html文件
①:修改application.yml文件,添加以下模板存放位置的配置信息,完整配置如下:
server: port: 8881 #服务端口 spring: application: name: freemarker-demo #指定服务名 freemarker: cache: false #关闭模板缓存,方便测试 settings: template_update_delay: 0 #检查模板更新延迟时间,设置为0表示立即检查,如果时间大于0会有缓存不方便进行模板测试 suffix: .ftl #指定Freemarker模板文件的后缀名 template-loader-path: classpath:/templates #模板存放位置
②:在test下创建测试类
package com.heima.freemarker.test; import com.heima.freemarker.FreemarkerDemoApplication; import com.heima.freemarker.entity.Student; import freemarker.template.Configuration; import freemarker.template.Template; import freemarker.template.TemplateException; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.junit4.SpringRunner; import java.io.FileWriter; import java.io.IOException; import java.util.*; @SpringBootTest(classes = FreemarkerDemoApplication.class) @RunWith(SpringRunner.class) public class FreemarkerTest { @Autowired private Configuration configuration; @Test public void test() throws IOException, TemplateException { //freemarker的模板对象,获取模板 Template template = configuration.getTemplate("02-list.ftl"); Map params = getData(); //合成 //第一个参数 数据模型 //第二个参数 输出流 template.process(params, new FileWriter("d:/list.html")); } private Map getData() { Mapmap = new HashMap<>(); //小强对象模型数据 Student stu1 = new Student(); stu1.setName("小强"); stu1.setAge(18); stu1.setMoney(1000.86f); stu1.setBirthday(new Date()); //小红对象模型数据 Student stu2 = new Student(); stu2.setName("小红"); stu2.setMoney(200.1f); stu2.setAge(19); //将两个对象模型数据存放到List集合中 List stus = new ArrayList<>(); stus.add(stu1); stus.add(stu2); //向map中存放List集合数据 map.put("stus", stus); //创建Map数据 HashMap stuMap = new HashMap<>(); stuMap.put("stu1", stu1); stuMap.put("stu2", stu2); //向map中存放Map数据 map.put("stuMap", stuMap); //返回Map return map; } }
MinIO基于Apache License v2.0开源协议的对象存储服务,可以做为云存储的解决方案用来保存海量的图片,视频,文档。由于采用Golang实现,服务端可以工作在Windows,Linux, OS X和FreeBSD上。配置简单,基本是复制可执行程序,单行命令可以运行起来。
MinIO兼容亚马逊S3云存储服务接口,非常适合于存储大容量非结构化的数据,例如图片、视频、日志文件、备份数据和容器/虚拟机镜像等,而一个对象文件可以是任意大小,从几kb到最大5T不等。
S3 ( Simple Storage Service简单存储服务)
基本概念
bucket – 类比于文件系统的目录
Object – 类比文件系统的文件
Keys – 类比文件名
官网文档:MinIO Object Storage for Kubernetes — MinIO Object Storage for Kubernetes
数据保护
Minio使用Minio Erasure Code(纠删码)来防止硬件故障。即便损坏一半以上的driver,但是仍然可以从中恢复。
高性能
作为高性能对象存储,在标准硬件条件下它能达到55GB/s的读、35GB/s的写速率
可扩容
不同MinIO集群可以组成联邦,并形成一个全局的命名空间,并跨越多个数据中心
SDK支持
基于Minio轻量的特点,它得到类似Java、Python或Go等语言的sdk支持
有操作页面
面向用户友好的简单操作界面,非常方便的管理Bucket及里面的文件资源
功能简单
这一设计原则让MinIO不容易出错、更快启动
丰富的API
支持文件资源的分享连接及分享链接的过期策略、存储桶操作、文件列表访问及文件上传下载的基本功能等
文件变化主动通知
存储桶(Bucket)如果发生改变,比如上传对象和删除对象,可以使用存储桶事件通知机制进行监控,并通过以下方式发布出去:AMQP、MQTT、Elasticsearch、Redis、NATS、MySQL、Kafka、Webhooks等。
我们提供的镜像中已经有minio的环境; 我们可以使用docker进行环境部署和启动
docker run -p 9000:9000 --name minio -d --restart=always -e "MINIO_ACCESS_KEY=minio" -e "MINIO_SECRET_KEY=minio123" -v /home/data:/data -v /home/config:/root/.minio minio/minio server /data
假设我们的服务器地址为http://192.168.200.130:9000,我们在地址栏输入:http://192.168.200.130:9000/ 即可进入登录界面。
Access Key为minio Secret_key 为minio123 进入系统后可以看到主界面
点击右下角的“+”号 ,点击下面的图标,创建一个桶
创建minio-demo,对应pom如下
heima-leadnews-test com.heima 1.0-SNAPSHOT 4.0.0 minio-demo8 8 io.minio minio7.1.0 org.springframework.boot spring-boot-starter-weborg.springframework.boot spring-boot-starter-test
引导类:
package com.heima.minio; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class MinIOApplication { public static void main(String[] args) { SpringApplication.run(MinIOApplication.class,args); } }
创建测试类,上传html文件
package com.heima.minio.test; import io.minio.MinioClient; import io.minio.PutObjectArgs; import java.io.FileInputStream; public class MinIOTest { public static void main(String[] args) { FileInputStream fileInputStream = new FileInputStream("C:\Users\yuhon\Downloads\index.js"); try { fileInputStream = new FileInputStream("D:\list.html");; //1.创建minio链接客户端 MinioClient minioClient = MinioClient.builder().credentials("minio", "minio123").endpoint("http://192.168.200.130:9000").build(); //2.上传 对象 PutObjectArgs putObjectArgs = PutObjectArgs.builder() .object("list.html")//文件名 .contentType("text/html")//文件类型 .bucket("leadnews")//桶名词 与minio创建的桶名称 一致 .stream(fileInputStream, fileInputStream.available(), -1) //文件流(流stream,大小,传到哪) //fileInputStream.available()代表有值就一直传递;-1代表传完所有文件 .build(); minioClient.putObject(putObjectArgs); //上传完成 //访问 System.out.println("http://192.168.200.130:9000/leadnews/ak47.jpg"); } catch (Exception ex) { ex.printStackTrace(); } } }
封装为了其它微服务使用
导入依赖
org.springframework.boot spring-boot-autoconfigureio.minio minio7.1.0 org.springframework.boot spring-boot-starterorg.springframework.boot spring-boot-configuration-processortrue org.springframework.boot spring-boot-starter-actuator
MinIOConfigProperties
package com.heima.file.config; import lombok.Data; import org.springframework.boot.context.properties.ConfigurationProperties; import java.io.Serializable; @Data @ConfigurationProperties(prefix = "minio") // 文件上传 配置前缀file.oss public class MinIOConfigProperties implements Serializable { private String accessKey; private String secretKey; private String bucket; private String endpoint; private String readPath; }
MinIOConfig
package com.heima.file.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Data @Configuration @EnableConfigurationProperties({MinIOConfigProperties.class}) //当引入FileStorageService接口时 @ConditionalOnClass(FileStorageService.class) public class MinIOConfig { @Autowired private MinIOConfigProperties minIOConfigProperties; @Bean public MinioClient buildMinioClient(){ return MinioClient .builder() .credentials(minIOConfigProperties.getAccessKey(), minIOConfigProperties.getSecretKey()) .endpoint(minIOConfigProperties.getEndpoint()) .build(); } }
FileStorageService
package com.heima.file.service; import java.io.InputStream; /** * @author itheima */ public interface FileStorageService { /** * 上传图片文件 * @param prefix 文件前缀 * @param filename 文件名 * @param inputStream 文件流 * @return 文件全路径 */ public String uploadImgFile(String prefix, String filename,InputStream inputStream); /** * 上传html文件 * @param prefix 文件前缀 * @param filename 文件名 * @param inputStream 文件流 * @return 文件全路径 */ public String uploadHtmlFile(String prefix, String filename,InputStream inputStream); /** * 删除文件 * @param pathUrl 文件全路径 */ public void delete(String pathUrl); /** * 下载文件 * @param pathUrl 文件全路径 * @return * */ public byte[] downLoadFile(String pathUrl); }
MinIOFileStorageService
package com.heima.file.service.impl; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.text.SimpleDateFormat; import java.util.Date; @Slf4j @EnableConfigurationProperties(MinIOConfigProperties.class) @Import(MinIOConfig.class) public class MinIOFileStorageService implements FileStorageService { @Autowired private MinioClient minioClient; @Autowired private MinIOConfigProperties minIOConfigProperties; private final static String separator = "/"; /** * @param dirPath * @param filename yyyy/mm/dd/file.jpg * @return */ public String builderFilePath(String dirPath,String filename) { StringBuilder stringBuilder = new StringBuilder(50); if(!StringUtils.isEmpty(dirPath)){ stringBuilder.append(dirPath).append(separator); } SimpleDateFormat sdf = new SimpleDateFormat("yyyy/MM/dd"); String todayStr = sdf.format(new Date()); stringBuilder.append(todayStr).append(separator); stringBuilder.append(filename); return stringBuilder.toString(); } /** * 上传图片文件 * @param prefix 文件前缀 * @param filename 文件名 * @param inputStream 文件流 * @return 文件全路径 */ @Override public String uploadImgFile(String prefix, String filename,InputStream inputStream) { String filePath = builderFilePath(prefix, filename); try { PutObjectArgs putObjectArgs = PutObjectArgs.builder() .object(filePath) .contentType("image/jpg") .bucket(minIOConfigProperties.getBucket()).stream(inputStream,inputStream.available(),-1) .build(); minioClient.putObject(putObjectArgs); StringBuilder urlPath = new StringBuilder(minIOConfigProperties.getReadPath()); urlPath.append(separator+minIOConfigProperties.getBucket()); urlPath.append(separator); urlPath.append(filePath); return urlPath.toString(); }catch (Exception ex){ log.error("minio put file error.",ex); throw new RuntimeException("上传文件失败"); } } /** * 上传html文件 * @param prefix 文件前缀 * @param filename 文件名 * @param inputStream 文件流 * @return 文件全路径 */ @Override public String uploadHtmlFile(String prefix, String filename,InputStream inputStream) { String filePath = builderFilePath(prefix, filename); try { PutObjectArgs putObjectArgs = PutObjectArgs.builder() .object(filePath) .contentType("text/html") .bucket(minIOConfigProperties.getBucket()).stream(inputStream,inputStream.available(),-1) .build(); minioClient.putObject(putObjectArgs); StringBuilder urlPath = new StringBuilder(minIOConfigProperties.getReadPath()); urlPath.append(separator+minIOConfigProperties.getBucket()); urlPath.append(separator); urlPath.append(filePath); return urlPath.toString(); }catch (Exception ex){ log.error("minio put file error.",ex); ex.printStackTrace(); throw new RuntimeException("上传文件失败"); } } /** * 删除文件 * @param pathUrl 文件全路径 */ @Override public void delete(String pathUrl) { String key = pathUrl.replace(minIOConfigProperties.getEndpoint()+"/",""); int index = key.indexOf(separator); String bucket = key.substring(0,index); String filePath = key.substring(index+1); // 删除Objects RemoveObjectArgs removeObjectArgs = RemoveObjectArgs.builder().bucket(bucket).object(filePath).build(); try { minioClient.removeObject(removeObjectArgs); } catch (Exception e) { log.error("minio remove file error. pathUrl:{}",pathUrl); e.printStackTrace(); } } /** * 下载文件 * @param pathUrl 文件全路径 * @return 文件流 * */ @Override public byte[] downLoadFile(String pathUrl) { String key = pathUrl.replace(minIOConfigProperties.getEndpoint()+"/",""); int index = key.indexOf(separator); String bucket = key.substring(0,index); String filePath = key.substring(index+1); InputStream inputStream = null; try { inputStream = minioClient.getObject(GetObjectArgs.builder().bucket(minIOConfigProperties.getBucket()).object(filePath).build()); } catch (Exception e) { log.error("minio down file error. pathUrl:{}",pathUrl); e.printStackTrace(); } ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); byte[] buff = new byte[100]; int rc = 0; while (true) { try { if (!((rc = inputStream.read(buff, 0, 100)) > 0)) break; } catch (IOException e) { e.printStackTrace(); } byteArrayOutputStream.write(buff, 0, rc); } return byteArrayOutputStream.toByteArray(); } }
在resources中新建META-INF/spring.factories
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ com.heima.file.service.impl.MinIOFileStorageService
第一,导入heima-file-starter的依赖
第二,在微服务中添加minio所需要的配置
minio: accessKey: minio secretKey: minio123 bucket: leadnews endpoint: http://192.168.200.130:9000 readPath: http://192.168.200.130:9000
第三,在对应使用的业务类中注入FileStorageService,样例如下:
package com.heima.minio.test; import java.io.FileInputStream; import java.io.FileNotFoundException; @SpringBootTest(classes = MinioApplication.class) @RunWith(SpringRunner.class) public class MinioTest { @Autowired private FileStorageService fileStorageService; @Test public void testUpdateImgFile() { try { FileInputStream fileInputStream = new FileInputStream("E:\tmp\ak47.jpg"); String filePath = fileStorageService.uploadImgFile("", "ak47.jpg", fileInputStream); System.out.println(filePath); } catch (FileNotFoundException e) { e.printStackTrace(); } } }
方案一
用户某一条文章,根据文章的id去查询文章内容表,返回渲染页面
方案二 效率高
这个要在nacos里面配 minio;
?allowPublicKeyRetrieval=true 成功解决;
4.在文章微服务中导入依赖
org.springframework.boot spring-boot-starter-freemarkercom.heima heima-file-starter1.0-SNAPSHOT
5.新建ApArticleContentMapper
package com.heima.article.mapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.heima.model.article.pojos.ApArticleContent; import org.apache.ibatis.annotations.Mapper; @Mapper public interface ApArticleContentMapper extends BaseMapper { }
6.在artile微服务中新增测试类(后期新增文章的时候创建详情静态页,目前暂时手动生成)
package com.heima.article.test; import java.io.ByteArrayInputStream; import java.io.InputStream; import java.io.StringWriter; import java.util.HashMap; import java.util.Map; @SpringBootTest(classes = ArticleApplication.class) @RunWith(SpringRunner.class) public class ArticleFreemarkerTest { @Autowired private Configuration configuration; @Autowired private FileStorageService fileStorageService; @Autowired private ApArticleMapper apArticleMapper; @Autowired private ApArticleContentMapper apArticleContentMapper; @Test public void createStaticUrlTest() throws Exception { //1.获取文章内容 ApArticleContent apArticleContent = apArticleContentMapper.selectOne(Wrappers.lambdaQuery().eq(ApArticleContent::getArticleId, 1390536764510310401L)); if(apArticleContent != null && StringUtils.isNotBlank(apArticleContent.getContent())){ //2.文章内容通过freemarker生成html文件 StringWriter out = new StringWriter(); Template template = configuration.getTemplate("article.ftl"); //第一个参数 数据模型 Mapparams = new HashMap<>(); //JSONArray.parseArray 字符串转成对象 params.put("content", JSONArray.parseArray(apArticleContent.getContent())); //合成 template.process(params, out); //输入流 InputStream is = new ByteArrayInputStream(out.toString().getBytes()); //3.把html文件上传到minio中 (前缀,文件名称,输入流) String path = fileStorageService.uploadHtmlFile("", apArticleContent.getArticleId() + ".html", is); //4.修改ap_article表,保存static_url字段 ApArticle article = new ApArticle(); article.setId(apArticleContent.getArticleId()); article.setStaticUrl(path); apArticleMapper.updateById(article); } } }
大佬弹幕:人家现在发布文章,都是直接用富文本 编辑器,发布后数据库存的是富文本,然后直接把富文本丢给前端人家就可以直接显示了,搞这么麻烦干嘛,
①:资料中找到heima-leadnews-wemedia.zip解压
拷贝到heima-leadnews-service工程下,并指定子模块
执行leadnews-wemedia.sql脚本
添加对应的nacos配置
spring: datasource: driver-class-name: com.mysql.jdbc.Driver url: jdbc:mysql://localhost:3306/leadnews_wemedia?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC username: root password: root # 设置Mapper接口所对应的XML文件位置,如果你在Mapper接口中有自定义方法,需要进行该配置 mybatis-plus: mapper-locations: classpath*:mapper/*.xml # 设置别名包扫描路径,通过该属性可以给包中的类注册别名 type-aliases-package: com.heima.model.media.pojos
②:资料中找到heima-leadnews-wemedia-gateway.zip解压
拷贝到heima-leadnews-gateway工程下,并指定子模块
添加对应的nacos配置
spring: cloud: gateway: globalcors: cors-configurations: '[/**]': # 匹配所有请求 allowedOrigins: "*" #跨域处理 允许所有的域 allowedMethods: # 支持的方法 - GET - POST - PUT - DELETE routes: # 平台管理 - id: wemedia uri: lb://leadnews-wemedia predicates: - Path=/wemedia/** filters: - StripPrefix= 1
③:在资料中找到类文件夹
拷贝wemedia文件夹到heima-leadnews-model模块下的com.heima.model
通过nginx的虚拟主机功能,使用同一个nginx访问多个项目
搭建步骤:
①:资料中找到wemedia-web.zip解压
②:在nginx中leadnews.conf目录中新增heima-leadnews-wemedia.conf文件
网关地址修改(localhost:51602)
前端项目目录修改(wemedia-web解压的目录)
访问端口修改(8802)
upstream heima-wemedia-gateway{ server localhost:51602; } server { listen 8802; location / { root D:/workspace/wemedia-web/; index index.html; } location ~/wemedia/MEDIA/(.*) { proxy_pass http://heima-wemedia-gateway/; proxy_set_header HOST $host; # 不改变源请求头的值 proxy_pass_request_body on; #开启获取请求体 proxy_pass_request_headers on; #开启获取请求头 proxy_set_header X-Real-IP $remote_addr; # 记录真实发出请求的客户端IP proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; #记录代理信息 } }
③:启动nginx,启动自媒体微服务和对应网关
④:联调测试登录功能 测试成功
图片上传的页面,首先是展示素材信息,可以点击图片上传,弹窗后可以上传图片
媒体图文素材信息表wm_material
对应实体类:
package com.heima.model.wemedia.pojos; import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import lombok.Data; import java.io.Serializable; import java.util.Date; /** ** 自媒体图文素材信息表 *
* * @author itheima */ @Data @TableName("wm_material") public class WmMaterial implements Serializable { private static final long serialVersionUID = 1L; /** * 主键 */ @TableId(value = "id", type = IdType.AUTO) private Integer id; /** * 自媒体用户ID */ @TableField("user_id") private Integer userId; /** * 图片地址 */ @TableField("url") private String url; /** * 素材类型 0 图片 1 视频 */ @TableField("type") private Short type; /** * 是否收藏 */ @TableField("is_collection") private Short isCollection; /** * 创建时间 */ @TableField("created_time") private Date createdTime; }
①:前端发送上传图片请求,类型为MultipartFile
②:网关进行token解析后,把解析后的用户信息存储到header中
//获得token解析后中的用户信息 Object userId = claimsBody.get("id"); //在header中添加新的信息 ServerHttpRequest serverHttpRequest = request.mutate().headers(httpHeaders -> { httpHeaders.add("userId", userId + ""); }).build(); //重置header exchange.mutate().request(serverHttpRequest).build();
③:自媒体微服务使用拦截器获取到header中的的用户信息,并放入到threadlocal中
在heima-leadnews-utils中新增工具类
注意:需要从资料中找出WmUser实体类拷贝到model工程下
package com.heima.utils.thread; import com.heima.model.wemedia.pojos.WmUser; public class WmThreadLocalUtil { private final static ThreadLocalWM_USER_THREAD_LOCAL = new ThreadLocal<>(); /** * 添加用户 * @param wmUser */ public static void setUser(WmUser wmUser){ WM_USER_THREAD_LOCAL.set(wmUser); } /** * 获取用户 */ public static WmUser getUser(){ return WM_USER_THREAD_LOCAL.get(); } /** * 清理用户 */ public static void clear(){ WM_USER_THREAD_LOCAL.remove(); } }
在heima-leadnews-wemedia中新增拦截器
package com.heima.wemedia.interceptor; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.util.Optional; @Slf4j public class WmTokenInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { //得到header中的信息 String userId = request.getHeader("userId"); Optionaloptional = Optional.ofNullable(userId); if(optional.isPresent()){ //把用户id存入threadloacl中 WmUser wmUser = new WmUser(); wmUser.setId(Integer.valueOf(userId)); WmThreadLocalUtils.setUser(wmUser); log.info("wmTokenFilter设置用户信息到threadlocal中..."); } return true; } @Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { log.info("清理threadlocal..."); WmThreadLocalUtils.clear(); } }
配置使拦截器生效,拦截所有的请求
package com.heima.wemedia.config; import com.heima.wemedia.interceptor.WmTokenInterceptor; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; @Configuration public class WebMvcConfig implements WebMvcConfigurer { @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new WmTokenInterceptor()).addPathPatterns("/**"); } }
④:先把图片上传到minIO中,获取到图片请求的路径——(2.2.5查看具体功能实现)
⑤:把用户id和图片上的路径保存到素材表中——(2.2.5查看具体功能实现)
说明 | |
接口路径 | /api/v1/material/upload_picture |
请求方式 | POST |
参数 | MultipartFile |
响应结果 | ResponseResult |
MultipartFile :Springmvc指定的文件接收类型
ResponseResult :
成功需要回显图片,返回素材对象
{ "host":null, "code":200, "errorMessage":"操作成功", "data":{ "id":52, "userId":1102, "url":"http://192.168.200.130:9000/leadnews/2021/04/26/a73f5b60c0d84c32bfe175055aaaac40.jpg", "type":0, "isCollection":0, "createdTime":"2021-01-20T16:49:48.443+0000" } }
失败:
参数失效
文章上传失败
①:导入heima-file-starter
com.heima heima-file-starter1.0-SNAPSHOT
②:在自媒体微服务的配置中心添加以下配置:
minio: accessKey: minio secretKey: minio123 bucket: leadnews endpoint: http://192.168.200.130:9000 readPath: http://192.168.200.130:9000
①:创建WmMaterialController
@RestController @RequestMapping("/api/v1/material") public class WmMaterialController { @PostMapping("/upload_picture") public ResponseResult uploadPicture(MultipartFile multipartFile){ return null; } }
②:mapper
package com.heima.wemedia.mapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.heima.model.wemedia.pojos.WmMaterial; import org.apache.ibatis.annotations.Mapper; @Mapper public interface WmMaterialMapper extends BaseMapper{ }
③:业务层:
package com.heima.wemedia.service; public interface WmMaterialService extends IService{ /** * 图片上传 * @param multipartFile * @return */ public ResponseResult uploadPicture(MultipartFile multipartFile); }
业务层实现类:
package com.heima.wemedia.service.impl; import java.io.IOException; import java.util.Date; import java.util.UUID; @Slf4j @Service @Transactional public class WmMaterialServiceImpl extends ServiceImplimplements WmMaterialService { @Autowired private FileStorageService fileStorageService; /** * 图片上传 * @param multipartFile * @return */ @Override public ResponseResult uploadPicture(MultipartFile multipartFile) { //1.检查参数 if(multipartFile == null || multipartFile.getSize() == 0){ return ResponseResult.errorResult(AppHttpCodeEnum.PARAM_INVALID); } //2.上传图片到minIO中 String fileName = UUID.randomUUID().toString().replace("-", ""); //aa.jpg String originalFilename = multipartFile.getOriginalFilename(); String postfix = originalFilename.substring(originalFilename.lastIndexOf(".")); String fileId = null; try { fileId = fileStorageService.uploadImgFile("", fileName + postfix, multipartFile.getInputStream()); log.info("上传图片到MinIO中,fileId:{}",fileId); } catch (IOException e) { e.printStackTrace(); log.error("WmMaterialServiceImpl-上传文件失败"); } //3.保存到数据库中 WmMaterial wmMaterial = new WmMaterial(); wmMaterial.setUserId(WmThreadLocalUtil.getUser().getId()); wmMaterial.setUrl(fileId); wmMaterial.setIsCollection((short)0); wmMaterial.setType((short)0); wmMaterial.setCreatedTime(new Date()); save(wmMaterial); //4.返回结果 return ResponseResult.okResult(wmMaterial); } }
④:控制器
@RestController @RequestMapping("/api/v1/material") public class WmMaterialController { @Autowired private WmMaterialService wmMaterialService; @PostMapping("/upload_picture") public ResponseResult uploadPicture(MultipartFile multipartFile){ return wmMaterialService.uploadPicture(multipartFile); } }
⑤:测试
启动自媒体微服务和自媒体网关,使用前端项目进行测试
说明 | |
接口路径 | /api/v1/material/list |
请求方式 | POST |
参数 | WmMaterialDto |
响应结果 | ResponseResult |
WmMaterialDto :
@Data public class WmMaterialDto extends PageRequestDto { /** * 1 收藏 * 0 未收藏 */ private Short isCollection; }
ResponseResult :
{ "host":null, "code":200, "errorMessage":"操作成功", "data":[ { "id":52, "userId":1102, "url":"http://192.168.200.130:9000/leadnews/2021/04/26/ec893175f18c4261af14df14b83cb25f.jpg", "type":0, "isCollection":0, "createdTime":"2021-01-20T16:49:48.000+0000" }, .... ], "currentPage":1, "size":20, "total":0 }
①:在WmMaterialController类中新增方法
@PostMapping("/list") public ResponseResult findList(@RequestBody WmMaterialDto dto){ return null; }
②:mapper已定义
③:业务层
在WmMaterialService中新增方法
/** * 素材列表查询 * @param dto * @return */ public ResponseResult findList( WmMaterialDto dto);
实现方法:
/** * 素材列表查询 * @param dto * @return */ @Override public ResponseResult findList(WmMaterialDto dto) { //1.检查参数 dto.checkParam(); //2.分页查询 IPage page = new Page(dto.getPage(),dto.getSize()); LambdaQueryWrapperlambdaQueryWrapper = new LambdaQueryWrapper<>(); //是否收藏 if(dto.getIsCollection() != null && dto.getIsCollection() == 1){ lambdaQueryWrapper.eq(WmMaterial::getIsCollection,dto.getIsCollection()); } //按照用户查询 lambdaQueryWrapper.eq(WmMaterial::getUserId,WmThreadLocalUtil.getUser().getId()); //按照时间倒序 lambdaQueryWrapper.orderByDesc(WmMaterial::getCreatedTime); page = page(page,lambdaQueryWrapper); //3.结果返回 ResponseResult responseResult = new PageResponseResult(dto.getPage(),dto.getSize(),(int)page.getTotal()); responseResult.setData(page.getRecords()); return responseResult; }
④:控制器:
@PostMapping("/list") public ResponseResult findList(@RequestBody WmMaterialDto dto){ return wmMaterialService.findList(dto); }
⑤:在自媒体引导类中 添加 mybatis-plus的分页拦截器
@Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; }
wm_channel 频道信息表
对应实体类:
package com.heima.model.wemedia.pojos; import java.util.Date; /** ** 频道信息表 *
* * @author itheima */ @Data @TableName("wm_channel") public class WmChannel implements Serializable { private static final long serialVersionUID = 1L; @TableId(value = "id", type = IdType.AUTO) private Integer id; /** * 频道名称 */ @TableField("name") private String name; /** * 频道描述 */ @TableField("description") private String description; /** * 是否默认频道 * 1:默认 true * 0:非默认 false */ @TableField("is_default") private Boolean isDefault; /** * 是否启用 * 1:启用 true * 0:禁用 false */ @TableField("status") private Boolean status; /** * 默认排序 */ @TableField("ord") private Integer ord; /** * 创建时间 */ @TableField("created_time") private Date createdTime; }
说明 | |
接口路径 | /api/v1/channel/channels |
请求方式 | POST |
参数 | 无 |
响应结果 | ResponseResult |
ResponseResult :
{ "host": "null", "code": 0, "errorMessage": "操作成功", "data": [ { "id": 4, "name": "java", "description": "java", "isDefault": true, "status": false, "ord": 3, "createdTime": "2019-08-16T10:55:41.000+0000" }, Object { ... }, Object { ... } ] }
接口定义:
package com.heima.wemedia.controller.v1; import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping("/api/v1/channel") public class WmchannelController { @GetMapping("/channels") public ResponseResult findAll(){ return null; } }
mapper
package com.heima.wemedia.mapper; import org.apache.ibatis.annotations.Mapper; @Mapper public interface WmChannelMapper extends BaseMapper{ }
service
package com.heima.wemedia.service; import com.baomidou.mybatisplus.extension.service.IService; import com.heima.model.common.dtos.ResponseResult; import com.heima.model.wemedia.pojos.WmChannel; public interface WmChannelService extends IService{ /** * 查询所有频道 * @return */ public ResponseResult findAll(); }
实现类
package com.heima.wemedia.service.impl; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @Service @Transactional @Slf4j public class WmChannelServiceImpl extends ServiceImplimplements WmChannelService { /** * 查询所有频道 * @return */ @Override public ResponseResult findAll() { return ResponseResult.okResult(list()); } }
控制层
package com.heima.wemedia.controller.v1; import com.heima.model.common.dtos.ResponseResult; import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping("/api/v1/channel") public class WmchannelController { @Autowired private WmChannelService wmChannelService; @GetMapping("/channels") public ResponseResult findAll(){ return wmChannelService.findAll(); } }
wm_news 自媒体文章表
对应实体类:
package com.heima.model.wemedia.pojos; import java.io.Serializable; import java.util.Date; /** ** 自媒体图文内容信息表 *
* * @author itheima */ @Data @TableName("wm_news") public class WmNews implements Serializable { private static final long serialVersionUID = 1L; /** * 主键 */ @TableId(value = "id", type = IdType.AUTO) private Integer id; /** * 自媒体用户ID */ @TableField("user_id") private Integer userId; /** * 标题 */ @TableField("title") private String title; /** * 图文内容 */ @TableField("content") private String content; /** * 文章布局 0 无图文章 1 单图文章 3 多图文章 */ @TableField("type") private Short type; /** * 图文频道ID */ @TableField("channel_id") private Integer channelId; @TableField("labels") private String labels; /** * 创建时间 */ @TableField("created_time") private Date createdTime; /** * 提交时间 */ @TableField("submited_time") private Date submitedTime; /** * 当前状态 0 草稿 1 提交(待审核) 2 审核失败 3 人工审核 4 人工审核通过 8 审核通过(待发布) 9 已发布 */ @TableField("status") private Short status; /** * 定时发布时间,不定时则为空 */ @TableField("publish_time") private Date publishTime; /** * 拒绝理由 */ @TableField("reason") private String reason; /** * 发布库文章ID */ @TableField("article_id") private Long articleId; /** * //图片用逗号分隔 */ @TableField("images") private String images; @TableField("enable") private Short enable; //状态枚举类 @Alias("WmNewsStatus") public enum Status{ NORMAL((short)0),SUBMIT((short)1),FAIL((short)2),ADMIN_AUTH((short)3),ADMIN_SUCCESS((short)4),SUCCESS((short)8),PUBLISHED((short)9); short code; Status(short code){ this.code = code; } public short getCode(){ return this.code; } } }
说明 | |
接口路径 | /api/v1/news/list |
请求方式 | POST |
参数 | WmNewsPageReqDto |
响应结果 | ResponseResult |
WmNewsPageReqDto :
package com.heima.model.wemedia.dtos; import com.heima.model.common.dtos.PageRequestDto; import lombok.Data; import java.util.Date; @Data public class WmNewsPageReqDto extends PageRequestDto { /** * 状态 */ private Short status; /** * 开始时间 */ private Date beginPubDate; /** * 结束时间 */ private Date endPubDate; /** * 所属频道ID */ private Integer channelId; /** * 关键字 */ private String keyword; }
ResponseResult :
{ "host": "null", "code": 0, "errorMessage": "操作成功", "data": [ Object { ... }, Object { ... }, Object { ... } ], "currentPage":1, "size":10, "total":21 }
①:新增WmNewsController
package com.heima.wemedia.controller.v1; import com.heima.model.common.dtos.ResponseResult; @RestController @RequestMapping("/api/v1/news") public class WmNewsController { @PostMapping("/list") public ResponseResult findAll(@RequestBody WmNewsPageReqDto dto){ return null; } }
②:新增WmNewsMapper
package com.heima.wemedia.mapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.heima.model.wemedia.pojos.WmNews; import org.apache.ibatis.annotations.Mapper; @Mapper public interface WmNewsMapper extends BaseMapper{ }
③:新增WmNewsService
package com.heima.wemedia.service; import com.heima.model.wemedia.pojos.WmNews; public interface WmNewsService extends IService{ /** * 查询文章 * @param dto * @return */ public ResponseResult findAll(WmNewsPageReqDto dto); }
实现类:
package com.heima.wemedia.service.impl; import org.springframework.transaction.annotation.Transactional; @Service @Slf4j @Transactional public class WmNewsServiceImpl extends ServiceImplimplements WmNewsService { /** * 查询文章 * @param dto * @return */ @Override public ResponseResult findAll(WmNewsPageReqDto dto) { //1.检查参数 if(dto == null){ return ResponseResult.errorResult(AppHttpCodeEnum.PARAM_INVALID); } //分页参数检查 dto.checkParam(); //获取当前登录人的信息 WmUser user = WmThreadLocalUtil.getUser(); if(user == null){ return ResponseResult.errorResult(AppHttpCodeEnum.NEED_LOGIN); } //2.分页条件查询 IPage page = new Page(dto.getPage(),dto.getSize()); LambdaQueryWrapper lambdaQueryWrapper = new LambdaQueryWrapper<>(); //状态精确查询 if(dto.getStatus() != null){ lambdaQueryWrapper.eq(WmNews::getStatus,dto.getStatus()); } //频道精确查询 if(dto.getChannelId() != null){ lambdaQueryWrapper.eq(WmNews::getChannelId,dto.getChannelId()); } //时间范围查询 if(dto.getBeginPubDate()!=null && dto.getEndPubDate()!=null){ lambdaQueryWrapper.between(WmNews::getPublishTime,dto.getBeginPubDate(),dto.getEndPubDate()); } //关键字模糊查询 if(StringUtils.isNotBlank(dto.getKeyword())){ lambdaQueryWrapper.like(WmNews::getTitle,dto.getKeyword()); } //查询当前登录用户的文章 lambdaQueryWrapper.eq(WmNews::getUserId,user.getId()); //发布时间倒序查询 lambdaQueryWrapper.orderByDesc(WmNews::getCreatedTime); page = page(page,lambdaQueryWrapper); //3.结果返回 ResponseResult responseResult = new PageResponseResult(dto.getPage(),dto.getSize(),(int)page.getTotal()); responseResult.setData(page.getRecords()); return responseResult; } }
④:控制器
package com.heima.wemedia.controller.v1; import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping("/api/v1/news") public class WmNewsController { @Autowired private WmNewsService wmNewsService; @PostMapping("/list") public ResponseResult findAll(@RequestBody WmNewsPageReqDto dto){ return wmNewsService.findAll(dto); } }
启动后端自媒体微服务和自媒体网关微服务,测试文章列表查询
保存文章,除了需要wm_news表以外,还需要另外两张表;
保存了这个关系表,就可以保存素材,素材有被引用的标记,就不能被修改或删除;
其中wm_material和wm_news表的实体类已经导入到了项目中,下面是wm_news_material表对应的实体类:
package com.heima.model.wemedia.pojos; import java.io.Serializable; /** ** 自媒体图文引用素材信息表 *
* * @author itheima */ @Data @TableName("wm_news_material") public class WmNewsMaterial implements Serializable { private static final long serialVersionUID = 1L; /** * 主键 */ @TableId(value = "id", type = IdType.AUTO) private Integer id; /** * 素材ID */ @TableField("material_id") private Integer materialId; /** * 图文ID */ @TableField("news_id") private Integer newsId; /** * 引用类型 0 内容引用 1 主图引用 */ @TableField("type") private Short type; /** * 引用排序 */ @TableField("ord") private Short ord; }
1.前端提交发布或保存为草稿
2.后台判断请求中是否包含了文章id
3.如果不包含id,则为新增
3.1 执行新增文章的操作
3.2 关联文章内容图片与素材的关系
3.3 关联文章封面图片与素材的关系
4.如果包含了id,则为修改请求
4.1 删除该文章与素材的所有关系
4.2 执行修改操作
4.3 关联文章内容图片与素材的关系
4.4 关联文章封面图片与素材的关系
说明 | |
接口路径 | /api/v1/channel/submit |
请求方式 | POST |
参数 | WmNewsDto |
响应结果 | ResponseResult |
WmNewsDto
package com.heima.model.wemedia.dtos; import lombok.Data; import java.util.Date; import java.util.List; @Data public class WmNewsDto { private Integer id; /** * 标题 */ private String title; /** * 频道id */ private Integer channelId; /** * 标签 */ private String labels; /** * 发布时间 */ private Date publishTime; /** * 文章内容 */ private String content; /** * 文章封面类型 0 无图 1 单图 3 多图 -1 自动 */ private Short type; /** * 提交时间 */ private Date submitedTime; /** * 状态 提交为1 草稿为0 */ private Short status; /** * 封面图片列表 多张图以逗号隔开 */ private Listimages; }
前端给传递过来的json数据格式为:
{ "title":"黑马头条项目背景", "type":"1",//这个 0 是无图 1 是单图 3 是多图 -1 是自动 "labels":"黑马头条", "publishTime":"2020-03-14T11:35:49.000Z", "channelId":1, "images":[ "http://192.168.200.130/group1/M00/00/00/wKjIgl5swbGATaSAAAEPfZfx6Iw790.png" ], "status":1, "content":"[ { "type":"text", "value":"随着智能手机的普及,人们更加习惯于通过手机来看新闻。由于生活节奏的加快,很多人只能利用碎片时间来获取信息,因此,对于移动资讯客户端的需求也越来越高。黑马头条项目正是在这样背景下开发出来。黑马头条项目采用当下火热的微服务+大数据技术架构实现。本项目主要着手于获取最新最热新闻资讯,通过大数据分析用户喜好精确推送咨询新闻" }, { "type":"image", "value":"http://192.168.200.130/group1/M00/00/00/wKjIgl5swbGATaSAAAEPfZfx6Iw790.png" } ]" }
ResponseResult:
{ “code”:501, “errorMessage”:“参数失效" } { “code”:200, “errorMessage”:“操作成功" } { “code”:501, “errorMessage”:“素材引用失效" }
①:在新增WmNewsController中新增方法
@PostMapping("/submit") public ResponseResult submitNews(@RequestBody WmNewsDto dto){ return null; }
②:新增WmNewsMaterialMapper类,文章与素材的关联关系需要批量保存,索引需要定义mapper文件和对应的映射文件
package com.heima.wemedia.mapper; import java.util.List; @Mapper public interface WmNewsMaterialMapper extends BaseMapper{ void saveRelations(@Param("materialIds") List materialIds,@Param("newsId") Integer newsId, @Param("type")Short type); }
WmNewsMaterialMapper.xml
insert into wm_news_material (material_id,news_id,type,ord) values (#{mid},#{newsId},#{type},#{ord})
③:常量类准备
package com.heima.common.constants; public class WemediaConstants { public static final Short COLLECT_MATERIAL = 1;//收藏 public static final Short CANCEL_COLLECT_MATERIAL = 0;//取消收藏 public static final String WM_NEWS_TYPE_IMAGE = "image"; public static final Short WM_NEWS_NONE_IMAGE = 0; public static final Short WM_NEWS_SINGLE_IMAGE = 1; public static final Short WM_NEWS_MANY_IMAGE = 3; public static final Short WM_NEWS_TYPE_AUTO = -1; public static final Short WM_CONTENT_REFERENCE = 0; public static final Short WM_COVER_REFERENCE = 1; }
④:在WmNewsService中新增方法
/** * 发布文章或保存草稿 * @param dto * @return */ public ResponseResult submitNews(WmNewsDto dto);
实现方法:
/** * 发布修改文章或保存为草稿 * @param dto * @return */ @Override public ResponseResult submitNews(WmNewsDto dto) { //0.条件判断 if(dto == null || dto.getContent() == null){ return ResponseResult.errorResult(AppHttpCodeEnum.PARAM_INVALID); } //1.保存或修改文章 WmNews wmNews = new WmNews(); //属性拷贝 属性名词和类型相同才能拷贝 BeanUtils.copyProperties(dto,wmNews); //封面图片 list---> string if(dto.getImages() != null && dto.getImages().size() > 0){ //[1dddfsd.jpg,sdlfjldk.jpg]--> 1dddfsd.jpg,sdlfjldk.jpg String imageStr = StringUtils.join(dto.getImages(), ","); wmNews.setImages(imageStr); } //如果当前封面类型为自动 -1 if(dto.getType().equals(WemediaConstants.WM_NEWS_TYPE_AUTO)){ wmNews.setType(null); } saveOrUpdateWmNews(wmNews); //2.判断是否为草稿 如果为草稿结束当前方法 if(dto.getStatus().equals(WmNews.Status.NORMAL.getCode())){ return ResponseResult.okResult(AppHttpCodeEnum.SUCCESS); } //3.不是草稿,保存文章内容图片与素材的关系 //获取到文章内容中的图片信息 Listmaterials = ectractUrlInfo(dto.getContent()); saveRelativeInfoForContent(materials,wmNews.getId()); //4.不是草稿,保存文章封面图片与素材的关系,如果当前布局是自动,需要匹配封面图片 saveRelativeInfoForCover(dto,wmNews,materials); return ResponseResult.okResult(AppHttpCodeEnum.SUCCESS); } /** * 第一个功能:如果当前封面类型为自动,则设置封面类型的数据 * 匹配规则: * 1,如果内容图片大于等于1,小于3 单图 type 1 * 2,如果内容图片大于等于3 多图 type 3 * 3,如果内容没有图片,无图 type 0 * * 第二个功能:保存封面图片与素材的关系 * @param dto * @param wmNews * @param materials */ private void saveRelativeInfoForCover(WmNewsDto dto, WmNews wmNews, List materials) { List images = dto.getImages(); //如果当前封面类型为自动,则设置封面类型的数据 if(dto.getType().equals(WemediaConstants.WM_NEWS_TYPE_AUTO)){ //多图 if(materials.size() >= 3){ wmNews.setType(WemediaConstants.WM_NEWS_MANY_IMAGE); images = materials.stream().limit(3).collect(Collectors.toList()); }else if(materials.size() >= 1 && materials.size() < 3){ //单图 wmNews.setType(WemediaConstants.WM_NEWS_SINGLE_IMAGE); images = materials.stream().limit(1).collect(Collectors.toList()); }else { //无图 wmNews.setType(WemediaConstants.WM_NEWS_NONE_IMAGE); } //修改文章 if(images != null && images.size() > 0){ wmNews.setImages(StringUtils.join(images,",")); } updateById(wmNews); } if(images != null && images.size() > 0){ saveRelativeInfo(images,wmNews.getId(),WemediaConstants.WM_COVER_REFERENCE); } } /** * 处理文章内容图片与素材的关系 * @param materials * @param newsId */ private void saveRelativeInfoForContent(List materials, Integer newsId) { saveRelativeInfo(materials,newsId,WemediaConstants.WM_CONTENT_REFERENCE); } @Autowired private WmMaterialMapper wmMaterialMapper; /** * 保存文章图片与素材的关系到数据库中 * @param materials * @param newsId * @param type */ private void saveRelativeInfo(List materials, Integer newsId, Short type) { if(materials!=null && !materials.isEmpty()){ //通过图片的url查询素材的id List dbMaterials = wmMaterialMapper.selectList(Wrappers. lambdaQuery().in(WmMaterial::getUrl, materials)); //判断素材是否有效 if(dbMaterials==null || dbMaterials.size() == 0){ //手动抛出异常 第一个功能:能够提示调用者素材失效了,第二个功能,进行数据的回滚 throw new CustomException(AppHttpCodeEnum.MATERIASL_REFERENCE_FAIL); } if(materials.size() != dbMaterials.size()){ throw new CustomException(AppHttpCodeEnum.MATERIASL_REFERENCE_FAIL); } List idList = dbMaterials.stream().map(WmMaterial::getId).collect(Collectors.toList()); //批量保存 wmNewsMaterialMapper.saveRelations(idList,newsId,type); } } /** * 提取文章内容中的图片信息 * @param content * @return */ private List ectractUrlInfo(String content) { List materials = new ArrayList<>(); List
④:控制器
@PostMapping("/submit") public ResponseResult submitNews(@RequestBody WmNewsDto dto){ return wmNewsService.submitNews(dto); }
《黑马头条》 内容安全 自动审核 feign 延迟任务精准发布 kafka_软工菜鸡的博客-CSDN博客