javascript
《黑马头条》SpringBoot+SpringCloud+ Nacos等企业级微服务架构项目
01環境搭建、SpringCloud微服務(注冊發現、服務調用、網關)
1)課程對比
2)項目概述
2.1)能讓你收獲什么
2.2)項目課程大綱
2.3)項目概述
隨著智能手機的普及,人們更加習慣于通過手機來看新聞。由于生活節奏的加快,很多人只能利用碎片時間來獲取信息,因此,對于移動資訊客戶端的需求也越來越高。
黑馬頭條項目正是在這樣背景下開發出來。黑馬頭條項目采用當下火熱的微服務+大數據技術架構實現。本項目主要著手于獲取最新最熱新聞資訊,通過大數據分析用戶喜好精確推送咨詢新聞
2.4)項目術語
2.5)業務說明
項目演示地址:
平臺管理:http://heima-admin-java.research.itcast.cn
自媒體:http://heime-media-java.research.itcast.cn
app端:http://heima-app-java.research.itcast.cn
平臺管理與自媒體為PC端,用電腦瀏覽器打開即可。
其中app端為移動端,打開方式有兩種:
谷歌瀏覽器打開,調成移動端模式
手機瀏覽器打開或掃描右側二維碼
3)技術棧
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 : 靜態代碼掃描工具,在項目中掃描項目代碼,檢查異常點、優化點、代碼規范等,為開發團隊提供規范統一,提升項目代碼質量
4)nacos環境搭建
4.1)虛擬機鏡像準備
1)打開當天資料文件中的鏡像,拷貝到一個地方,然后解壓
2)解壓后,雙擊ContOS7-hmtt.vmx文件,前提是電腦上已經安裝了VMware
修改虛擬網絡地址(NAT)
①,選中VMware中的編輯
②,選擇虛擬網絡編輯器
③,找到NAT網卡,把網段改為200(當前掛載的虛擬機已固定ip地址)
4)修改虛擬機的網絡模式為NAT
5)啟動虛擬機,用戶名:root 密碼:itcast,當前虛擬機的ip已手動固定(靜態IP), 地址為:192.168.200.130
6)使用FinalShell客戶端鏈接
4.2)nacos安裝
①: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.0MODE=standalone 單機版
--restart=always 開機啟動
-p 8848:8848 映射端口
-d 創建一個守護式容器在后臺運行
③:訪問地址:http://192.168.200.130:8848/nacos
5)初始工程搭建
5.1)環境準備
①:項目依賴環境(需提前安裝好)
JDK1.8
Intellij Idea
maven-3.6.1
Git
②:在當天資料中解壓heima-leadnews.zip文件,拷貝到 沒有中文和空格的目錄,使用idea打開即可
③:IDEA開發工具配置
設置本地倉庫,建議使用資料中提供好的倉庫
④:設置項目編碼格式
5.2)主體結構
6)登錄
6.1)需求分析
戶點擊開始使用
登錄后的用戶權限較大,可以查 看,也可以操作(點贊,關注,評論)
用戶點擊不登錄,先看看
游客只有查看的權限
6.2)表結構分析
關于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;/*** <p>* APP用戶信息表* </p>** @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)處理
注冊->生成鹽
登錄->使用鹽來配合驗證
6.3)思路分析
1,用戶輸入了用戶名和密碼進行登錄,校驗成功后返回jwt(基于當前用戶的id生成)
2,用戶游客登錄,生成jwt返回(基于默認值0生成)‘
6.4)運營端微服務搭建
在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-usercloud:nacos:discovery:server-addr: 192.168.200.130:8848config:server-addr: 192.168.200.130:8848file-extension: yml在nacos中創建配置文件
spring:datasource:driver-class-name: com.mysql.jdbc.Driverurl: jdbc:mysql://localhost:3306/leadnews_user?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTCusername: rootpassword: 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
<?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>6.4)登錄功能實現
①:接口定義
@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<ApUser> { }③:業務層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<ApUser>{/*** 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<ApUserMapper, ApUser> implements ApUserService {@Overridepublic ResponseResult login(LoginDto dto) {//1.正常登錄(手機號+密碼登錄)if (!StringUtils.isBlank(dto.getPhone()) && !StringUtils.isBlank(dto.getPassword())) {//1.1查詢用戶ApUser apUser = getOne(Wrappers.<ApUser>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 返回數據 jwtMap<String, Object> map = 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 = 0Map<String, Object> 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 {@Autowiredprivate 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
7)接口工具postman、swagger、knife4j
7.1)postman
Postman是一款功能強大的網頁調試與發送網頁HTTP請求的Chrome插件。postman被500萬開發者和超100,000家公司用于每月訪問1.3億個API。
官方網址:https://www.postman.com/
解壓資料文件夾中的軟件,安裝即可
通常的接口測試查看請求和響應,下面是登錄請求的測試
http://localhost:51801/api/v1/login/login_auth
{"phone":"13511223456","password":"admin"}
7.2)swagger
(1)簡介
Swagger 是一個規范和完整的框架,用于生成、描述、調用和可視化 RESTful 風格的 Web 服務(https://swagger.io/)。 它的主要作用是:
使得前后端分離開發更加方便,有利于團隊協作
接口的文檔在線自動生成,降低后端開發人員編寫接口文檔的負擔
功能測試
Spring已經將Swagger納入自身的標準,建立了Spring-swagger項目,現在叫Springfox。
通過在項目中引入Springfox ,即可非常簡單快捷的使用Swagger。
(2)SpringBoot集成Swagger
引入依賴,在heima-leadnews-model和heima-leadnews-common模塊中引入該依賴
只需要在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 {@Beanpublic 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 {@Autowiredprivate 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
7.3)knife4j
(1)簡介
knife4j是為Java MVC框架集成Swagger生成Api文檔的增強解決方案,前身是swagger-bootstrap-ui,取名kni4j是希望它能像一把匕首一樣小巧,輕量,并且功能強悍!
gitee地址:https://gitee.com/xiaoym/knife4j
官方文檔:https://doc.xiaominfo.com/
效果演示: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的依賴,如下:
創建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
8)網關
(1)在heima-leadnews-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>(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-gatewaycloud:nacos:discovery:server-addr: 192.168.200.130:8848config:server-addr: 192.168.200.130:8848file-extension: yml在nacos的配置中心創建dataid為leadnews-app-gateway的yml配置
spring:cloud:gateway:globalcors:add-to-simple-url-handler-mapping: truecorsConfigurations:'[/**]':allowedHeaders: "*"allowedOrigins: "*"allowedMethods:- GET- POST- DELETE- PUT- OPTIONroutes:# 平臺管理- id: useruri: lb://leadnews-userpredicates:- Path=/user/**filters:- StripPrefix= 1環境搭建完成以后,啟動項目網關和用戶兩個服務,使用postman進行測試
請求地址:http://localhost:51601/user/api/v1/login/login_auth
1.3 全局過濾器實現jwt校驗
思路分析:
用戶進入網關開始登陸,網關過濾器進行判斷,如果是登錄,則路由到后臺管理微服務進行登錄
用戶登錄成功,后臺管理微服務簽發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 {@Overridepublic 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.獲取tokenString 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*/@Overridepublic int getOrder() {return 0;} }測試:
啟動user服務,繼續訪問其他微服務,會提示需要認證才能訪問,這個時候需要在heads中設置設置token才能正常訪問。
9)前端集成
9.1)前端項目部署思路
通過nginx來進行配置,功能如下
通過nginx的反向代理功能訪問后臺的網關資源
通過nginx的靜態服務器功能訪問前端靜態頁面
9.2)配置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/$1;proxy_set_header HOST $host; # 不改變源請求頭的值proxy_pass_request_body on; #開啟獲取請求體proxy_pass_request_headers on; #開啟獲取請求頭proxy_set_header X-Real-IP $remote_addr; # 記錄真實發出請求的客戶端IPproxy_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
用谷歌瀏覽器打開,調試移動端模式進行訪問
02app端文章查看,靜態化freemarker,分布式文件系統minIO
1)文章列表加載
1.1)需求分析
文章布局展示
1.2)表結構分析
ap_article 文章基本信息表
ap_article_config 文章配置表
ap_article_content 文章內容表
三張表關系分析
表的拆分—垂直分表
1.3)導入文章數據庫
1.3.1)導入數據庫
查看當天資料文件夾,在數據庫連接工具中執行leadnews_article.sql
1.3.2)導入對應的實體類
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;/*** <p>* 文章信息表,存儲已發布的文章* </p>** @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;/*** <p>* APP已發布文章配置表* </p>** @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.4)實現思路
1,在默認頻道展示10條文章信息
2,可以切換頻道查看不同種類文章
3,當用戶下拉可以加載最新的文章(分頁)本頁文章列表中發布時間為最大的時間為依據
4,當用戶上拉可以加載更多的文章信息(按照發布時間)本頁文章列表中發布時間最小的時間為依據
5,如果是當前頻道的首頁,前端傳遞默認參數:
maxBehotTime:0(毫秒)
minBehotTime:20000000000000(毫秒)--->2063年
1.5)接口定義
加載首頁 | 加載更多 | 加載最新 | |
接口路徑 | /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;// 分頁sizeInteger size;// 頻道IDString tag; }1.6)功能實現
1.6.1):導入heima-leadnews-article微服務,資料在當天的文件夾中
注意:需要在heima-leadnews-service的pom文件夾中添加子模塊信息,如下:
<modules><module>heima-leadnews-user</module><module>heima-leadnews-article</module> </modules>在idea中的maven中更新一下,如果工程還是灰色的,需要在重新添加文章微服務的pom文件,操作步驟如下:
需要在nacos中添加對應的配置
spring:datasource:driver-class-name: com.mysql.jdbc.Driverurl: jdbc:mysql://localhost:3306/leadnews_article?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTCusername: rootpassword: root # 設置Mapper接口所對應的XML文件位置,如果你在Mapper接口中有自定義方法,需要進行該配置 mybatis-plus:mapper-locations: classpath*:mapper/*.xml# 設置別名包掃描路徑,通過該屬性可以給包中的類注冊別名type-aliases-package: com.heima.model.article.pojos1.6.2):定義接口
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;} }1.6.3):編寫mapper文件
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<ApArticle> {public List<ApArticle> loadArticleList(@Param("dto") ArticleHomeDto dto, @Param("type") Short type);}對應的映射文件
在resources中新建mapper/ApArticleMapper.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">SELECTaa.*FROM`ap_article` aaLEFT JOIN ap_article_config aac ON aa.id = aac.article_id<where>and aac.is_delete != 1and 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 desclimit #{dto.size}</select></mapper>1.6.4):編寫業務層代碼
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<ApArticle> {/*** 根據參數加載文章列表* @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<ApArticleMapper, ApArticle> implements ApArticleService {// 單頁最大加載的數字private final static short MAX_PAGE_SIZE = 50;@Autowiredprivate ApArticleMapper apArticleMapper;/*** 根據參數加載文章列表* @param loadtype 1為加載更多 2為加載最新* @param dto* @return*/@Overridepublic 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;}}定義常量類
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__";}1.6.5):編寫控制器代碼
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 {@Autowiredprivate 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);} }1.6.6):swagger測試或前后端聯調測試
第一:在app網關的微服務的nacos的配置中心添加文章微服務的路由,完整配置如下:
spring:cloud:gateway:globalcors:cors-configurations:'[/**]': # 匹配所有請求allowedOrigins: "*" #跨域處理 允許所有的域allowedMethods: # 支持的方法- GET- POST- PUT- DELETEroutes:# 用戶微服務- id: useruri: lb://leadnews-userpredicates:- Path=/user/**filters:- StripPrefix= 1# 文章微服務- id: articleuri: lb://leadnews-articlepredicates:- Path=/article/**filters:- StripPrefix= 1第二:啟動nginx,直接使用前端項目測試,啟動文章微服務,用戶微服務、app網關微服務
2)freemarker
2.1) freemarker 介紹
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 年版本得到迭代,但為時已晚。
2.2) 環境搭建&&快速入門
freemarker作為springmvc一種視圖格式,默認情況下SpringMVC支持freemarker視圖格式。
需要創建Spring Boot+Freemarker工程用于測試模板。
2.2.1) 創建測試工程
創建一個freemarker-demo 的測試工程專門用于freemarker的功能測試與模板的測試。
pom.xml如下
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"><parent><artifactId>heima-leadnews-test</artifactId><groupId>com.heima</groupId><version>1.0-SNAPSHOT</version></parent><modelVersion>4.0.0</modelVersion><artifactId>freemarker-demo</artifactId><properties><maven.compiler.source>8</maven.compiler.source><maven.compiler.target>8</maven.compiler.target></properties><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-freemarker</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId></dependency><!-- lombok --><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId></dependency><!-- apache 對 java io 的封裝工具庫 --><dependency><groupId>org.apache.commons</groupId><artifactId>commons-io</artifactId><version>1.3.2</version></dependency></dependencies></project>2.2.2) 配置文件
配置application.yml
server:port: 8881 #服務端口 spring:application:name: freemarker-demo #指定服務名freemarker:cache: false #關閉模板緩存,方便測試settings:template_update_delay: 0 #檢查模板更新延遲時間,設置為0表示立即檢查,如果時間大于0會有緩存不方便進行模板測試suffix: .ftl #指定Freemarker模板文件的后綴名2.2.3) 創建模型類
在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;//錢包 }2.2.4) 創建模板
在resources下創建templates,此目錄為freemarker的默認模板存放目錄。
在templates下創建模板文件 01-basic.ftl ,模板中的插值表達式最終會被freemarker替換成具體的數據。
<!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>2.2.5) 創建controller
創建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,使用插值表達式填充數據
<!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>2.2.6) 創建啟動類
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);} }2.2.7) 測試
請求:http://localhost:8881/basic
2.3) freemarker基礎
2.3.1) 基礎語法種類
1、注釋,即<#-- -->,介于其之間的內容會被freemarker忽略
<#--我是一個freemarker注釋-->2、插值(Interpolation):即 ${..} 部分,freemarker會用真實的值代替**${..}**
Hello ${name}3、FTL指令:和HTML標記類似,名字前加#予以區分,Freemarker會解析標簽中的表達式或邏輯。
<# >FTL指令</#>4、文本,僅文本信息,這些不是freemarker的注釋、插值、FTL指令的內容會被freemarker忽略解析,直接輸出內容。
<#--freemarker中的普通文本--> 我是一個普通的文本2.3.2) 集合指令(List和Map)
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集合中List<Student> stus = new ArrayList<>();stus.add(stu1);stus.add(stu2);//向model中存放List集合數據model.addAttribute("stus",stus);//------------------------------------//創建Map數據HashMap<String,Student> 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文件
<!DOCTYPE html> <html> <head><meta charset="utf-8"><title>Hello World!</title> </head> <body><#-- list 數據的展示 --> <b>展示list中的stu數據:</b> <br> <br> <table><tr><td>序號</td><td>姓名</td><td>年齡</td><td>錢包</td></tr> </table> <hr><#-- Map 數據的展示 --> <b>map數據的展示:</b> <br/><br/> <a href="###">方式一:通過map['keyname'].property</a><br/> 輸出stu1的學生信息:<br/> 姓名:<br/> 年齡:<br/> <br/> <a href="###">方式二:通過map.keyname.property</a><br/> 輸出stu2的學生信息:<br/> 姓名:<br/> 年齡:<br/><br/> <a href="###">遍歷map中兩個學生信息:</a><br/> <table><tr><td>序號</td><td>姓名</td><td>年齡</td><td>錢包</td> </tr> </table> <hr></body> </html>實例代碼:
<!DOCTYPE html> <html> <head><meta charset="utf-8"><title>Hello World!</title> </head> <body><#-- list 數據的展示 --> <b>展示list中的stu數據:</b> <br> <br> <table><tr><td>序號</td><td>姓名</td><td>年齡</td><td>錢包</td></tr><#list stus as stu><tr><td>${stu_index+1}</td><td>${stu.name}</td><td>${stu.age}</td><td>${stu.money}</td></tr></#list></table> <hr><#-- Map 數據的展示 --> <b>map數據的展示:</b> <br/><br/> <a href="###">方式一:通過map['keyname'].property</a><br/> 輸出stu1的學生信息:<br/> 姓名:${stuMap['stu1'].name}<br/> 年齡:${stuMap['stu1'].age}<br/> <br/> <a href="###">方式二:通過map.keyname.property</a><br/> 輸出stu2的學生信息:<br/> 姓名:${stuMap.stu2.name}<br/> 年齡:${stuMap.stu2.age}<br/><br/> <a href="###">遍歷map中兩個學生信息:</a><br/> <table><tr><td>序號</td><td>姓名</td><td>年齡</td><td>錢包</td></tr><#list stuMap?keys as key ><tr><td>${key_index}</td><td>${stuMap[key].name}</td><td>${stuMap[key].age}</td><td>${stuMap[key].money}</td></tr></#list> </table> <hr></body> </html>👆上面代碼解釋:
${k_index}:
index:得到循環的下標,使用方法是在stu后邊加"_index",它的值是從0開始
2.3.3) if指令
if 指令即判斷指令,是常用的FTL指令,freemarker在解析時遇到if會進行判斷,條件為真則輸出if中間的內容,否則跳過內容不再輸出。
指令格式
1、數據模型:
使用list指令中測試數據模型,判斷名稱為小紅的數據字體顯示為紅色。
2、模板:
<table><tr><td>姓名</td><td>年齡</td><td>錢包</td></tr><#list stus as stu><tr><td >${stu.name}</td><td>${stu.age}</td><td >${stu.mondy}</td></tr></#list></table>實例代碼:
<table><tr><td>姓名</td><td>年齡</td><td>錢包</td></tr><#list stus as stu ><#if stu.name='小紅'><tr style="color: red"><td>${stu_index}</td><td>${stu.name}</td><td>${stu.age}</td><td>${stu.money}</td></tr><#else ><tr><td>${stu_index}</td><td>${stu.name}</td><td>${stu.age}</td><td>${stu.money}</td></tr></#if></#list> </table>3、輸出:
姓名為“小強”則字體顏色顯示為紅色。
2.3.4) 運算符
1、算數運算符
FreeMarker表達式中完全支持算術運算,FreeMarker支持的算術運算符包括:
加法: +
減法: -
乘法: *
除法: /
求模 (求余): %
模板代碼
<b>算數運算符</b> <br/><br/>100+5 運算: ${100 + 5 }<br/>100 - 5 * 5運算:${100 - 5 * 5}<br/>5 / 2運算:${5 / 2}<br/>12 % 10運算:${12 % 10}<br/> <hr>除了 + 運算以外,其他的運算只能和 number 數字類型的計算。
2、比較運算符
=或者==:判斷兩個值是否相等.
!=:判斷兩個值是否不等.
>或者gt:判斷左邊值是否大于右邊值
>=或者gte:判斷左邊值是否大于等于右邊值
<或者lt:判斷左邊值是否小于右邊值
<=或者lte:判斷左邊值是否小于等于右邊值
= 和 == 模板代碼
<!DOCTYPE html> <html> <head><meta charset="utf-8"><title>Hello World!</title> </head> <body><b>比較運算符</b><br/><br/><dl><dt> =/== 和 != 比較:</dt><dd><#if "xiaoming" == "xiaoming">字符串的比較 "xiaoming" == "xiaoming"</#if></dd><dd><#if 10 != 100>數值的比較 10 != 100</#if></dd></dl><dl><dt>其他比較</dt><dd><#if 10 gt 5 >形式一:使用特殊字符比較數值 10 gt 5</#if></dd><dd><#-- 日期的比較需要通過?date將屬性轉為data類型才能進行比較 --><#if (date1?date >= date2?date)>形式二:使用括號形式比較時間 date1?date >= date2?date</#if></dd></dl><br/> <hr> </body> </html>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、邏輯運算符
邏輯與:&&
邏輯或:||
邏輯非:!
邏輯運算符只能作用于布爾值,否則將產生錯誤 。
模板代碼
<b>邏輯運算符</b><br/><br/><#if (10 lt 12 )&&( 10 gt 5 ) >(10 lt 12 )&&( 10 gt 5 ) 顯示為 true</#if><br/><br/><#if !false>false 取反為true</#if> <hr>2.3.5) 空值處理
1、判斷某變量是否存在使用 “??”
用法為:variable??,如果該變量存在,返回true,否則返回false
例:為防止stus為空報錯可以加上判斷如下:
<#if stus??><#list stus as stu>......</#list></#if>2、缺失變量默認值使用 “!”
使用!要以指定一個默認值,當變量為空時顯示默認值
例: ${name!''}表示如果name為空顯示空字符串。
如果是嵌套對象則建議使用()括起來
例: ${(stu.bestFriend.name)!''}表示,如果stu或bestFriend或name為空默認顯示空字符串。
2.3.6) 內建函數
內建函數語法格式: 變量+?+函數名稱
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}模板代碼:
<!DOCTYPE html> <html> <head><meta charset="utf-8"><title>inner Function</title> </head> <body><b>獲得集合大小</b><br>集合大小:<hr><b>獲得日期</b><br>顯示年月日: <br>顯示時分秒:<br>顯示日期+時間:<br>自定義格式化: <br><hr><b>內建函數C</b><br>沒有C函數顯示的數值: <br>有C函數顯示的數值:<hr><b>聲明變量assign</b><br><hr> </body> </html>內建函數模板頁面:
<!DOCTYPE html> <html> <head><meta charset="utf-8"><title>inner Function</title> </head> <body><b>獲得集合大小</b><br>集合大小:${stus?size}<hr><b>獲得日期</b><br>顯示年月日: ${today?date} <br>顯示時分秒:${today?time}<br>顯示日期+時間:${today?datetime}<br>自定義格式化: ${today?string("yyyy年MM月")}<br><hr><b>內建函數C</b><br>沒有C函數顯示的數值:${point} <br>有C函數顯示的數值:${point?c}<hr><b>聲明變量assign</b><br><#assign text="{'bank':'工商銀行','account':'10101920201920212'}" /><#assign data=text?eval />開戶行:${data.bank} 賬號:${data.account}<hr> </body> </html>內建函數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集合中List<Student> stus = 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"; }2.4) 靜態化測試
之前的測試都是SpringMVC將Freemarker作為視圖解析器(ViewReporter)來集成到項目中,工作中,有的時候需要使用Freemarker原生Api來生成靜態內容,下面一起來學習下原生Api生成文本文件
2.4.1) 需求分析
使用freemarker原生Api將頁面生成html文件,本節測試html文件生成的方法:
2.4.2) 靜態化測試
根據模板文件生成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 {@Autowiredprivate Configuration configuration;@Testpublic 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() {Map<String, Object> map = 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<Student> stus = new ArrayList<>();stus.add(stu1);stus.add(stu2);//向map中存放List集合數據map.put("stus", stus);//創建Map數據HashMap<String, Student> stuMap = new HashMap<>();stuMap.put("stu1", stu1);stuMap.put("stu2", stu2);//向map中存放Map數據map.put("stuMap", stuMap);//返回Mapreturn map;} }3) 對象存儲服務MinIO
3.1 MinIO簡介
MinIO基于Apache License v2.0開源協議的對象存儲服務,可以做為云存儲的解決方案用來保存海量的圖片,視頻,文檔。由于采用Golang實現,服務端可以工作在Windows,Linux, OS X和FreeBSD上。配置簡單,基本是復制可執行程序,單行命令可以運行起來。
MinIO兼容亞馬遜S3云存儲服務接口,非常適合于存儲大容量非結構化的數據,例如圖片、視頻、日志文件、備份數據和容器/虛擬機鏡像等,而一個對象文件可以是任意大小,從幾kb到最大5T不等。
S3 ( Simple Storage Service簡單存儲服務)
基本概念
bucket – 類比于文件系統的目錄
Object – 類比文件系統的文件
Keys – 類比文件名
官網文檔:http://docs.minio.org.cn/docs/
3.2 MinIO特點
數據保護
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等。
3.3 開箱使用
3.3.1 安裝啟動
我們提供的鏡像中已經有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 /data3.3.2 管理控制臺
假設我們的服務器地址為http://192.168.200.130:9000,我們在地址欄輸入:http://192.168.200.130:9000/ 即可進入登錄界面。
Access Key為minio Secret_key 為minio123 進入系統后可以看到主界面
點擊右下角的“+”號 ,點擊下面的圖標,創建一個桶
3.4 快速入門
3.4.1 創建工程,導入pom依賴
創建minio-demo,對應pom如下
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"><parent><artifactId>heima-leadnews-test</artifactId><groupId>com.heima</groupId><version>1.0-SNAPSHOT</version></parent><modelVersion>4.0.0</modelVersion><artifactId>minio-demo</artifactId><properties><maven.compiler.source>8</maven.compiler.source><maven.compiler.target>8</maven.compiler.target></properties><dependencies><dependency><groupId>io.minio</groupId><artifactId>minio</artifactId><version>7.1.0</version></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId></dependency></dependencies></project>引導類:
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();}}}3.5 封裝MinIO為starter
封裝為了其它微服務使用
3.5.1 創建模塊heima-file-starter
導入依賴
<dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-autoconfigure</artifactId></dependency><dependency><groupId>io.minio</groupId><artifactId>minio</artifactId><version>7.1.0</version></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-configuration-processor</artifactId><optional>true</optional></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-actuator</artifactId></dependency> </dependencies>3.5.2 配置類
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 {@Autowiredprivate MinIOConfigProperties minIOConfigProperties;@Beanpublic MinioClient buildMinioClient(){return MinioClient.builder().credentials(minIOConfigProperties.getAccessKey(), minIOConfigProperties.getSecretKey()).endpoint(minIOConfigProperties.getEndpoint()).build();} }3.5.3 封裝操作minIO類
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 {@Autowiredprivate MinioClient minioClient;@Autowiredprivate 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 文件全路徑*/@Overridepublic 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 文件全路徑*/@Overridepublic 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 文件全路徑*/@Overridepublic 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);// 刪除ObjectsRemoveObjectArgs 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 文件流**/@Overridepublic 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();} }3.5.4 對外加入自動配置
在resources中新建META-INF/spring.factories
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\com.heima.file.service.impl.MinIOFileStorageService3.5.5 其他微服務使用
第一,導入heima-file-starter的依賴
第二,在微服務中添加minio所需要的配置
minio:accessKey: miniosecretKey: minio123bucket: leadnewsendpoint: http://192.168.200.130:9000readPath: 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 {@Autowiredprivate FileStorageService fileStorageService;@Testpublic 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();}} }4)文章詳情
4.1)需求分析
4.2)實現方案
方案一
用戶某一條文章,根據文章的id去查詢文章內容表,返回渲染頁面
方案二 效率高
報錯:
這個要在nacos里面配 minio;
?allowPublicKeyRetrieval=true 成功解決;
4.3)實現步驟
4.在文章微服務中導入依賴
<dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-freemarker</artifactId></dependency><dependency><groupId>com.heima</groupId><artifactId>heima-file-starter</artifactId><version>1.0-SNAPSHOT</version></dependency> </dependencies>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<ApArticleContent> { }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 {@Autowiredprivate Configuration configuration;@Autowiredprivate FileStorageService fileStorageService;@Autowiredprivate ApArticleMapper apArticleMapper;@Autowiredprivate ApArticleContentMapper apArticleContentMapper;@Testpublic 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<>();//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);}} }大佬彈幕:人家現在發布文章,都是直接用富文本 編輯器,發布后數據庫存的是富文本,然后直接把富文本丟給前端人家就可以直接顯示了,搞這么麻煩干嘛,
手機端適宜的屏幕大小
03自媒體文章發布
1)自媒體前后端搭建
1.1)后臺搭建
①:資料中找到heima-leadnews-wemedia.zip解壓
拷貝到heima-leadnews-service工程下,并指定子模塊
執行leadnews-wemedia.sql腳本
添加對應的nacos配置
spring:datasource:driver-class-name: com.mysql.jdbc.Driverurl: jdbc:mysql://localhost:3306/leadnews_wemedia?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTCusername: rootpassword: 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- DELETEroutes:# 平臺管理- id: wemediauri: lb://leadnews-wemediapredicates:- Path=/wemedia/**filters:- StripPrefix= 1③:在資料中找到類文件夾
拷貝wemedia文件夾到heima-leadnews-model模塊下的com.heima.model
1.2)前臺搭建
通過nginx的虛擬主機功能,使用同一個nginx訪問多個項目
搭建步驟:
①:資料中找到wemedia-web.zip解壓
②:在nginx中leadnews.conf目錄中新增heima-leadnews-wemedia.conf文件
網關地址修改(localhost:51602)
前端項目目錄修改(wemedia-web解壓的目錄)
訪問端口修改(8802)
③:啟動nginx,啟動自媒體微服務和對應網關
④:聯調測試登錄功能 測試成功
2)自媒體素材管理
2.1)素材上傳
2.2.1)需求分析
圖片上傳的頁面,首先是展示素材信息,可以點擊圖片上傳,彈窗后可以上傳圖片
2.2.2)素材管理-圖片上傳-表結構
媒體圖文素材信息表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;/*** <p>* 自媒體圖文素材信息表* </p>** @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;}2.2.3)實現思路
①:前端發送上傳圖片請求,類型為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 ThreadLocal<WmUser> WM_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 {@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {//得到header中的信息String userId = request.getHeader("userId");Optional<String> optional = 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;}@Overridepublic 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 {@Overridepublic void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(new WmTokenInterceptor()).addPathPatterns("/**");} }④:先把圖片上傳到minIO中,獲取到圖片請求的路徑——(2.2.5查看具體功能實現)
⑤:把用戶id和圖片上的路徑保存到素材表中——(2.2.5查看具體功能實現)
2.2.4)接口定義
說明 | |
接口路徑 | /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"} }失敗:
參數失效
文章上傳失敗
2.2.5)自媒體微服務集成heima-file-starter
①:導入heima-file-starter
<dependencies><dependency><groupId>com.heima</groupId><artifactId>heima-file-starter</artifactId><version>1.0-SNAPSHOT</version></dependency> </dependencies>②:在自媒體微服務的配置中心添加以下配置:
minio:accessKey: miniosecretKey: minio123bucket: leadnewsendpoint: http://192.168.200.130:9000readPath: http://192.168.200.130:90002.2.6)具體實現
①:創建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<WmMaterial> { }③:業務層:
package com.heima.wemedia.service;public interface WmMaterialService extends IService<WmMaterial> {/*** 圖片上傳* @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 ServiceImpl<WmMaterialMapper, WmMaterial> implements WmMaterialService {@Autowiredprivate FileStorageService fileStorageService;/*** 圖片上傳* @param multipartFile* @return*/@Overridepublic 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.jpgString 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 {@Autowiredprivate WmMaterialService wmMaterialService;@PostMapping("/upload_picture")public ResponseResult uploadPicture(MultipartFile multipartFile){return wmMaterialService.uploadPicture(multipartFile);} }⑤:測試
啟動自媒體微服務和自媒體網關,使用前端項目進行測試
2.2)素材列表查詢
2.2.1)接口定義
說明 | |
接口路徑 | /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 }2.2.2)功能實現
①:在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());LambdaQueryWrapper<WmMaterial> lambdaQueryWrapper = 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; }3)自媒體文章管理
3.1)查詢所有頻道
3.1.1)需求分析
3.1.2)表結構
wm_channel 頻道信息表
對應實體類:
package com.heima.model.wemedia.pojos;import java.util.Date;/*** <p>* 頻道信息表* </p>** @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;}3.1.3)接口定義
說明 | |
接口路徑 | /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 { ... }] }3.1.4)功能實現
接口定義:
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<WmChannel> { }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<WmChannel> {/*** 查詢所有頻道* @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 ServiceImpl<WmChannelMapper, WmChannel> implements WmChannelService {/*** 查詢所有頻道* @return*/@Overridepublic 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 {@Autowiredprivate WmChannelService wmChannelService;@GetMapping("/channels")public ResponseResult findAll(){return wmChannelService.findAll();} }3.1.5)測試
3.2)查詢自媒體文章
3.2.1)需求說明
3.2.2)表結構分析
wm_news 自媒體文章表
對應實體類:
package com.heima.model.wemedia.pojos;import java.io.Serializable; import java.util.Date;/*** <p>* 自媒體圖文內容信息表* </p>** @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;}}}3.2.3)接口定義
說明 | |
接口路徑 | /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 }3.2.4)功能實現
①:新增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<WmNews> {}③:新增WmNewsService
package com.heima.wemedia.service;import com.heima.model.wemedia.pojos.WmNews;public interface WmNewsService extends IService<WmNews> {/*** 查詢文章* @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 ServiceImpl<WmNewsMapper, WmNews> implements WmNewsService {/*** 查詢文章* @param dto* @return*/@Overridepublic 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<WmNews> 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 {@Autowiredprivate WmNewsService wmNewsService;@PostMapping("/list")public ResponseResult findAll(@RequestBody WmNewsPageReqDto dto){return wmNewsService.findAll(dto);}}3.2.5)測試
啟動后端自媒體微服務和自媒體網關微服務,測試文章列表查詢
3.3)文章發布
3.3.1)需求分析
3.3.2)表結構分析
保存文章,除了需要wm_news表以外,還需要另外兩張表;
保存了這個關系表,就可以保存素材,素材有被引用的標記,就不能被修改或刪除;
其中wm_material和wm_news表的實體類已經導入到了項目中,下面是wm_news_material表對應的實體類:
package com.heima.model.wemedia.pojos; import java.io.Serializable;/*** <p>* 自媒體圖文引用素材信息表* </p>** @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;}3.3.3)實現思路分析
1.前端提交發布或保存為草稿
2.后臺判斷請求中是否包含了文章id
3.如果不包含id,則為新增
3.1 執行新增文章的操作
3.2 關聯文章內容圖片與素材的關系
3.3 關聯文章封面圖片與素材的關系
4.如果包含了id,則為修改請求
4.1 刪除該文章與素材的所有關系
4.2 執行修改操作
4.3 關聯文章內容圖片與素材的關系
4.4 關聯文章封面圖片與素材的關系
3.3.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 List<String> images; }前端給傳遞過來的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”:“素材引用失效" }3.3.5)功能實現
①:在新增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<WmNewsMaterial> {void saveRelations(@Param("materialIds") List<Integer> materialIds,@Param("newsId") Integer newsId, @Param("type")Short type); }WmNewsMaterialMapper.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.wemedia.mapper.WmNewsMaterialMapper"><insert id="saveRelations">insert into wm_news_material (material_id,news_id,type,ord)values<foreach collection="materialIds" index="ord" item="mid" separator=",">(#{mid},#{newsId},#{type},#{ord})</foreach></insert></mapper>③:常量類準備
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---> stringif(dto.getImages() != null && dto.getImages().size() > 0){//[1dddfsd.jpg,sdlfjldk.jpg]--> 1dddfsd.jpg,sdlfjldk.jpgString imageStr = StringUtils.join(dto.getImages(), ",");wmNews.setImages(imageStr);}//如果當前封面類型為自動 -1if(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.不是草稿,保存文章內容圖片與素材的關系//獲取到文章內容中的圖片信息List<String> materials = 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<String> materials) {List<String> 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<String> materials, Integer newsId) {saveRelativeInfo(materials,newsId,WemediaConstants.WM_CONTENT_REFERENCE); }@Autowired private WmMaterialMapper wmMaterialMapper;/*** 保存文章圖片與素材的關系到數據庫中* @param materials* @param newsId* @param type*/ private void saveRelativeInfo(List<String> materials, Integer newsId, Short type) {if(materials!=null && !materials.isEmpty()){//通過圖片的url查詢素材的idList<WmMaterial> dbMaterials = wmMaterialMapper.selectList(Wrappers.<WmMaterial>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<Integer> idList = dbMaterials.stream().map(WmMaterial::getId).collect(Collectors.toList());//批量保存wmNewsMaterialMapper.saveRelations(idList,newsId,type);}}/*** 提取文章內容中的圖片信息* @param content* @return*/ private List<String> ectractUrlInfo(String content) {List<String> materials = new ArrayList<>();List<Map> maps = JSON.parseArray(content, Map.class);for (Map map : maps) {if(map.get("type").equals("image")){String imgUrl = (String) map.get("value");materials.add(imgUrl);}}return materials; }@Autowired private WmNewsMaterialMapper wmNewsMaterialMapper;/*** 保存或修改文章* @param wmNews*/ private void saveOrUpdateWmNews(WmNews wmNews) {//補全屬性wmNews.setUserId(WmThreadLocalUtil.getUser().getId());wmNews.setCreatedTime(new Date());wmNews.setSubmitedTime(new Date());wmNews.setEnable((short)1);//默認上架if(wmNews.getId() == null){//保存save(wmNews);}else {//修改//刪除文章圖片與素材的關系wmNewsMaterialMapper.delete(Wrappers.<WmNewsMaterial>lambdaQuery().eq(WmNewsMaterial::getNewsId,wmNews.getId()));updateById(wmNews);}}④:控制器
@PostMapping("/submit") public ResponseResult submitNews(@RequestBody WmNewsDto dto){return wmNewsService.submitNews(dto); }3.3.6)測試
總結
以上是生活随笔為你收集整理的《黑马头条》SpringBoot+SpringCloud+ Nacos等企业级微服务架构项目的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 【数学建模】基于matlab三维海浪模型
- 下一篇: 海康威视 2024届实习生 应用软件开发