编辑
2025-11-11
黑马头条
00

目录

1.阶段1
2.app网关搭建
3.文章展示
4.文章详情
5.MinIO分布式文件系统

1.阶段1

项目

image.png

技术栈

image.png

docker安装nacos

image.png

shell
docker run --env MODE=standalone --name nacos --restart=always -d -p 8848:8848 nacos/nacos-server:1.2.0

导入初始项目

完成登录功能开发

表结构

image.png

ap_user

image.png

数据盐salt

image.png

在heima-service创建user服务并创建mapper service controller config 和相关的配置文件

image.png

bootstrap.yml
server: port: 51801 spring: application: name: leadnews-user cloud: nacos: discovery: server-addr: 192.168.42.10:8848 config: server-addr: 192.168.42.10:8848 file-extension: yml

在nacos中创建leadnews-user的配置文件

leadnews-user
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: qpj@123456 mybatis-plus: mapper-locations: classpath*:mapper/*.xml type-aliases-package: com.heima.model.user.pojos

日志logback.xml文件

XML
<?xml version="1.0" encoding="UTF-8"?> <configuration> <!--定义日志文件的存储地址,使用绝对路径--> <property name="LOG_HOME" value="e:/logs"/> <!-- Console 输出设置 --> <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender"> <encoder> <!--格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度%msg:日志消息,%n是换行符--> <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern> <charset>utf8</charset> </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}.log</fileNamePattern> </rollingPolicy> <encoder> <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern> </encoder> </appender> <!-- 异步输出 --> <appender name="ASYNC" class="ch.qos.logback.classic.AsyncAppender"> <!-- 不丢失日志.默认的,如果队列的80%已满,则会丢弃TRACT、DEBUG、INFO级别的日志 --> <discardingThreshold>0</discardingThreshold> <!-- 更改默认的队列的深度,该值会影响性能.默认值为256 --> <queueSize>512</queueSize> <!-- 添加附加的appender,最多只能添加一个 --> <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="ASYNC"/>--> <appender-ref ref="FILE"/> <appender-ref ref="CONSOLE"/> </root> </configuration>

登录接口说明

image.png

导入swagger

POM
<!-- swagger--> <dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger2</artifactId> </dependency> <dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger-ui</artifactId> </dependency>

配置Swagger

java
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(); } }

将Swaggerconfigration添加进springboot自动装配

config
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ com.heima.common.exception.ExceptionCatch,\ com.heima.common.swagger.SwaggerConfiguration

在微服务controller层添加相关接口信息

java
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 io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; 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") @Api(value = "app用户登录",tags = "app用户登录") public class ApUserLoginController { @Autowired private ApUserService apUserService; @PostMapping("login_auth") @ApiOperation("用户登录") public ResponseResult login(@RequestBody LoginDto dto){ return apUserService.login(dto); } }

model添加相关介绍

java
package com.heima.model.user.dtos; import io.swagger.annotations.ApiModelProperty; import lombok.Data; @Data public class LoginDto { /** * 手机号 */ @ApiModelProperty(value = "手机号",required = true) private String phone; /** * 密码 */ @ApiModelProperty(value = "密码",required = true) private String password; }

配置knife4j

pom
<dependency> <groupId>com.github.xiaoymin</groupId> <artifactId>knife4j-spring-boot-starter</artifactId> </dependency>

添加配置

java
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(); } }

2.app网关搭建

image.png

在gateway服务中导入网关依赖

pom
<dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-gateway</artifactId> </dependency> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> </dependency> </dependencies>

创建app网关服务 添加配置和nacos配置

bootstrap.yml
server: port: 51601 spring: application: name: leadnews-app-gateway cloud: nacos: discovery: server-addr: 192.168.42.10:8848 config: server-addr: 192.168.42.10:8848 file-extension: yml

nacos

config
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

向网关发送请求请求到user服务

image.png

添加认证过滤器校验token

image.png

添加全局过滤器代码

java
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 Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { // 获取request和response ServerHttpRequest request = exchange.getRequest(); ServerHttpResponse response = exchange.getResponse(); // 判断是否是登录 if (request.getURI().getPath().contains("login")){ // 放行 return chain.filter(exchange); } // 获取token String token = request.getHeaders().getFirst("token"); // 判断token是否存在 if (StringUtils.isBlank(token)){ response.setStatusCode(HttpStatus.UNAUTHORIZED); // 结束请求 return response.setComplete(); } try{ // 判断token是否有效 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(); } // 放行 return chain.filter(exchange); } /** * 优先级设置 * 值越小优先级越高 * @return */ @Override public int getOrder() { return 0; } }

3.文章展示

导入文章数据库

image.png

拷贝文章相关实体类

image.png

垂直分表

image.png

定义接口

image.png

创建ApArticleDto实体类

java
package com.heima.model.article.dtos; import lombok.Data; import java.util.Date; @Data public class ApArticleDto { /** * 最大时间 */ Date maxBehotTime; /** * 最小时间 */ Date minBehotTime; /** * 分页size */ Integer size; /** * 频道ID */ String tag; }

在nacos注册中心添加leadnew-article的配置文件

config
spring: datasource: driver-class-name: com.mysql.jdbc.Driver url: jdbc:mysql://localhost:3306/leadnews_article?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC&useSSL=false username: root password: qpj@123456 mybatis-plus: mapper-locations: classpath*:mapper/*.xml type-aliases-package: com.heima.model.article.pojos

创建加载首页数据 更新最新数据 加载更多数据 接口

java
package com.heima.article.controller.v1; import com.heima.model.article.dtos.ArticleHomeDto; import com.heima.model.common.dtos.ResponseResult; import io.swagger.annotations.Api; import org.apache.commons.net.nntp.Article; 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 { /** * 加载首页接口 * @param dto * @return */ @PostMapping("/load") public ResponseResult load(@RequestBody ArticleHomeDto dto) { return null; } /** * 加载首页更多接口 * @param dto * @return */ @PostMapping("/loadmore") public ResponseResult loadmore(@RequestBody ArticleHomeDto dto) { return null; } /** * 加载首页最新接口 * @param dto * @return */ @PostMapping("/loadnew") public ResponseResult loadnew(@RequestBody ArticleHomeDto dto) { return null; } }

使用mybatis-plus实现service mapper 并编写xml文件

XML
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.heima.article.mapper.ApArticleMapper"> <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 <!-- loadmore --> <if test="type != null and type == 1"> and aa.publish_time <![CDATA[<]]> #{dto.minBehotTime} </if> <if test="type != null and type == 2"> and aa.publish_time <![CDATA[>]]> #{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> </mapper>

编写service层

java
package com.heima.article.service.impl; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.heima.article.ArticleAppliction; 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.lang.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 @Slf4j @Transactional public class ApArticleServiceImpl extends ServiceImpl<ApArticleMapper, ApArticle> implements ApArticleService { @Autowired private ApArticleMapper apArticleMapper; private final static Short MAX_PAGE_SIZE = 50; /** * 加载文章列表实现 * @param dto * @param type 1加载更多 2加载最新 * @return */ @Override public ResponseResult load(ArticleHomeDto dto, Short type) { // 参数校验 // 1.分页条数校验 Integer size = dto.getSize(); if (size == null || size <= 0) { size = 10; } size = Math.min(size,MAX_PAGE_SIZE); // 2.type校验 if (!type.equals(ArticleConstants.LOADTYPE_LOAD_MORE) && !type.equals(ArticleConstants.LOADTYPE_LOAD_NEW)){ type = ArticleConstants.LOADTYPE_LOAD_MORE; } // 3.频道参数校验 if (StringUtils.isBlank(dto.getTag())){ dto.setTag(ArticleConstants.DEFAULT_TAG); } // 4.时间校验 if (dto.getMaxBehotTime() == null){ dto.setMaxBehotTime(new Date()); } if (dto.getMinBehotTime() == null){ dto.setMinBehotTime(new Date()); } // 查询 List<ApArticle> apArticles = apArticleMapper.loadArticleList(dto, type); // 结果返回 return ResponseResult.okResult(apArticles); } }

4.文章详情

静态模板展示

image.png

freemaker

image.png

搭建freemaker快速入门

创建freemaker-demo项目

导入配置

XML
server: port: 8881 #服务端口 spring: application: name: freemarker-demo #指定服务名 freemarker: cache: false #关闭模板缓存,方便测试 settings: template_update_delay: 0 #检查模板更新延迟时间,设置为0表示立即检查,如果时间大于0会有缓存不方便进行模板测试 suffix: .ftl #指定Freemarker模板文件的后缀名

编写controller ftl entity

java
package com.heima.freemaker.controller; import com.heima.freemaker.entity.Student; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.GetMapping; @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"; } }
java
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>Hello World!</title> </head> <body> <b>普通文本 String 展示:</b><br><br> Hello ${name} <br> <hr> <b>对象Student中的数据展示:</b><br/> 姓名:${stu.name}<br/> 年龄:${stu.age} <hr> </body> </html>

freemaker基础语法种类

image.png

集合指令 list

image.png

map数据展示

image.png

image.png

模板中进行判断

image.png

比较运算符

image.png

空值处理

image.png

image.png

数据为空则替换成‘|’后的文本

image.png

内建函数

image.png

image.png

freemaker输出静态文件

image.png

5.MinIO分布式文件系统

image.png

docker创建minio

image.png

shell
sudo mkdir -p /home/minio/{data,config} sudo chmod -R 755 /home/minio

使用老版本全部功能镜像

shell
docker run -d --name minio -p 9000:9000 -p 9001:9001 \ -e "MINIO_ROOT_USER=admin" \ -e "MINIO_ROOT_PASSWORD=admin123" \ -v /home/minio/data:/data \ -v /home/minio/config:/root/.minio \ --restart=always \ minio/minio:RELEASE.2025-04-22T22-12-26Z server /data --console-address ":9001"

基本概念

image.png

封装MinIO为starter

image.png

image.png

上传文章详情的静态文件到MinIO中

导入文章详情的模板文并件上传css和js静态文件到MinIO中

image.png

编写测试代码上传静态文件到MinIO服务中

java
package com.heima.article; import com.alibaba.fastjson.JSONArray; import com.baomidou.mybatisplus.core.toolkit.StringUtils; import com.baomidou.mybatisplus.core.toolkit.Wrappers; import com.heima.article.mapper.ApArticleContentMapper; import com.heima.article.service.ApArticleService; 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 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.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; import java.io.StringWriter; import java.util.HashMap; import java.util.Map; @SpringBootTest(classes = ArticleAppliction.class) @RunWith(SpringRunner.class) public class ArticleUploadTest { @Autowired private Configuration configuration; @Autowired private FileStorageService fileStorageService; @Autowired private ApArticleContentMapper apArticleContentMapper; @Autowired private ApArticleService apArticleService; @Test public void uploadArticle() throws IOException, TemplateException { ApArticleContent apArticleContent = apArticleContentMapper.selectOne(Wrappers.<ApArticleContent>lambdaQuery().eq(ApArticleContent::getArticleId,1302862387124125698L)); if(apArticleContent != null && StringUtils.isNotBlank(apArticleContent.getContent())){ Template template = configuration.getTemplate("article.ftl"); Map<String, Object> content = new HashMap<>(); content.put("content", JSONArray.parse(apArticleContent.getContent())); StringWriter out = new StringWriter(); template.process(content,out); InputStream in = new ByteArrayInputStream(out.toString().getBytes()); String path = fileStorageService.uploadHtmlFile("",apArticleContent.getArticleId()+".html",in); apArticleService.update(Wrappers.<ApArticle>lambdaUpdate().eq(ApArticle::getId,apArticleContent.getArticleId()) .set(ApArticle::getStaticUrl,path)); } } }

本文作者:钱小杰

本文链接:

版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!