项目

技术栈

docker安装nacos

shelldocker run --env MODE=standalone --name nacos --restart=always -d -p 8848:8848 nacos/nacos-server:1.2.0
导入初始项目
完成登录功能开发
表结构

ap_user

数据盐salt

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

bootstrap.ymlserver: 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-userspring: 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>
登录接口说明

导入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
javapackage 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自动装配
configorg.springframework.boot.autoconfigure.EnableAutoConfiguration=\ com.heima.common.exception.ExceptionCatch,\ com.heima.common.swagger.SwaggerConfiguration
在微服务controller层添加相关接口信息
javapackage 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添加相关介绍
javapackage 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>
添加配置
javapackage 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();
}
}

在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.ymlserver: 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
configspring: 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服务

添加认证过滤器校验token

添加全局过滤器代码
javapackage 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;
}
}
导入文章数据库

拷贝文章相关实体类

垂直分表

定义接口

创建ApArticleDto实体类
javapackage 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的配置文件
configspring: 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
创建加载首页数据 更新最新数据 加载更多数据 接口
javapackage 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层
javapackage 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);
}
}
静态模板展示

freemaker

搭建freemaker快速入门
创建freemaker-demo项目
导入配置
XMLserver: port: 8881 #服务端口 spring: application: name: freemarker-demo #指定服务名 freemarker: cache: false #关闭模板缓存,方便测试 settings: template_update_delay: 0 #检查模板更新延迟时间,设置为0表示立即检查,如果时间大于0会有缓存不方便进行模板测试 suffix: .ftl #指定Freemarker模板文件的后缀名
编写controller ftl entity
javapackage 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基础语法种类

集合指令 list

map数据展示


模板中进行判断

比较运算符

空值处理


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

内建函数


freemaker输出静态文件


docker创建minio

shellsudo mkdir -p /home/minio/{data,config} sudo chmod -R 755 /home/minio
使用老版本全部功能镜像
shelldocker 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"
基本概念

封装MinIO为starter


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

编写测试代码上传静态文件到MinIO服务中
javapackage 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 许可协议。转载请注明出处!