使用docker部署nacos
docker拉取镜像
docker pull nacos/nacos-server:1.2.0
创建nacos的docker容器(开机启动)
docker run --env MODE=standalone --name nacos --restart=always -d -p 8848:8848 nacos/nacos-server:1.2.0
访问nacos
http://192.168.142.100:8848/nacos
导入基础项目,以及导入数据库和相关表
配置文件编码、maven仓库等信息。
注意使用资料的maven仓库,不然有问题。
在heima-headnews-service下添加模块heima-headnews-user,并创建config、mapper、service、controller.v1包。
模块添加启动类
@SpringBootApplication
@EnableDiscoveryClient
@MapperScan("com.heima.user.mapper")
public class UserApplication {
public static void main(String[] args) {
SpringApplication.run(UserApplication.class,args);
}
}
添加配置文件
logback日志配置
<configuration>
<property name="LOG_HOME" value="C:\Users\ASUS\Desktop\logs"/>
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%npattern>
<charset>utf8charset>
encoder>
appender>
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${LOG_HOME}/leadnews.%d{yyyy-MM-dd}.logfileNamePattern>
rollingPolicy>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%npattern>
encoder>
appender>
<appender name="ASYNC" class="ch.qos.logback.classic.AsyncAppender">
<discardingThreshold>0discardingThreshold>
<queueSize>512queueSize>
<appender-ref ref="FILE"/>
appender>
<logger name="org.apache.ibatis.cache.decorators.LoggingCache" level="DEBUG" additivity="false">
<appender-ref ref="CONSOLE"/>
logger>
<logger name="org.springframework.boot" level="debug"/>
<root level="info">
<appender-ref ref="FILE"/>
<appender-ref ref="CONSOLE"/>
root>
configuration>
user模块配置
server:
port: 51801
spring:
application:
name: leadnews-user
cloud:
nacos:
discovery:
server-addr: 192.168.142.100:8848
config:
server-addr: 192.168.142.100:8848
file-extension: yml
nacos配置中心user服务配置
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/leadnews_user?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC
username: root
password: 1234
# 设置Mapper接口所对应的XML文件位置,如果你在Mapper接口中有自定义方法,需要进行该配置
mybatis-plus:
mapper-locations: classpath*:mapper/*.xml
# 设置别名包扫描路径,通过该属性可以给包中的类注册别名
type-aliases-package: com.heima.model.user.pojos
业务实现--------------------------(小技巧:注释都写正常的流程)
//app端用户登录功能
@Override
public ResponseResult login(LoginDto dto) {
HashMap<String, Object> map = new HashMap<>();//返回结果的map
// 1. 用户登录
if (!StringUtils.isBlank(dto.getPhone()) || !StringUtils.isBlank(dto.getPassword())) {
// 1.1获取登录用户
ApUser user = getOne(Wrappers.<ApUser>lambdaQuery().eq(ApUser::getPhone, dto.getPhone()));
if (user == null){
return ResponseResult.errorResult(AppHttpCodeEnum.DATA_NOT_EXIST,"用户不存在!");
}
// 1.2验证密码
String salt = user.getSalt();
String password = dto.getPassword();
String pswd = DigestUtils.md5DigestAsHex((password + salt).getBytes());//加密用户输入的密码
if (!pswd.equals(user.getPassword())){
return ResponseResult.errorResult(AppHttpCodeEnum.LOGIN_PASSWORD_ERROR);
}
// 1.3生成token,返回数据
String token = AppJwtUtil.getToken(user.getId().longValue());
map.put("token",token);
user.setSalt("");
user.setPassword("");
map.put("user",user);
return ResponseResult.okResult(map);
}
// 2. 用户不登陆
map.put("token",AppJwtUtil.getToken(0L));//游客token的id都是0
return ResponseResult.okResult(map);
}
在common和model模块都添加swagger依赖
<dependency>
<groupId>io.springfoxgroupId>
<artifactId>springfox-swagger2artifactId>
dependency>
<dependency>
<groupId>io.springfoxgroupId>
<artifactId>springfox-swagger-uiartifactId>
dependency>
@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();
}
}
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.heima.common.exception.ExceptionCatch,\
com.heima.common.swagger.SwaggerConfiguration
在Java类中添加Swagger的注解即可生成Swagger接口文档,常用Swagger注解如下:
@Api:修饰整个类,描述Controller的作用
@ApiOperation:描述一个类的一个方法,或者说一个接口
@ApiParam:单个参数的描述信息
@ApiModel:用对象来接收参数
@ApiModelProperty:用对象接收参数时,描述对象的一个字段
@ApiResponse:HTTP响应其中1个描述
@ApiResponses:HTTP响应整体描述
@ApiIgnore:使用该注解忽略这个API
@ApiError :发生错误返回的信息
@ApiImplicitParam:一个请求参数
@ApiImplicitParams:多个请求参数的描述信息
controller层添加注解
@RestController
@RequestMapping("/api/v1/login")
@Api(value = "app端用户登录", tags = "ap_user", description = "app端用户登录API")
public class ApUserLoginController {
@Resource
private ApUserService apUserService;
//app端用户登录功能
@PostMapping("/login_auth")
@ApiOperation("用户登录")
public ResponseResult login(@RequestBody LoginDto dto){
return apUserService.login(dto);
}
}
实体类添加注解
@Data
public class LoginDto {
//手机号
@ApiModelProperty(value = "手机号")
private String phone;
//密码
@ApiModelProperty(value = "密码")
private String password;
}
启动user微服务,访问地址:http://localhost:51801/swagger-ui.html
common中导入即可
<dependency>
<groupId>com.github.xiaoymingroupId>
<artifactId>knife4j-spring-boot-starterartifactId>
dependency>
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();
}
}
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.heima.common.exception.ExceptionCatch,\
com.heima.common.swagger.SwaggerConfiguration,\
com.heima.common.swagger.Swagger2Configuration
在浏览器输入地址:http://localhost:51801/doc.html
geteway模块中
<dependencies>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-gatewayartifactId>
dependency>
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discoveryartifactId>
dependency>
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-nacos-configartifactId>
dependency>
<dependency>
<groupId>io.jsonwebtokengroupId>
<artifactId>jjwtartifactId>
dependency>
dependencies>
app-gateway配置文件bootstrap.yml
server:
port: 51601
spring:
application:
name: leadnews-app-gateway
cloud:
nacos:
discovery:
server-addr: 192.168.142.100:8848
config:
server-addr: 192.168.142.100:8848
file-extension: yml
注册中心网关配置
首先,在spring.cloud.gateway
下的gateway
属性中,我们定义了全局的CORS跨域请求配置。
globalcors属性指定了将CORS配置添加到简单URL处理程序映射中。
corsConfigurations属性定义了实际的CORS规则。在这里,使用通配符[/**]
匹配所有的URL路径,然后设置了允许的请求头和来源,以及允许的请求方法。
然后,在routes属性中,我们定义了路由规则。凡是以/user开头的请求都转发到leadnews-user服务处理,lb://是负载均衡。然后StripPrefix过滤规则会去除前缀/user。
这段配置的作用是在Spring Cloud Gateway中实现了CORS跨域请求的配置和路由规则的定义,使请求可以正确地被转发到后端服务中。
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
@SpringBootApplication
@EnableDiscoveryClient //开启注册中心
public class AppGatewayApplication {
public static void main(String[] args) {
SpringApplication.run(AppGatewayApplication.class,args);
}
}
请求地址:http://localhost:51601/user/api/v1/login/login_auth
如果可以登录成功,说明网关搭建成功
记得添加一个jwt的工具类到app的网关里
@Component
@Slf4j
public class AuthorizeFilter implements Ordered, GlobalFilter {
@Override
public Mono<Void> filter(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)){
//token不存在设置状态码返回
response.setStatusCode(HttpStatus.UNAUTHORIZED);
return response.setComplete();
}
try {
Claims claimsBody = AppJwtUtil.getClaimsBody(token);
int res = AppJwtUtil.verifyToken(claimsBody);
//token过期了返回
if (res == 1 || res == 2){
response.setStatusCode(HttpStatus.UNAUTHORIZED);
return response.setComplete();
}
} catch (Exception e) {
e.printStackTrace();
//解析失败也返回
response.setStatusCode(HttpStatus.UNAUTHORIZED);
return response.setComplete();
}
// 5.放行
return chain.filter(exchange);
}
//过滤器的优先级,数值越小,优先级越高
@Override
public int getOrder() {
return 0;
}
}
在nginx安装的conf目录下新建一个文件夹leadnews.conf
,在当前文件夹中新建heima-leadnews-app.conf
文件
heima-leadnews-app.conf配置如下:
upstream heima-app-gateway{
server localhost:51601;
}
server {
listen 8801;
location / {
root html/app-web/;
index index.html;
}
location ~/app/(.*) {
proxy_pass http://heima-app-gateway/$1;
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 -s reload
再重新启动nginx
文章相关表都进行了垂直分表
垂直分表:将一个表的字段分散到多个表中,每个表存储其中一部分字段。
优势:
减少IO争抢,减少锁表的几率,查看文章概述与文章详情互不影响
充分发挥高频数据的操作效率,对文章概述数据操作的高效率不会被操作文章详情数据的低效率所拖累。
拆分规则:
把不常用的字段单独放在一张表
把text,blob等大字段拆分出来单独放在一张表
经常组合查询的字段单独放在一张表中
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/leadnews_article?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC
username: root
password: 1234
# 设置Mapper接口所对应的XML文件位置,如果你在Mapper接口中有自定义方法,需要进行该配置
mybatis-plus:
mapper-locations: classpath*:mapper/*.xml
# 设置别名包扫描路径,通过该属性可以给包中的类注册别名
type-aliases-package: com.heima.model.article.pojos
文章dto类
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;
}
常量类(common中添加)
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__";
}
mapper接口
@Repository
public interface ApArticleMapper extends BaseMapper<ApArticle> {
//根据type类型加载数据,1表示加载更多,2表示加载最新
List<ApArticle> load(@Param("dto") ArticleHomeDto dto,@Param("type") Short type);
}
mapper映射文件
是用于转义的
<resultMap id="resultMap" type="com.heima.model.article.pojos.ApArticle">
<id column="id" property="id"/>
<result column="title" property="title"/>
<result column="author_id" property="authorId"/>
<result column="author_name" property="authorName"/>
<result column="channel_id" property="channelId"/>
<result column="channel_name" property="channelName"/>
<result column="layout" property="layout"/>
<result column="flag" property="flag"/>
<result column="images" property="images"/>
<result column="labels" property="labels"/>
<result column="likes" property="likes"/>
<result column="collection" property="collection"/>
<result column="comment" property="comment"/>
<result column="views" property="views"/>
<result column="province_id" property="provinceId"/>
<result column="city_id" property="cityId"/>
<result column="county_id" property="countyId"/>
<result column="created_time" property="createdTime"/>
<result column="publish_time" property="publishTime"/>
<result column="sync_status" property="syncStatus"/>
<result column="static_url" property="staticUrl"/>
resultMap>
<select id="loadArticleList" resultMap="resultMap">
SELECT
aa.*
FROM
`ap_article` aa
LEFT JOIN ap_article_config aac ON aa.id = aac.article_id
<where>
and aac.is_delete != 1
and aac.is_down != 1
<if test="type != null and type == 1">
and aa.publish_time #{dto.minBehotTime}
if>
<if test="type != null and type == 2">
and aa.publish_time ]]> #{dto.maxBehotTime}
if>
<if test="dto.tag != '__all__'">
and aa.channel_id = #{dto.tag}
if>
where>
order by aa.publish_time desc
limit #{dto.size}
select>
业务代码
@Resource
private ApArticleMapper apArticleMapper;
// 单页最大加载的数字
private final static short MAX_PAGE_SIZE = 50;
/**
* 根据类型加载文章
*
* @param loadtype
* @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<ApArticle> apArticles = apArticleMapper.loadArticleList(dto, loadtype);
//3.结果封装
ResponseResult responseResult = ResponseResult.okResult(apArticles);
return responseResult;
}
controller层
@RestController
@RequestMapping("/api/v1/article")
public class ArticleHomeController {
@Resource
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);
}
}
网关配置文件添加文章服务的路由
# 文章管理
- id: article
uri: lb://leadnews-article
predicates:
- Path=/article/**
filters:
- StripPrefix= 1
1.在artile微服务中添加MinIO(是我们自定义的starter)和freemarker的支持
添加依赖
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-freemarkerartifactId>
dependency>
<dependency>
<groupId>com.heimagroupId>
<artifactId>heima-file-starterartifactId>
<version>1.0-SNAPSHOTversion>
dependency>
添加配置
freemarker:
cache: false #关闭模板缓存,方便测试
settings:
template_update_delay: 0 #检查模板更新延迟时间,设置为0表示立即检查,如果时间大于0会有缓存不方便进行模板测试
suffix: .ftl #指定Freemarker模板文件的后缀名
minio:
accessKey: miniouser
secretKey: miniouser
bucket: leadnews
endpoint: http://192.168.142.100:9000
readPath: http://192.168.142.100:900
2.资料中找到模板文件(article.ftl)拷贝到article微服务下
3.资料中找到index.js和index.css两个文件上传到MinIO中
4.新建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<ApArticleContent> {
}
6.在artile微服务中新增测试类(后期新增文章的时候创建详情静态页,目前暂时手动生成)
package com.heima.article.test;
import com.alibaba.fastjson.JSONArray;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.heima.article.ArticleApplication;
import com.heima.article.mapper.ApArticleContentMapper;
import com.heima.article.mapper.ApArticleMapper;
import com.heima.file.service.FileStorageService;
import com.heima.model.article.pojos.ApArticle;
import com.heima.model.article.pojos.ApArticleContent;
import freemarker.template.Configuration;
import freemarker.template.Template;
import org.apache.commons.lang3.StringUtils;
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.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;
//根据文章id查询文章内容,利用freemarker模板引擎生成html文件,上传至minio,并将静态文件访问路径设置给文章StaticUrl属性
@Test
public void createStaticUrlTest() throws Exception {
//1.获取文章内容
ApArticleContent apArticleContent = apArticleContentMapper.selectOne(Wrappers.<ApArticleContent>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");
Map<String, Object> params = new HashMap<>();
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);
}
}
}