當前位置:網站首頁>旅遊評點項目

旅遊評點項目

2022-05-13 12:52:19磊哥的小迷妹

旅遊評點項目

1、項目概述

目前電商類 App 應該占有市場最大氛圍。但是目前單純類的銷售型電商平臺越來越少,逐漸進入了內容為主的模式

內容類 App 主要分為三類:

  1. UGC:user generated content,用戶生產內容,要求平臺普通用戶都能參與內容的生產, 比如社區類平臺,短視頻平臺都是典型的 UGC 類型平臺;

  2. PGC:professional generated content,專業生產內容。如果所有由用戶參與平臺的內容生產,造成的最大問題是內容質量的參差不齊,所以要求平臺的內容只由部分平臺指定用戶生產,比如平臺認證專家等;例如優酷的專家欄目,喜馬拉雅的專家聲音等;

  3. OGC:occupational generated content,職業生產內容。內容由專職人員生產,並以此支付對應的報酬

    和 PGC 最大的區別在於,PGC 是以免費生產內容為主

    而 OGC 以收費作為輸出內容的回報。一般企業網站就是典型的 OGC

因為內容類 App 涵蓋的面很廣,包括知識內容分享,購物體驗分享,新聞資訊分享,純社區類,社區偏內容,內容偏社區等等。在本項目中,我們主要針對典型的 OGC+UGC 場景, 做一個點評類內容 App

點評類內容 App 針對的行業非常多,常見的美食,旅遊,等等,包含了生活中的吃喝玩樂 住行都有。在本項目中,主要針對旅遊行業,做一個點評內容 App

在本項目中,我們把關注點聚焦,來完成一個針對旅遊類的攻略,點評,分享旅遊日記的 App

技術路線

項目技術路線:

  1. 數據庫:mongodb + elasticsearch

  2. 持久化層:mongodb+Redis (緩存)

  3. 業務層:Springboot;

  4. Web:SpringMVC;

  5. 前端:

    管理後臺:jQuery+Bootstrap3

    前端展示:vue +jquery + css;

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-hkg6fYoT-1651834690330)(C:\Users\ASUS\AppData\Roaming\Typora\typora-user-images\image-20220324140734197.png)]

項目組成結構

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-nuBYFcUx-1651834690331)(C:\Users\ASUS\AppData\Roaming\Typora\typora-user-images\image-20220324140848132.png)]

trip-parent 項目怎麼管理依賴:

  1. 如果 所有子項目都需要 某個依賴,將該依賴,添加到父項目pom. xml文件的 <dependencies> 標簽裏面,錶示所有項目共享

  2. 如果 部分子項目都需要 某個依賴,將該依賴,添加到父項目pom. xml文件的 <dependencyManagement> 標簽裏面,父項目對這個依賴進行版本管理

    其他需要用該依賴的子項目在pom. xml的 <dependencies> 標簽裏面引入依賴,此時不需要引入依賴的版本

  3. 如果 某個子項目需要 某個依賴,將該依賴,添加到子項目pom. xml文件的 <dependencies> 標簽裏面,自己用即可

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-wfNYmQcD-1651834690332)(C:\Users\ASUS\AppData\Roaming\Typora\typora-user-images\image-20220324111246542.png)]

2、搭建環境

打開 MongoDB

進入 MongoDB 安裝目錄的 bin 目錄中,雙擊打開 mongo.exe

執行以下語句,准備數據:

db.getCollection("userInfo").insert([ {
   _id: ObjectId("5e295f01a00e265228f963ea"),
   nickname: "dafei",
   phone: "13700000000",
   email: "",
   password: "1111",
   gender: NumberInt("0"),
   level: NumberInt("1"),
   city: "廣州",
   headImgUrl: "/images/default.jpg",
   info: "廣州最靚的仔",
   state: NumberInt("0")
} ]);
db.getCollection("userInfo").insert([ {
   _id: ObjectId("5e92d6cdacd8de311e99c99e"),
   nickname: "xiaofeii",
   phone: "13700000001",
   email: "",
   password: "1111",
   gender: NumberInt("0"),
   level: NumberInt("1"),
   city: "廣州",
   headImgUrl: "/images/default.jpg",
   info: "廣州最靚的仔",
   state: NumberInt("0")
} ]);

1、wolf2w 模塊

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">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.afei</groupId>
    <artifactId>parent</artifactId>
    <packaging>pom</packaging>
    <version>1.0.0</version>
    <modules>
        <module>../trip-core</module>
        <module>../trip-mgrsite</module>
        <module>../trip-website-api</module>
    </modules>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.2.6.RELEASE</version>
        <relativePath/>
    </parent>

    <!-- 依賴版本管理 -->
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>com.afei</groupId>
                <artifactId>trip-core</artifactId>
                <version>1.0.0</version>
            </dependency>
        </dependencies>
    </dependencyManagement>

</project>

2、trip-core 模塊

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>parent</artifactId>
        <groupId>com.afei</groupId>
        <version>1.0.0</version>
        <relativePath>../wolf2w/pom.xml</relativePath>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>trip-core</artifactId>

    <dependencies>
        <!-- gettgetset方法 -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
        <!-- mongodb -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-mongodb</artifactId>
        </dependency>
        <!-- toJson -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.47</version>
        </dependency>
        <!-- redis -->
        <!--<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <scope>provided</scope> </dependency>-->
        <!-- oss -->
        <!--<dependency> <groupId>com.aliyun.oss</groupId> <artifactId>aliyun-sdk-oss</artifactId> <version>3.5.0</version> </dependency> <dependency> <groupId>commons-io</groupId> <artifactId>commons-io</artifactId> <version>2.6</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-elasticsearch</artifactId> </dependency> <dependency> <groupId>commons-beanutils</groupId> <artifactId>commons-beanutils</artifactId> <version>1.9.3</version> </dependency>-->
    </dependencies>
</project>

domain

在 trip-core 模塊中定義一些類

@Setter
@Getter
public class BaseDomain implements Serializable {
    
    @Id   //MongoDB中文檔的id建議使用 String 類型,並且貼上@Id注解
    private String id;
}
@Setter
@Getter
@Document("userInfo")   //設置MongoDB中文檔所在的集合
@ToString
public class UserInfo extends BaseDomain{
    
    public static final int GENDER_SECRET = 0; //保密
    public static final int GENDER_MALE = 1;   //男
    public static final int GENDER_FEMALE = 2;  //女
    public static final int STATE_NORMAL = 0;  //正常
    public static final int STATE_DISABLE = 1;  //凍結

    private String nickname;  //昵稱
    private String phone;  //手機
    private String email;  //郵箱
    private String password; //密碼
    private int gender = GENDER_SECRET; //性別
    private int level = 0;  //用戶級別
    private String city;  //所在城市
    private String headImgUrl; //頭像
    private String info;  //個性簽名
    private int state = STATE_NORMAL; //狀態
}

MongoDB 數據庫錶

db.getCollection("userInfo").drop();
db.createCollection("userInfo");

db.getCollection("userInfo").insert([ {
    _id: ObjectId("5e295f01a00e265228f963ea"),
    nickname: "dafei",
    phone: "13700000000",
    email: "",
    password: "1111",
    gender: NumberInt("0"),
    level: NumberInt("1"),
    city: "廣州",
    headImgUrl: "/images/default.jpg",
    info: "廣州最靚的仔",
    state: NumberInt("0")
} ]);
db.getCollection("userInfo").insert([ {
    _id: ObjectId("5e92d6cdacd8de311e99c99e"),
    nickname: "xiaofeii",
    phone: "13700000001",
    email: "",
    password: "1111",
    gender: NumberInt("0"),
    level: NumberInt("1"),
    city: "廣州",
    headImgUrl: "/images/default.jpg",
    info: "廣州最靚的仔",
    state: NumberInt("0")
} ]);

參數查詢對象

@Setter
@Getter
public class QueryObject implements Serializable {
    

    private int currentPage = 1;
    private int pageSize = 10;
    private String keyword;
    
    /*private Pageable pageable; //分頁設置對象 public Pageable getPageable(){ if(pageable == null){ //沒有指定分頁對象值, 默認id倒序 return PageRequest.of(currentPage - 1, pageSize, Sort.Direction.ASC, "_id"); } return pageable; }*/

    public String getKeyword(){
    
        return StringUtils.hasLength(keyword)? keyword : null;
    }

}

持久化接口

//用戶持久化操作接口,類似 Mapper 接口
/** * 繼承接口:MongoRepository * 1、接口泛型1:操作domain對象 UserInfo * 2、接口泛型2:操作對象主鍵類型 String */
@Repository
public interface UserInfoRepository extends MongoRepository<UserInfo, String> {
    
}

服務接口及實現類

/** * 用戶服務接口 */
public interface IUserInfoService  {
    
    //添加
    void save(UserInfo userInfo);
    void delete(String id);
    //更新
    void update(UserInfo userInfo);
    /** * 查單個 */
    UserInfo get(String id);
    //查所有
    List<UserInfo> list();
}
@Service
//@Transactional 現在先不能用,MongoDB不支持,在講完複制集後才需要
public class UserInfoServiceImpl implements IUserInfoService {
    

    @Autowired
    private UserInfoRepository repository;

    @Override
    public void save(UserInfo userInfo) {
    
        repository.save(userInfo);
    }

    @Override
    public void delete(String id) {
    
        repository.deleteById(id);
    }

    @Override
    public void update(UserInfo userInfo) {
    
        repository.save(userInfo);
    }

    @Override
    public UserInfo get(String id) {
    
        //orElse 錶示若沒有值就返回null
        return repository.findById(id).orElse(null);
    }

    @Override
    public List<UserInfo> list() {
    
        return repository.findAll();
    }
}

3、trip-website 模塊

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>parent</artifactId>
        <groupId>com.afei</groupId>
        <version>1.0.0</version>
        <relativePath>../wolf2w/pom.xml</relativePath>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>trip-website-api</artifactId>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>com.afei</groupId>
            <artifactId>trip-core</artifactId>
        </dependency>

        <!--<dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger2</artifactId> <version>2.9.2</version> </dependency> <dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger-ui</artifactId> <version>2.9.2</version> </dependency>-->
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <fork>true</fork>
                    <mainClass>cn.wolfcode.wolf2w.WebSite</mainClass>
                    <layout>ZIP</layout>
                </configuration>
                <executions>
                    <execution>
                        <goals>
                            <!--<goal>repackage</goal>-->
                            <!-- 可以把依賴的包都打包到生成的Jar包中 -->
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
</project>

application.properties

server.port=8080
#mongodb
#spring.data.mongodb.uri=mongodb://127.0.0.1:27017,127.0.0.1:27018,127.0.0.1:27019/wolf2w?replicaSet=rs
spring.data.mongodb.uri=mongodb://127.0.0.1:27017/wolf2w
logging.level.org.springframework.data.mongodb.core= DEBUG

#redis
#spring.redis.host=127.0.0.1

#elasticsearch
# elasticsearch集群名稱,默認的是elasticsearch
#spring.data.elasticsearch.cluster-name=elasticsearch
#節點的地址,注意api模式下端口號是9300,千萬不要寫成9200
#集群時,用逗號隔開,es會自動尋找節點
#spring.data.elasticsearch.cluster-nodes=127.0.0.1:9300
#是否開啟本地存儲
#spring.data.elasticsearch.repositories.enable=true


#短信
#sms.url=https://way.jd.com/chonry/smsapi?sign={1}&mobile={2}&content={3}&appkey={4}
#sms.appkey=dd1f7d99cd632060789a56cfaa3b77ce
#sms.url=https://way.jd.com/HZXINXI/noticeSms?mobile={1}&content={2}&sendTime=&appkey={3}
#sms.appkey=dd1f7d99cd632060789a56cfaa3b77ceappkey

後端控制器方法

@RestController
@RequestMapping("users")
public class UserInfoController {
    
    @Autowired
    private IUserInfoService userInfoService;
    
    @GetMapping("/get")
    public Object get(String id){
    
        return userInfoService.get(id);
    }
}

springBoot 啟動類

@SpringBootApplication
public class WebSite {
    
    public static void main(String[] args) {
    
        SpringApplication.run(WebSite.class,args);
    }
}

修改一下啟動類的名稱

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-nIUCZP9j-1651834690333)(C:\Users\ASUS\AppData\Roaming\Typora\typora-user-images\image-20220324141526048.png)]

測試一下

運行啟動類

訪問 http://localhost:8080/users/get?id=5e295f01a00e265228f963ea

有數據,錶示環境搭建成功

4、trip-website

創建 靜態的 web 項目

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-bx9baCGK-1651834690334)(C:\Users\ASUS\AppData\Roaming\Typora\typora-user-images\image-20220324132239981.png)]

拷貝文件

配置 Tomcat

搭建環境的問題集合

  1. 如果搭建錯誤,但是不想重新搭建

    打開項目所在文件夾,删除裏面 .idea 等所有文件,只保留 src 目錄、pom.xml 文件 兩個內容

    然後打開 idea ,選擇導入項目工程 ,注意 勾選search for projects recursively

    [外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-4imub7s5-1651834690335)(C:\Users\ASUS\AppData\Roaming\Typora\typora-user-images\image-20220324133431549.png)]

    對於靜態的web模塊,需要在導入項目之後,單獨導入模塊

  2. 在右側的 Maven Projects 裏面,呈現灰色

    點擊上面的 + ,選擇磁盤中 pom.xml 文件導入

  3. 在 pom.xml 文件中爆紅

    1. 網絡不行,下載慢

    2. 可能以前下載一半,或者下載錯誤。導致現在找不到對應的依賴

      進入磁盤中 maven 倉庫的文件夾,找到對應爆紅依賴的目錄,删除,然後回到 idea 重新下載

    3. 可能是 idea 版本問題

    4. 可能是 SpringBoot 版本問題,換個版本

  4. 還是不行。清空並重啟 idea

碼雲准備

  1. 先在 碼雲 上新建倉庫

  2. 進入磁盤中項目所在目錄,放入忽略文件

  3. 右擊打開 Git Bash,分步執行一下命令

    git config --global user.name "afei"
    git config --global user.email "[email protected]"
    git init 
    git add .
    git commit -m "項目初始化"
    # 丟入遠程倉庫
    git remote add origin https://gitee.com/leigedexiaomimei/wolf2w.git
    # 推送上去
    git push -u origin "master"
    
  4. 碼雲上可以查看項目

  5. 如果有更新,本地更新完畢之後,執行以下,即可推送

    git init 
    git add .
    git commit -m "項目初始化"
    git push
    

3、用戶注册

手機號校驗

分析需求

查看前端代碼,發現注册發起請求,會寫帶參數,且返回值為 Boolean 類型

編寫後端控制器

trip-website-api模塊中
    
@GetMapping("/checkPhone")
public Object checkPhone(String phone){
    
    boolean ret = userInfoService.checkPhone(phone);
    return ret;
}

service 接口

注意,對於 返回值為 Boolean 類型的方法, 一定要寫注解錶明返回值所錶達的意思

/** * 檢查手機號碼是否存在 * @param phone * @return true:手機號碼存在;false:手機號碼不存在 */
boolean checkPhone(String phone);
@Override
public boolean checkPhone(String phone) {
    
    UserInfo userInfo =repository.findByPhone(phone);
    return userInfo == null;
}

Repository 接口中聲明方法

注意,定義方法要有寫注釋的習慣

@Repository
public interface UserInfoRepository extends MongoRepository<UserInfo, String> {
    
    /** * 通過手機號查詢用戶對象 * @param phone * @return */
    UserInfo findByPhone(String phone);
}

跨域配置

啟動 trip-website-api 的啟動類,啟動 trip-website 的 Tomcat

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-8TIs8hsf-1651834690336)(C:\Users\ASUS\AppData\Roaming\Typora\typora-user-images\image-20220324161219021.png)]

查看注册頁面,進行注册

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-vxqzzelq-1651834690336)(C:\Users\ASUS\AppData\Roaming\Typora\typora-user-images\image-20220324161837940.png)]

出現跨域問題,在啟動類中進行配置即可

@SpringBootApplication
public class WebSite {
    

    //跨域訪問
    @Bean
    public WebMvcConfigurer corsConfigurer() {
    
        return new WebMvcConfigurer() {
    
            @Override
            //重寫父類提供的跨域請求處理的接口
            public void addCorsMappings(CorsRegistry registry) {
    
                //添加映射路徑
                registry.addMapping("/**")
                        //放行哪些原始域
                        .allowedOrigins("*")
                        //是否發送Cookie信息
                        .allowCredentials(true)
                        //放行哪些原始域(請求方式)
                        .allowedMethods("GET", "POST", "PUT", "DELETE","OPTIONS")
                        //放行哪些原始域(頭部信息)
                        .allowedHeaders("*")
                        //暴露哪些頭部信息(因為跨域訪問默認不能獲取全部頭部信息)
                        .exposedHeaders("Header1", "Header2");
            }
        };
    }

    public static void main(String[] args) {
    
        SpringApplication.run(WebSite.class,args);
    }
}

短信驗證碼

分析需求

需要在點擊獲取驗證碼時發起請求,並得到驗證碼1,

用戶輸入後,對比驗證碼時會發起請求,得到用戶輸入的驗證碼2,

如何在跨請求中獲取到驗證碼數據,以進行對比是否輸入正確?可選擇將驗證碼存儲到 session 、redis 、數據庫 等中。但是因為驗證碼還有時效要求,推薦使用 session 、redis 。

下面是使用 session :

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-4EP8raMJ-1651834690337)(C:\Users\ASUS\AppData\Roaming\Typora\typora-user-images\image-20220324203011209.png)]

但是 session 在 安卓 等平臺不是很好使用,所以推薦使用 redis

下面是使用 redis :

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-1tueoK3Q-1651834690338)(C:\Users\ASUS\AppData\Roaming\Typora\typora-user-images\image-20220324203430492.png)]

啟動 Redis 服務

推薦使用 Redis ,因為在 trip-website-api 和 trip-mgrsite 裏面都會用到 Redis,所以在 trip-core 裏面進行代碼編寫

下面是 Redis 代碼部分:

trip-core 的pom.xml

引入依賴 redis

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

trip-core 的service

@Service
public class UserInfoRedisServiceImpl implements IUserInfoRedisService {
    
    @Autowired
    private StringRedisTemplate template;
}

trip-website-api 的配置文件

為什麼配置文件在 trip-website-api 而不是在 trip-core 裏面?

如果 trip-core 裏面有配置文件,在 trip-website-api 引入其 jar 包時,就會被覆蓋

同理,trip-mgrsite 裏面也會被覆蓋

然後二者各自還要配置自己需要的,還不如直接自己配置自己的

#redis
spring.redis.host=127.0.0.1

工具類

提供兩個工具類:

  • Consts :提供時間常量

    /** * 系統常量 */
    public class Consts {
          
        //驗證碼有效時間
        public static final int VERIFY_CODE_VAI_TIME = 5;  //單比特分
        //token有效時間
        public static final int USER_INFO_TOKEN_VAI_TIME = 30;  //單比特分
    }
    
  • JsonResult :提供返回結果

    @Setter
    @Getter
    @NoArgsConstructor
    public class JsonResult<T> {
          
        public static final int CODE_SUCCESS = 200;
        public static final String MSG_SUCCESS = "操作成功";
        public static final int CODE_NOLOGIN = 401;
        public static final String MSG_NOLOGIN = "請先登錄";
    
        public static final int CODE_ERROR = 500;
        public static final String MSG_ERROR = "系統异常,請聯系管理員";
    
        public static final int CODE_ERROR_PARAM = 501;  //參數异常
    
        private int code;  //區分不同結果, 而不再是true或者false
        private String msg;
        private T data;  //除了操作結果之後, 還行攜帶數據返回
        public JsonResult(int code, String msg, T data){
          
            this.code = code;
            this.msg = msg;
            this.data = data;
        }
        public static <T> JsonResult success(T data){
          
            return new JsonResult(CODE_SUCCESS, MSG_SUCCESS, data);
        }
    
        public static JsonResult success(){
          
            return new JsonResult(CODE_SUCCESS, MSG_SUCCESS, null);
        }
    
        public static <T>  JsonResult error(int code, String msg, T data){
          
            return new JsonResult(code, msg, data);
        }
    
        public static JsonResult defaultError(){
          
            return new JsonResult(CODE_ERROR, MSG_ERROR, null);
        }
    
        public static JsonResult noLogin() {
          
            return new JsonResult(CODE_NOLOGIN, MSG_NOLOGIN, null);
        }
    }
    

後端控制器方法

@GetMapping("/sendVerifyCode")
public Object sendVerifyCode(String phone){
    
    userInfoService.sendVerifyCode(phone);
    return JsonResult.success();
}

MongoDB的service (trip-website-api)

@Autowired
private IUserInfoRedisService userInfoRedisService;
@Override
public void sendVerifyCode(String phone) {
    
    //創建驗證碼
    String code = UUID.randomUUID().toString()
            .replaceAll("-", "")
            .substring(0, 4);
    //創建短信
    StringBuilder sb = new StringBuilder(80);
    sb.append("您注册的短信驗證碼是:").append(code).append(",請在")
            .append(Consts.VERIFY_CODE_VAI_TIME)
            .append("分鐘內使用");
    //假裝短信已發送
    System.out.println(sb);
    //將驗證碼緩存到redis中
    userInfoRedisService.setVerifyCode(phone,code);
}

Redis的service (trip-core)

@Service
public class UserInfoRedisServiceImpl implements IUserInfoRedisService {
    
    @Autowired
    private StringRedisTemplate template;

    //將驗證碼緩存到redis中
    @Override
    public void setVerifyCode(String phone, String code) {
    
        //key 必須唯一、可讀、有效、靈活
        String key = "verify_code:" + phone;
        template.opsForValue()
                //參數1:key值; 參數2:value值; 參數3:有效時間; 參數4:時間單比特
                .set(key, code, Consts.VERIFY_CODE_VAI_TIME*60L, TimeUnit.SECONDS);
    }
}

點擊注册

自定義异常

/** * 自定義异常, 用於區分給用戶看异常與系統异常 */
public class LogicException extends RuntimeException {
    
    public LogicException(String message) {
    
        super(message);
    }
}

參數斷言判斷的工具類

/** * 參數斷言判斷 */
public class AssertUtils {
    
    /** * 判斷指定value參數是否有值, 如果沒有拋出异常, 信息: msg * @param v * @param msg */
    public static void hasLength(String v, String msg) {
    
        if(!StringUtils.hasLength(v)){
    
            throw new LogicException(msg);
        }
    }

    /** * 判斷傳入的2個參數是否相等 * @param v1 * @param v2 * @param msg */
    public static void isEquals(String v1 , String v2, String msg) {
    
        if(v1 == null || v2 == null){
       //判斷,工具類必須嚴謹
            throw new LogicException("傳入參數不能為null");
        }
        if(!v1.equals(v2)){
    
            throw new LogicException(msg);
        }
    }
}

實現注册

後端控制器

@PostMapping("/regist")
public Object regist(String phone, String nickname,String password,String rpassword,String verifyCode){
    
    userInfoService.regist(phone,nickname,password,rpassword,verifyCode);
    return JsonResult.success();
}

MongoDB 中 service 實現類

@Override
public void regist(String phone, String nickname, String password, String rpassword, String verifyCode) {
    
    //校驗參數是否為空
    AssertUtils.hasLength(phone , "手機號不可為空");
    AssertUtils.hasLength(nickname , "昵稱不可為空");
    AssertUtils.hasLength(password , "密碼不可為空");
    AssertUtils.hasLength(rpassword , "確認密碼不可為空");
    AssertUtils.hasLength(verifyCode , "驗證碼不可為空");
    //校驗兩次密碼是否相等
    AssertUtils.isEquals(password,rpassword,"兩次輸入的密碼不一致");
    //校驗手機號碼是否正確 @Todo java的正則錶達式
    //校驗手機號是否唯一
    if (!this.checkPhone(phone)){
    
        throw new RuntimeException("該手機號碼已經被注册");
    }
    //從redis獲取驗證碼,再校驗短信驗證碼是否正確
    String code = userInfoRedisService.getVerifyCode(phone, verifyCode);
    if (!verifyCode.equalsIgnoreCase(code)){
    
        throw new RuntimeException("驗證碼失效或錯誤");
    }
    //注册
    UserInfo userInfo = new UserInfo();
    userInfo.setNickname(nickname);
    userInfo.setPhone(phone);
    userInfo.setEmail("");
    userInfo.setPassword(password);  //假裝加密
    userInfo.setGender(UserInfo.GENDER_SECRET);
    userInfo.setLevel(1);
    userInfo.setCity("");
    userInfo.setHeadImgUrl("/images/default.jpg");
    userInfo.setInfo("");

    //核心屬性必須自己控制,就算實體類裏面有默認值,為了防止意外最好自己添加
    userInfo.setState(UserInfo.STATE_NORMAL);

    this.save(userInfo);
}

Redis 中 service 實現類

//從redis獲取驗證碼
@Override
public String getVerifyCode(String phone, String verifyCode) {
    
    String key = "verify_code:" + phone;
    return template.opsForValue().get(key);
}

細節優化

統一异常處理

現在的异常只有後臺可以看到,用戶體驗不到。所以需要將异常信息反饋給用戶,需要捕獲异常

但是存在用戶提示异常、系統异常,系統异常需要美化,所以使用自定義异常類來區分兩種异常

  1. 可以選擇直接修改後端控制器方法(不推薦)

    這樣很麻煩,所以選擇統一异常處理

    @PostMapping("/regist")
    public Object regist(String phone, String nickname,String password,String rpassword,String verifyCode){
          
        try {
          
            userInfoService.regist(phone,nickname,password,rpassword,verifyCode);
        } catch (LogicException e) {
          
            e.printStackTrace();
            //給用戶看得提示异常
            return JsonResult.error(JsonResult.CODE_ERROR_PARAM, e.getMessage(), null);
        } catch (Exception e) {
          
            /* 這種抓取异常方式存在問題: 該給用戶看的提示异常、系統的异常,都在這個比特置被抓取了 而真正的要求是系統的异常要美化 此時需要操作區分:區分系統异常、用戶提示异常 */
            e.printStackTrace();
            //出問題後給頁面提示信息
            return JsonResult.defaultError();
        }
        return JsonResult.success();
    }
    
  2. 自定義异常處理類

    這個類是為了 controller 方法定義的,所以建議聲明在 trip-website-api 模塊裏面

    /** * 通用异常處理類 * @ControllerAdvice:controller類功能增强注解,動態代理controller類實現一些額外功能 * 作用: * 請求進入controller方法之前做功能增强:日期格式化 * 請求進入controller方法之後做功能增强:統一异常處理 */
    @ControllerAdvice
    public class CommonExceptionHandler {
          
        
        @ExceptionHandler(LogicException.class)
        @ResponseBody
        public Object logicExp
            (Exception e, HttpServletResponse resp) throws IOException {
          
            e.printStackTrace();
            resp.setContentType("application/json;charset=utf-8");
            return JsonResult.error(JsonResult.CODE_ERROR_PARAM, e.getMessage(), null);
        }
        
        @ExceptionHandler(RuntimeException.class)
        @ResponseBody
        public Object runtimeExp
            (Exception e, HttpServletResponse resp) throws IOException {
          
            e.printStackTrace();
            resp.setContentType("application/json;charset=utf-8");
         return JsonResult.defaultError();
        }
    

}


現在的後端控制器方法

```java
@PostMapping("/regist")
public Object regist(String phone, String nickname,String password,String rpassword,String verifyCode){
    userInfoService.regist(phone,nickname,password,rpassword,verifyCode);
    return JsonResult.success();
}

_class屬性排除

因為給 MongoDB 添加數據,自動會多一列 _class 屬性

在啟動類裏面添加以下代碼:

//mongodb 去除_class屬性
@Bean
public MappingMongoConverter mappingMongoConverter(MongoDbFactory factory, MongoMappingContext context, BeanFactory beanFactory) {
    
    DbRefResolver dbRefResolver = new DefaultDbRefResolver(factory);
    MappingMongoConverter mappingConverter = new MappingMongoConverter(dbRefResolver, context);
    try {
       mappingConverter.setCustomConversions(beanFactory.getBean(CustomConversions.class));
    } catch (NoSuchBeanDefinitionException ignore) {
    
    }
    // Don't save _class to mongo
    mappingConverter.setTypeMapper(new DefaultMongoTypeMapper(null));
    return mappingConverter;
}

Redis key重設計:枚舉類

前面對於驗證碼存入 redis 時,key 都是直接保存為 手機號:

String key = "verify_code:" + phone;

當項目多人開發時,其他人也可能使用以上字符串作為 key ,就會導致不唯一

手寫容易寫錯

怎麼管理 redis 的 key ?

使用 枚舉類 直接定義死 前面的前綴拼接

/* 枚舉類特點: 1、構造器私有 2、當類定義完成之後,實例個數固定 3、剩下操作和普通類一樣 */
/** * redis key管理 * 約定:一個枚舉實例就是一個 key */
@Getter
public enum  RedisKeys{
    
    //短信驗證碼
    VERIFY_CODE("verify_code", Consts.VERIFY_CODE_VAI_TIME * 60L);

    private String prefix;  //redis的key的前綴
    private Long time;  //redis的key的有效時間,-1L 錶示不需要指定過期時間,單比特 秒
    private RedisKeys(String prefix, Long time){
    
        this.prefix = prefix;
        this.time = time;
    }
    
    //拼接完整的redis的 key
    public String join(String ...keys){
    
        StringBuilder sb = new StringBuilder();
        sb.append(prefix);
        for (String key : keys) {
    
            sb.append(":").append(key);
        }
        return sb.toString();
    }
}

修改 redis 的 service 實現類

@Service
public class UserInfoRedisServiceImpl implements IUserInfoRedisService {
    
    @Autowired
    private StringRedisTemplate template;

    @Override
    public void setVerifyCode(String phone, String code) {
    
        //key 必須唯一、可讀、有效、靈活
        String key = RedisKeys.VERIFY_CODE.join(phone);
        template.opsForValue()
                //參數1:key值; 參數2:value值; 參數3:有效時間; 參數4:時間單比特
                .set(key, code, RedisKeys.VERIFY_CODE.getTime(), TimeUnit.SECONDS);
    }

    @Override
    public String getVerifyCode(String phone, String verifyCode) {
    
        String key = RedisKeys.VERIFY_CODE.join(phone);
        return template.opsForValue().get(key);
    }
}

發送真實短信

短信原理分析

需要第三方中間商,使用短信網關實現

就是針對網關暴露出來的接口,進行調用,攜帶網管提供的 appkey ,

短信網關

京東萬象:https://wx.jdcloud.com/api-66

中國網建:http://sms.webchinese.com.cn/

阿裏短信

這裏使用京東萬象,登錄進頁面。選擇一個短信接口,點擊測試

右邊會有短信網關提供的接口

用戶注册(加入短信驗證)

SpringMVC 提供了一個工具類 RestTemplate 用於發起 http 請求,使用此類需要引入 web 依賴

需要引入以下依賴:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    <scope>provided</scope>  編譯時有效,運行時無效
</dependency>

修改 MongoDB 中 業務實現類

@Override
public void sendVerifyCode(String phone) {
    
    //創建驗證碼
    String code = UUID.randomUUID().toString()
            .replaceAll("-", "")
            .substring(0, 4);
    //創建短信
    StringBuilder sb = new StringBuilder(80);
    sb.append("您注册的短信驗證碼是:").append(code).append(",請在")
            .append(Consts.VERIFY_CODE_VAI_TIME)
            .append("分鐘內使用");
    //假裝短信已發送
    System.out.println(sb);
    
    //真實發送短信,本質就是使用java發起 http 請求即可。路徑就是短信網關提供的路徑
    //SpringMVC 提供了一個工具類 RestTemplate 用於發起 http 請求,使用此類需要引入 web 依賴
    RestTemplate template = new RestTemplate();

    //參數1:短信接口 url
    String url = "https://way.jd.com/chuangxin/dxjk?mobile={1}&content=【創信】{2},3分鐘內有效!&appkey={3}";
    //參數2:請求接口完之後響應數據封裝的對象類型
    //參數3:請求參數列錶。通過在接口地址上進行挖洞占比特,一一對應參數 ?mobile={1}
    String ret = template.getForObject(url ,String.class, phone, sb.toString(),
                                       "5572fb4609843c40daa7e517e2a65b70");
    System.out.println(ret);
    if (!ret.contains("Success")){
    
        throw new LogicException("短信發送失敗");
    }

    //將驗證碼緩存到redis中
    userInfoRedisService.setVerifyCode(phone,code);
}

20220325124315005

短信硬編碼優化

因為上面代碼將 短信內容、網關接口 寫死了,後續修改不方便

可以將信息放入配置文件

配置文件:

#短信
sms.url=https://way.jd.com/chuangxin/dxjk?mobile={1}&content=【創信】{2},3分鐘內有效!&appkey={3}
sms.appkey=5572fb4609843c40daa7e517e2a65b70

業務實現類:

@Value("sms.url")
private String url;
@Value("sms.appkey")
private String appkey;

@Override
public void sendVerifyCode(String phone) {
    
    //創建驗證碼
    String code = UUID.randomUUID().toString()
            .replaceAll("-", "")
            .substring(0, 4);
    //創建短信
    StringBuilder sb = new StringBuilder(80);
    sb.append("您注册的短信驗證碼是:").append(code).append(",請在")
            .append(Consts.VERIFY_CODE_VAI_TIME)
            .append("分鐘內使用");
    //假裝短信已發送
    System.out.println(sb);

    //真實發送短信,本質就是使用java發起 http 請求即可。路徑就是短信網關提供的路徑
    //SpringMVC 提供了一個工具類 RestTemplate 用於發起 http 請求,使用此類需要引入 web 依賴
    RestTemplate template = new RestTemplate();

    //參數1:短信接口 url
    //參數2:請求接口完之後響應數據封裝的對象類型
    //參數3:請求參數列錶。通過在接口地址上進行挖洞占比特,一一對應參數 ?mobile={1}
    String ret = template.getForObject(url ,String.class, phone, sb.toString(), appkey);
    System.out.println(ret);
    if (!ret.contains("Success")){
    
        throw new LogicException("短信發送失敗");
    }

    //將驗證碼緩存到redis中
    userInfoRedisService.setVerifyCode(phone,code);
}

4、用戶登錄

互聯網項目登錄

邏輯分析

傳統登錄邏輯分析:

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-1Uulfdkj-1651834690339)(C:\Users\ASUS\AppData\Roaming\Typora\typora-user-images\image-20220325154717004.png)]

互聯網登錄方式:令牌方式(token)+redis

邏輯分析:

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-YbGJVWNc-1651834690340)(C:\Users\ASUS\AppData\Roaming\Typora\typora-user-images\image-20220325154758045.png)]

首次登陸代碼實現

枚舉類 RedisKeys 中設置登錄token

//登錄token
LOGIN_TOKEN("user_login_token", Consts.USER_INFO_TOKEN_VAI_TIME * 60L);

MongoDB 中 service 實現類:

@Override
public UserInfo login(String username, String password) {
    
   
    UserInfo user = repository.findByPhone(username);
    
    if (user == null || !user.getPassword().equals(password)) {
    
        throw new LogicException("賬號或密碼錯誤");
    }
    //防止將user傳遞給瀏覽器時,看到用戶的真實密碼
    user.setPassword("");
    return user;
}

redis 中 service 實現類:

@Override
public String setToken(UserInfo user) {
    
    // 1、創建token
    String token = UUID.randomUUID().toString().replaceAll("-", "");
    
    // 2、將token作為key,用戶對象作為value,設置到redis中
    String key = RedisKeys.LOGIN_TOKEN.join(token);
    String value = JSON.toJSONString(user);
    
    // 3、將數據存到redis中,設置有效時間30分鐘
    template.opsForValue().set(key,value,RedisKeys.LOGIN_TOKEN.getTime(), TimeUnit.SECONDS);
    
    return token;
}

後端控制器方法

@Autowired
private IUserInfoRedisService userInfoRedisService;
@PostMapping("/login")
public Object login(String username,String password){
    
    //user
    UserInfo user = userInfoService.login(username,password);
    //token
    String token = userInfoRedisService.setToken(user);

    Map<String, Object> map = new HashMap<>();
    map.put("user",user);
    map.put("token",token);
    return JsonResult.success(map);
}

二次訪問代碼實現

需求:頁面通過 /users/currentUser 接口獲取到redis中當前登錄用戶信息(注意需要攜帶參數token)

<script>
    //需求:頁面通過/users/currentUser接口獲取到redis中當前登錄用戶信息(注意需要攜帶參數token)
    $(function () {
    
        /*$.ajax({ url:domainUrl+"/users/currentUser", data:{}, beforeSend:function (xhr) { //請求發送前執行邏輯 // xhr:ajax執行對象 //getToken():前端自定義方法,獲取到前面存儲在cookie中的 token xhr.setRequestHeader("token", Cookies.get('token')); }, success:function (data) { console.log(data); } })*/
    
        // 對上面代碼進行封裝,然後使用以下代碼進行調用
        ajaxGet("/users/currentUser", {
    }, function (data) {
    
            console.log(data);
        })
    })
</script>

後端查詢登錄用戶

後端控制器方法:

@GetMapping("/currentUser")
public Object currentUser(HttpServletRequest request){
    
    String token = request.getHeader("token");
    UserInfo user = userInfoRedisService.getUserByToken(token);
    return JsonResult.success(user);
}

redis 中 service 實現類:

@Override
public UserInfo getUserByToken(String token) {
    
    if (!StringUtils.hasLength(token)){
    
        return null;
    }
    String key = RedisKeys.LOGIN_TOKEN.join(token);
    if (!template.hasKey(key)){
    
        return null;
    }
    String user = template.opsForValue().get(key);
    //JSON的解析操作,將JSON字符串還原為java對象
    UserInfo userInfo = JSON.parseObject(user, UserInfo.class);
    //重置token的有效時間30分鐘
    template.expire(key, RedisKeys.LOGIN_TOKEN.getTime(), TimeUnit.SECONDS);
    return userInfo;
}

瀏覽器代碼分析

在首次登陸,點擊登錄之後,瀏覽器接收並解析相應數據得到 token 、user。存儲到 cookie 中,並設置有效時間 30 分鐘

實現了如果在一個頁面點擊了登錄,登錄之後會回到這個頁面。

如果這個頁面是注册頁面、登錄頁面,就跳轉到首頁

登錄時代碼如下:

$("#_js_loginBtn").click(function () {
    
    $("#_j_login_form").ajaxSubmit({
    
        url:domainUrl +"/users/login",
        type:"POST",
        success:function (data) {
    
            if(data.code == 200){
    
                var map = data.data;
                var token = map.token;  //後續後端獲取當前登錄用戶信息
                var user = map.user;  //前端頁面需要顯示用戶信息
                // 1、sessionStorage 客戶端技術可以在瀏覽器窗口存儲數據, 一但關閉窗口,
                						// 數據就沒了, 如果多個窗口, 數據無法共享
                // 2、localStorage 客戶端技術可以在瀏覽器窗口存儲數據, 數據操作是永久
                // 3、cookie 客戶端技術可以在瀏覽器窗口存儲數據, 特點有時效性

                //參數1:cookie的key值, 參數2: cookie的value值, 參數3: 有效時間, 單比特天
                Cookies.set('user', JSON.stringify(user), {
     expires: 1/48,path:'/'});
                Cookies.set('token', token, {
     expires: 1/48,path:'/'});

                // document.referrer 上一個請求路徑
                //實現了如果在一個頁面點擊了登錄,登錄之後會回到這個頁面
                var url = document.referrer ? document.referrer : "/";

                //如果這個頁面是注册頁面、登錄頁面,就跳轉到首頁
               if(url.indexOf("regist.html") > -1 || url.indexOf("login.html") > -1){
    
                    url = "/";
                }
                window.location.href = url
            }else{
    
                popup(data.msg);
            }
        }
    })
})

二次登陸時,前端也會延長 cookie 的有效時間

//延長登錄時間
var token = Cookies.get("token");
var user = Cookies.get("user");
if(token&&user){
    
    Cookies.set('token', token, {
     expires: 1/48,path:'/'});
    Cookies.set('user', user, {
     expires: 1/48,path:'/'});
}

將數據庫中的用戶信息,顯示前端頁面

顯示頭像代碼:

<script>
    var user = getUserInfo();
    if(user){
    
        $(".login_info").css("display", "")
        $(".login-out").css("display", "none")

        $("#login_user_headUrl").prop("src", user.headImgUrl);
    }else{
    
        $(".login-out").css("display", "")
        $(".login_info").css("display", "none")
    }
    $("li[name="+currentNav+"]").addClass("header_nav_active");
</script>

登錄控制——請求統一攔截

分析

登錄控制可以通過 過濾器 filter 、攔截器 interceptor 兩種方式實現

但是本例中使用的是 SpringBoot 基礎的框架,可選擇 攔截器 interceptor

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-WexlIrPc-1651834690341)(C:\Users\ASUS\AppData\Roaming\Typora\typora-user-images\image-20220328095556644.png)]

代碼實現

因為現在的登錄控制,控制的是前端用戶的登錄,所以攔截器放在 trip-website-api 模塊裏面比較合適

自定義攔截器:

這裏需要 解决攔截器的跨域問題

/** * 用戶登錄檢查 */
public class CheckLoginInterceptor implements HandlerInterceptor {
    
    @Autowired
    private IUserInfoRedisService userInfoRedisService;

    //請求進入是進行登錄攔截
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
    

        //解决攔截器跨域問題
        /* 原理: 如果請求不是動態的(靜態資源),handler對象就不是HandlerMethod類的實例 如果請求是跨域的(請求方式是:OPTIONS),handler對象也不是HandlerMethod類的實例 */
        if (!(handler instanceof HandlerMethod)){
    
            //說明訪問的是靜態資源,不是控制器的處理方法
            return true;
        }
        /** * HandlerMethod 請求映射方法信息(所在類的信息、方法信息[映射路徑/方法名/參數/注解/返回值...])的封裝對象 * 1、SpringMVC啟動時,掃描所有Controller類,解析所有映射方法,將每個映射方法封裝為一個對象 HandlerMethod * 該類包含所有請求映射方法信息 * 2、SpringMVC針對 請求映射方法信息封裝對象類 ,使用類似map的數據結構進行統一管理 * key:請求映射路徑 value:請求映射方法信息封裝對象類 * 3、頁面發起請求時,進入攔截器之後,SpringMVC自動解析請求路徑,得到 url * 獲取 url 之後,進而獲取 url 路徑對於的映射方法的 HandlerMethod 示例(handler) * 4、調用攔截器 preHandle 方法,並將請求對象request、響應對象response、映射方法對象handler 一起傳入 */

        String token = request.getHeader("token");
        UserInfo user = userInfoRedisService.getUserByToken(token);
        if (user == null) {
    
            //沒登錄
            response.setContentType("application/json;charset=utf-8");
            response.getWriter().write(JSON.toJSONString(JsonResult.noLogin()));
            return false;
        }
        return false;
    }
}

配置攔截器:

在主配置類中 implements WebMvcConfigurer ,實現 WebMvcConfigurer 此類,錶示配置的 web.xml

@SpringBootApplication
public class WebSite implements WebMvcConfigurer{
    
    
    //將攔截器注入Spring容器中,交給SpringBoot管理
    @Bean
    public CheckLoginInterceptor checkLoginInterceptor(){
    
        return new CheckLoginInterceptor();
    }
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
    
        registry.addInterceptor(checkLoginInterceptor())
                .addPathPatterns("/**")
                .excludePathPatterns("/users/checkPhone")
                .excludePathPatterns("/users/sendVerifyCode")
                .excludePathPatterns("/users/regist")
                .excludePathPatterns("/users/login");
    }

    //mongodb 去除_class屬性
    @Bean
    public MappingMongoConverter mappingMongoConverter(MongoDbFactory factory, MongoMappingContext context, BeanFactory beanFactory) {
    
        //......
    }

    //處理跨域訪問
    @Bean
    public WebMvcConfigurer corsConfigurer() {
    
        return new WebMvcConfigurer() {
    
            @Override
            //重寫父類提供的跨域請求處理的接口
            public void addCorsMappings(CorsRegistry registry) {
    
                //添加映射路徑
                registry.addMapping("/**")
                        //放行哪些原始域
                        .allowedOrigins("*")
                        //是否發送Cookie信息
                        .allowCredentials(true)
                        //放行哪些原始域(請求方式)
                        .allowedMethods("GET", "POST", "PUT", "DELETE","OPTIONS")
                        //放行哪些原始域(頭部信息)
                        .allowedHeaders("*")
                        //暴露哪些頭部信息(因為跨域訪問默認不能獲取全部頭部信息)
                        .exposedHeaders("Header1", "Header2");
            }
        };
    }

    public static void main(String[] args) {
    
        SpringApplication.run(WebSite.class,args);
    }
}

為什麼要二次解决跨域?

明明之前在啟動類裏面已經處理了跨域問題,為什麼現在在自定義攔截器類裏面又要處理一次跨域問題

跨域請求原理:

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-h203Y3MB-1651834690342)(C:\Users\ASUS\AppData\Roaming\Typora\typora-user-images\image-20220328102701218.png)]

攔截器放行:

本例中,首次預請求的時候,沒有攜帶 cookie 的參數,在攔截器的比特置就被攔截,導致第二步瀏覽器沒有收到允許的指令,就直接報錯。訪問失敗

所以需要在攔截器對跨域請求進行放行

if (!(handler instanceof HandlerMethod)){
    
    //說明訪問是靜態的,或者請求是跨域的
    return true;
}

登錄控制——請求區分攔截

對於旅遊網頁,就算用戶不登錄應該也可以看到一部分的網頁信息,所以此處有新的需求

分析

**需求:**要求部分請求必須登錄之後才可以訪問,但是部分請求就算不登錄也可以訪問

此處就可以想到 RBAC 項目中權限控制 的操作:

使用注解來控制,需要控制權限的就加上注解,不需要控制的就不加上注解

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-8UaJXBgy-1651834690343)(C:\Users\ASUS\AppData\Roaming\Typora\typora-user-images\image-20220328103651438.png)]

代碼實現

trip-website-api 自定義注解

因為和攔截器相關,所以放在 trip-website-api 模塊

/** * 登錄校驗注解 * 約定:如果該注解貼某個映射方法上面,錶示該映射方法必須登錄之後才可以訪問 * 如果某個映射方法沒有該注解,錶示隨意 */
@Target(ElementType.METHOD)  //錶示該注解貼的是方法上面
@Retention(RetentionPolicy.RUNTIME)  //錶示該注解在代碼執行時
public @interface RequireLogin {
     }

在攔截器裏實現邏輯

需要注意:

​ 在攔截器裏面,對於讓登錄的 token 增加有效時間的方法,需要讓其一直能執行到,不能放在條件判斷裏面。

​ 否則會出現無法登陸的 bug

/** * 用戶登錄檢查 */
public class CheckLoginInterceptor implements HandlerInterceptor {
    
    @Autowired
    private IUserInfoRedisService userInfoRedisService;

    //請求進入是進行登錄攔截
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
    
        
        /* 原理: 如果請求不是動態的(靜態資源),handler對象就不是HandlerMethod類的實例 如果請求是跨域的(請求方式是:OPTIONS),handler對象也不是HandlerMethod類的實例 */
        //解决攔截器跨域問題
        if (!(handler instanceof HandlerMethod)){
    
            return true;
        }

        //判斷當前請求映射方法是否貼有 @RequireLogin 注解
        HandlerMethod hm = (HandlerMethod) handler;

        // getUserByToken()方法底層會重置token的有效時間30分鐘
        //所以,注意此行代碼需要放在判斷的 if 語句外面,以防出現登錄不上的 bug
        String token = request.getHeader("token");
        UserInfo user = userInfoRedisService.getUserByToken(token);
        
        if (hm.hasMethodAnnotation(RequireLogin.class)){
    
            //如果有,需要進行登錄校驗
            if (user == null) {
    
                //沒登錄
                response.setContentType("application/json;charset=utf-8");
                response.getWriter().write(JSON.toJSONString(JsonResult.noLogin()));
                return false;
            }
        }
        //如果沒有,直接放行
        return true;
    }
}

5、目的地

trip-mgrsite 模塊配置

現在做的是數據管理後端,需要單獨模塊,也需要啟動類

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>parent</artifactId>
        <groupId>com.afei</groupId>
        <version>1.0.0</version>
        <relativePath>../../wolf2w/pom.xml</relativePath>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>trip-mgrsite</artifactId>

    <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>com.afei</groupId>
            <artifactId>trip-core</artifactId>
        </dependency>
    </dependencies>

</project>

配置文件

server.port=9999
#spring.data.mongodb.uri=mongodb://127.0.0.1:27017,127.0.0.1:27018,127.0.0.1:27019/wolf2w?replicaSet=rs
spring.data.mongodb.uri=mongodb://127.0.0.1:27017/wolf2w
logging.level.org.springframework.data.mongodb.core= DEBUG

spring.freemarker.suffix=.ftl
#freemarker去除數字間隔符:330,000
spring.freemarker.settings.number_format=0.##

啟動類

@SpringBootApplication
public class MgrSite {
    
    public static void main(String[] args) {
    
        SpringApplication.run(MgrSite.class,args);
    }
}

靜態頁面

區域管理

錶、模型

添加 MongoDB 數據庫錶:地區(destination_region)

refIds Array 列是關聯的目的地的 id

db.getCollection("destination_region").drop();
db.createCollection("destination_region");

db.getCollection("destination_region").insert([ {
    _id: ObjectId("5e2aa2e1884700005900694a"),
    name: "日本",
    sn: "Japan",
    ishot: NumberInt("1"),
    sequence: NumberInt("2"),
    info: "日本",
    refIds: [
        ObjectId("5e2ab4f48847000059006e65"),
        ObjectId("5e2ab4f48847000059006e66"),
        ObjectId("5e2ab4f48847000059006e67"),
        ObjectId("5e2ab4f48847000059006e68")
    ]
} ]);
-- ... 此處省略

domain

/** * 區域 */
@Setter
@Getter
@Document("destination_region")
public class Region extends BaseDomain {
    
    public static final int STATE_HOT = 1;
    public static final int STATE_NORMAL = 0;
    
    private String name;        //地區名
    private String sn;          //地區編碼
    private List<String> refIds = new ArrayList<>();     //關聯的id
    private int ishot = STATE_NORMAL;         //是否為熱點
    private int sequence;   //序號
    private String info;  //簡介
    
    public String getJsonString(){
    
        Map<String, Object> map = new HashMap<>();
        map.put("id", id);  //注意,此處需要處理
        map.put("name",name);
        map.put("sn",sn);
        map.put("refIds",getRefIds());
        map.put("ishot",ishot);
        map.put("sequence",sequence);
        map.put("info",info);
        return JSON.toJSONString(map);
    }
}

因為 id 是從父類 BaseDomain 中獲取的,有兩種方式:

  1. 若父類中是 private 修飾:

    不可以直接獲取,會報錯。使用 super.getId()

  2. 若父類中是 protected 修飾:

    直接 id 獲取,不報錯

CRUD 准備

參數查詢對象

@Setter
@Getter
public class RegionQuery extends QueryObject {
     }

持久化接口

//區域持久化操作接口,類似 Mapper 接口
@Repository
public interface RegionRepository extends MongoRepository<Region, String> {
     }

業務接口及實現類

@Service
public class IRegionServiceImpl implements IRegionService {
    
    @Autowired
    private RegionRepository regionRepository;
    
    // ......
}

分頁列錶

PageHelper 是一個 MyBatis 的分頁插件,是一個 PageInfo<> 對象

Page<> 對象是 spring-data 提供的,需要使用 MongodbTemplate

springdata 是spring團隊為了同一數據庫(關系型與非關系型)操作而提供一套訪問數據庫的API規範

MongoDB的service

//直接使用
@Autowired
private MongoTemplate mongoTemplate;
@Override
public Page<Region> query(RegionQuery qo) {
    
    // 創建查詢對象:理解為MQL語句拼接對象
    Query query = new Query();
    //總數total
    Long total = mongoTemplate.count(query, Region.class);
    if(total == 0){
    
        return Page.empty();  //空的分頁對象
    }
    //分頁參數對象pageable
    Pageable pageable = PageRequest.of(qo.getCurrentPage()-1, qo.getPageSize(), Sort.Direction.ASC, "id");
    //分頁數據content
    query.with(pageable);
    List<Region> list = mongoTemplate.find(query, Region.class);

    //需要三個參數:List<T> content, Pageable pageable, long total
    return new PageImpl<Region>(list,pageable,total);
}

後端控制器方法

@Controller
@RequestMapping("region")
public class RegionController {
    
    @Autowired
    private IRegionService regionService;
    @RequestMapping("/list")
    public String list(Model model, @ModelAttribute("qo") RegionQuery qo){
    
        Page<Region> page = regionService.query(qo);
        model.addAttribute("page",page);
        return "region/list";
    }
}

添加與編輯

添加編輯需要注意點, 關聯的目的地需要是多選的

下拉框數據顯示、搜索

顯示:

@RequestMapping("/getDestByDeep")
public Object getDestByDeep(){
    
    return destinationService.list();
}
$.get("/region/getDestByDeep", {
    deep:3}, function (data) {
    
    var html = '';
    $.each(data, function (index, item) {
    
        html += '<option value="' + item.id + '">'+item.name+'</option>'
    })
    $("#refIds").html(html);

    $('#refIds').selectpicker('refresh'); //刷新組件
})

下拉搜索框:

使用 bootstrap-select 插件:引入插件之後,在下拉框的 class 屬性中,使用 selectpicker 即可。

multiple 的作用是可以在下拉框中實現多選

<select class="form-control selectpicker " multiple id="refIds" name="refIds" data-live-search="true" title="請選擇關聯的目的地">
</select>

在下拉框中需要刷新

$('#refIds').selectpicker('refresh'); //刷新組件

添加與編輯

在區域的 service 實現類中,給保存數據的方法設置空的 id 。MongoDB 會在添加的時候無法設置自動生成的 id 值(後面遊記的時候會說)

@Service
public class RegionServiceImpl implements IRegionService {
    
    @Autowired
    private RegionRepository regionRepository;

    @Override
    public void save(Region region) {
    
        region.setId(null);
        regionRepository.save(region);
    }
    
    // ......
}

後端控制器方法

@RequestMapping("/saveOrUpdate")
@ResponseBody
public Object saveOrUpdate(Region region){
    
    regionService.saveOrUpdate(region);
    return JsonResult.success();
}

service 實現類

@Override
public void saveOrUpdate(Region region) {
    
    if (StringUtils.hasLength(region.getId())){
     //編輯
        /** * 注意,MongoDB的Repository接口的更新操作是全量更新,容易出現參數丟失問題 * 此處沒有出現問題,是因為編輯頁面需要輸入的數據個數和錶中列數一致。是全量 */
        this.update(region);
    }else {
      // 添加
        this.save(region);
    }
}

查看

查看:指定區域下掛載的關聯目的地

後端控制器方法

@RequestMapping("/getDestByRegionId")
@ResponseBody
public Object getDestByRegionId(String rid){
    
    return destinationService.queryDestByRegionId(rid);
}

service 實現類

@Autowired
private IRegionService regionService;
@Override
public List<Destination> queryDestByRegionId(String rid) {
    
    //獲取區域對象
    Region region = regionService.get(rid);
    //通過區域對象獲取管理目的地id集合
    List<String> ids = region.getRefIds();
    return destinationRepository.findByIdIn(ids);
}

這裏使用了 MongoDB 的 JPA 查詢規範: 參考下圖

設置熱門

後端控制器方法

@RequestMapping("/changeHotValue")
@ResponseBody
public Object changeHotValue(String id, Integer hot){
    
    regionService.changeHotById(id, hot);
    return JsonResult.success();
}

service 實現類

@Override
public void changeHotById(String id, Integer hot) {
    
    //全量更新
    /*Region region = this.get(id); region.setIshot(hot); this.update(region);*/

    //部分字段更新 :db.集合名.updateMany({_id: "123"},{$set: {ishot: 1}})
    Query query = new Query();
    query.addCriteria( Criteria.where("_id").is(id));
    Update update = new Update();
    update.set("ishot",hot);
    mongoTemplate.updateMulti(query, update, Region.class);
}

删除

@RequestMapping("/delete")
@ResponseBody
public Object delete(String id){
    
    regionService.delete(id);
    return JsonResult.success();
}

目的地管理

錶、模型

添加 MongoDB 數據庫錶:目的地錶(destination)

屬性名稱屬性類型屬性說明
IdString主鍵
parentIdString父級id
parentNameString父級名稱
nameString目的地名
englishString英文
coverUrlString封面
infoString簡介,描述

domain

/** * 目的地(行政地區:國家/省份/城市) */
@Setter
@Getter
@Document("destination")
public class Destination extends BaseDomain {
    
    private String name;        //名稱
    private String english;  //英文名
    private String parentId; //上級目的地
    private String parentName;  //上級目的名
    private String info;    //簡介
    private int deep;
    private  String coverUrl;
    
    //子地區 org.springframework.data.annotation.Transient
    @Transient  //MongoDB添加時忽略該字段,不需要添加到庫中
    private List<Destination> children = new ArrayList<>();
    
    public String getJsonString(){
    
        Map<String,Object> map = new HashMap<>();
        map.put("id", super.getId());
        map.put("info", this.info);
        return JSON.toJSONString(map);
    }
}

CRUD 准備

參數查詢對象

@Setter
@Getter
public class DestinationQuery extends QueryObject {
     }

持久化接口

//區域持久化操作接口,類似 Mapper 接口
@Repository
public interface DestinationRepository extends MongoRepository<Destination, String> {
    }

業務接口及實現類

@Service
public class IDestinationServiceImpl implements IRegionService {
    
    @Autowired
    private DestinationRepository destinationRepository;
    
    // ......
}

分頁工具類

涉及高級查詢

因為分頁操作與 區域的分頁有很多相同代碼,所以抽取工具類

工具類

/** * 數據庫操作工具類 */
public class DBHelper {
    
    //Class<Destination> cla = Destination.class;
    public static <T> Page<T> query(Class<T> clazz, MongoTemplate template, 
                                    Query query, Pageable pageable, QueryObject qo) 
    {
    
        //total
        long total = template.count(query, clazz);
        if (total == 0){
    
            return Page.empty();
        }
        //list
        query.with(pageable);
        List<T> list = template.find(query, clazz);
        return new PageImpl<T>(list,pageable,total);
    }
}

service實現類 涉及高級查詢

//直接使用
@Autowired
private MongoTemplate template;
@Override
public Page<Destination> query(DestinationQuery qo) {
    
    Query query = new Query();
    
    //需要攜帶關鍵字查詢
    if (StringUtils.hasLength(qo.getKeyword())){
    
        query.addCriteria(Criteria.where("name").regex(qo.getKeyword()));
    }
    
    //pageable
    Pageable pageable = PageRequest.of(qo.getCurrentPage() - 1, 
                                       qo.getPageSize(), Sort.Direction.ASC, "id");
    return DBHelper.query(Destination.class,template,query,pageable,qo);
}

導航吐司

從國家比特置開始,一直往下探, 根>>中國>>廣東>>廣州

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-IFnAmUE4-1651834690344)(C:\Users\ASUS\AppData\Roaming\Typora\typora-user-images\image-20220329095400831.png)]

數據庫錶裏面設計了列 parentId ,可以通過上級 id 來實現導航吐司

數據顯示

數據庫錶裏面設計了列 parentId ,可以通過上級 id 來實現查詢當前點擊的目的地的 子級 目的地

頁面攜帶參數 <a href="/destination/list?parentId=${entity.id}">

添加查詢參數屬性

@Setter
@Getter
public class DestinationQuery extends QueryObject {
    
    private String parentId;
}

修改分頁查詢方法

@Override
public Page<Destination> query(DestinationQuery qo) {
    
    Query query = new Query();
    //攜帶關鍵字查詢
    if (StringUtils.hasLength(qo.getKeyword())){
    
        query.addCriteria(Criteria.where("name").regex(qo.getKeyword()));
    }
    //攜帶上級 id 查詢
    if (StringUtils.hasLength(qo.getParentId())){
    
        query.addCriteria(Criteria.where("parentId").is(qo.getParentId()));
    }else {
      //導航是 根 的情况
        query.addCriteria(Criteria.where("parentId").is(null));
    }
    //pageable
    Pageable pageable = PageRequest.of(qo.getCurrentPage() - 1, qo.getPageSize(), Sort.Direction.ASC, "id");
    return DBHelper.query(Destination.class,template,query,pageable,qo);
}

導航

要實現導航是多級導航,中國>>廣東>>廣州 。當點擊廣州,導航會出現 根、中國、廣東、廣州

有兩種實現方式: 1、遞歸; 2、 使用 while(父id不為null)

下面是遞歸方式:

service實現類

@Override
public List<Destination> getToasts(String desId) {
    
    List<Destination> list = new ArrayList<>();
    createToast(list,desId);
    //list集合元素順序調轉
    Collections.reverse(list);
    return list;
}
//示例:中國 >> 廣東 >> 廣州
private void createToast(List<Destination> list, String parentId){
    
    //遞歸前提:必須能出遞歸的循環
    if (!StringUtils.hasLength(parentId)){
      //當前id為空
        return;
    }
    Destination dest = this.get(parentId);
    list.add(dest);
    if (StringUtils.hasLength(dest.getParentId())){
      //當前的父id不為空
        createToast(list,dest.getParentId());  // 執行父id的方法
    }
}

後端控制器方法

@RequestMapping("/list")
public String list(Model model, @ModelAttribute("qo") DestinationQuery qo){
    
    //page
    Page<Destination> page = destinationService.query(qo);
    model.addAttribute("page", page);
    //toasts
    model.addAttribute("toasts", destinationService.getToasts(qo.getParentId()));
    return "destination/list";
}

前端熱門目的地

項目啟動後, 訪問 /views/destination/index.html 進入前端目的地展示頁面

分析

  1. 鼠標移動到不同區域,顯示該區域下掛載的目的地集合
  2. 同時將掛載目的地的所有子目的地集合進行列錶顯示

熱門目的地一

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-FF7Sl1z6-1651834690344)(C:\Users\ASUS\AppData\Roaming\Typora\typora-user-images\image-20220329112656884.png)]

因為現在寫的是前端頁面的數據,所以需要在 trip-website-api 模塊 裏面編寫

var vue = new Vue({
    
    el:"#app",
    data:{
    
        regions:[],  //熱門排序的區域集合
        destListLeft:[],
        destListRight:[],
        regionId:''
    },
    methods:{
    
		//.....
    },
    //數據初始化比特置
    mounted:function () {
    
        //熱門數據
        ajaxGet("/destinations/hotRegion",{
    }, function (data) {
    
            vue.regions = data.data;
        })

        //通過區域id查詢目的地
        this.queryRegion(-1);
    }
});

後端控制器方法

@RestController
@RequestMapping("destinations")
public class DestinationController {
    

    @Autowired
    private IRegionService regionService;

    @GetMapping("/hotRegion")
    public Object hotRegion(){
    
        List<Region> list = regionService.queryhotRegion();
        return JsonResult.success(list);
    }
}

service實現類

@Override
public List<Region> queryhotRegion() {
    
    return regionRepository.findByIshotOrderBySequence(1);
}

Repository 接口

//區域持久化操作接口,類似 Mapper 接口
@Repository
public interface RegionRepository extends MongoRepository<Region, String> {
    
    /** * 查詢熱門區域並排序 * @param hot * @return */
    List<Region> findByIshotOrderBySequence(int hot);
}

熱門目的地二

實體類中定義屬性,用於掛載目的地下的子目的地

/** * 目的地(行政地區:國家/省份/城市) */
@Setter
@Getter
@Document("destination")
public class Destination extends BaseDomain {
    
    
    //......

    //子地區 org.springframework.data.annotation.Transient
    @Transient  //MongoDB添加時忽略該字段,不需要添加到庫中
    private List<Destination> children = new ArrayList<>();
}

後端控制器方法

@GetMapping("/search")
public Object search(String regionId){
    
    List<Destination> list = destinationService.queryByRegionIdForApi(regionId);
    return JsonResult.success(list);
}

service實現類

@Override
public List<Destination> queryByRegionIdForApi(String rid) {
    
    //區域掛載的目的地
    List<Destination> list = null;
    if ("-1".equals(rid)){
    
        //查詢國內:查詢中國所有省份
        list = repository.findByParentName("中國");
    }else {
    
        list = this.queryDestByRegionId(rid);
    }
    //目的地下的子目的地
    for (Destination des : list) {
    
        //要求只顯示5個
        // 1、截取 children.subList(0,5) 包含0不包含5
        // 2、使用 JPA
        Pageable pageable = PageRequest.of(0,5);
        List<Destination> children = repository.findByParentId(des.getId(),pageable);
        des.setChildren(children); //是實體類定義的一個屬性
    }
    return list;
}

Repository 接口

List<Destination> findByParentName(String parentName);
List<Destination> findByParentId(String parentId);

6、旅遊攻略

攻略對象分析

一般錶數據量比較少的時候才需要單獨 序號 列來進行排序

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-BwhaqVbf-1651834690345)(C:\Users\ASUS\AppData\Roaming\Typora\typora-user-images\image-20220330094829732.png)]

導入數據庫數據:在文件所在目錄下執行以下操作 mongo localhost/wolf2w luowowoDemo.js

攻略分類(strategy_catalog)錶設計 :

屬性名稱屬性類型屬性說明
idString主鍵
nameString分類名稱
destIdString目的地id
destNameString目的地名稱
stateInt狀態
sequenceInt排序

攻略主題(strategy_theme)錶設計 :

屬性名稱屬性類型屬性說明
IdString主鍵
nameString攻略主題名字
stateInt狀態
sequenceint排序

攻略明細(strategy_detail)錶設計 :

屬性名稱屬性類型屬性說明
idString主鍵
destIdString目的地id
destNameString目的地名稱
themeIdString攻略主題id
themeNameString攻略主題name
catalogIdString分類id
catalogNameString分類名稱
titleString攻略標題
subTitleString攻略副標題
summaryString摘要,文章正文的前100個字
coverUrlString攻略封面
createtimeDate創建時間
isabroadboolean是否國外
viewnumInt閱讀人數
replynumInt回複人數
favornumint收藏人數
sharenumInt分享人數
thumbsupnumint點贊人數
stateint狀態 正常, 發布
contentString內容

CRUD准備

domain

QueryObject

repository

service

trip-mgrsite模塊的controller

攻略明細的添加:

1、上傳封面圖片

前端 uploadifive

前端需要使用上傳圖片的插件 uploadifive ,輸入框沒有要求,但是需要有 js 代碼,如下:

$(function () {
    


    //富文本框圖片配置
    var ck = CKEDITOR.replace( 'content',{
    
        filebrowserBrowseUrl: '/圖片服務器,假裝這裏有',
        filebrowserUploadUrl: '/uploadImg_ck'
    });

    //圖片上傳
    $('.imgBtn').uploadifive({
    
        'auto' : true,  //自動發起圖片上傳請求
        'uploadScript' : '/uploadImg',   //處理上傳文件的請求路徑
        buttonClass:"btn-link",
        'fileObjName' : 'pic',   //上傳文件參數名
        'buttonText' : '瀏覽...',
        'fileType' : 'image',
        'multi' : false,
        'fileSizeLimit'   : 5242880,
        'removeCompleted' : true, //取消上傳完成提示
        'uploadLimit' : 1,
        //'queueSizeLimit' : 10,
        'overrideEvents': ['onDialogClose', 'onError'],    //onDialogClose 取消自帶的錯誤提示
        'onUploadComplete' : function(file, data) {
    
            $("#imgUrl").attr("src" ,data);  //data約定是json格式 圖片地址
            $("#coverUrl").val(data);

        },
        onFallback : function() {
    
            $.messager.alert("溫馨提示","該瀏覽器無法使用!");
        }
    });
})

後端 阿裏OSS

封面圖片上傳之後,保存在哪裏?

因為現在是前後端分離,此封面前後端都要使用。可以將圖片放在網上(文件共享服務器/圖片共享服務器) ,讓都可以進行訪問

**操作原理:**將瀏覽器上傳圖片上傳到一個公共服務空間,並將這個圖片路徑返回即可

公共服務空間:公司自建服務器、新浪圖床、七牛圖床、阿裏對象存儲(OSS)

  1. 進入阿裏雲官網,搜索阿裏對象存儲

  2. 創建 Bucket

    [外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-ms980AJU-1651834690346)(C:\Users\ASUS\AppData\Roaming\Typora\typora-user-images\image-20220329180046591.png)]

  3. 注意域名,即訪問時文件路徑

    [外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-3ZGsdW0K-1651834690347)(C:\Users\ASUS\AppData\Roaming\Typora\typora-user-images\image-20220329180251731.png)]

  4. 上傳文件時如果有目錄,訪問文件時還需要在域名後加目錄路徑

java使用步驟:

  1. 導入依賴

    <!-- oss -->
    <dependency>
        <groupId>com.aliyun.oss</groupId>
        <artifactId>aliyun-sdk-oss</artifactId>
        <version>3.5.0</version>
    </dependency>
    <!-- io流處理 -->
    <dependency>
        <groupId>commons-io</groupId>
        <artifactId>commons-io</artifactId>
        <version>2.6</version>
    </dependency>
    
  2. 拷貝工具類

    /** * 文件上傳工具 */
    public class UploadUtil {
          
       //阿裏域名
       public static final String ALI_DOMAIN = "https://wolf2w42.oss-cn-shenzhen.aliyuncs.com/";
    
       public static String uploadAli(MultipartFile file) throws Exception {
          
          //生成文件名稱
          String uuid = UUID.randomUUID().toString();
          String orgFileName = file.getOriginalFilename();//獲取真實文件名稱 xxx.jpg
          String ext= "." + FilenameUtils.getExtension(orgFileName);//獲取拓展名字.jpg
          String fileName =uuid + ext;//xxxxxsfsasa.jpg
    
          // Endpoint以杭州為例,其它Region請按實際情况填寫。
          String endpoint = "http://oss-cn-shenzhen.aliyuncs.com";
          // 雲賬號AccessKey有所有API訪問權限,建議遵循阿裏雲安全最佳實踐,創建並使用RAM子賬號進行API訪問或日常運維,
          // 請登錄 https://ram.console.aliyun.com 創建。
          String accessKeyId = "LTAI4FcxXbQVaDXncy2bgaGz";
          String accessKeySecret = "wAsBTLhiNobVkwgmVUSPjdDpdxhhUz";
          // 創建OSSClient實例。
          OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId,accessKeySecret);
          // 上傳文件流。
          ossClient.putObject("wolf2w42", fileName, file.getInputStream());
          // 關閉OSSClient。
          ossClient.shutdown();
          return ALI_DOMAIN + fileName;
       }
    }
    
  3. 後端控制器方法使用

    @Controller
    public class UploadController {
          
        @Autowired
        private IStrategyService strategyService;
    
        @RequestMapping("/uploadImg")
        public String uploadImg(MultipartFile pic) throws Exception {
          
            //執行文件保存
            String path = UploadUtil.uploadAli(pic);
            return path;
        }
    }
    

2、分組下拉框

分析

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-fl4m6SEo-1651834690347)(C:\Users\ASUS\AppData\Roaming\Typora\typora-user-images\image-20220330122238805.png)]

代碼實現

定義 vo 類,用於封裝需要的數據,因為沒有對應的 domain 可以封裝,就可以自定義 VO 類

/** * 分組下拉框的vo對象 */
@Setter
@Getter
public class CatalogVO {
    
    private String destName;
    private List<StrategyCatalog> catalogList = new ArrayList<>();
}

操作哪種對象,就使用哪種服務。

所以將方法定義在 IStrategyCatalogService 接口裏面

@Override
public List<CatalogVO> groupList() {
    
    List<CatalogVO> list = new ArrayList<>();
    /* 等價於如下: db.strategy_catalog.aggregate([{ $group: { _id: "$destName", names: { $push: "$name" }, ids: { $push: "$_id" } } }]) */
    //聚合查詢
    TypedAggregation<StrategyCatalog> agg = Aggregation.newAggregation(StrategyCatalog.class,
            Aggregation.group("destName").
                    push("name").as("names").
                    push("id").as("ids")
    );
    //執行聚合查詢,指定返回的類型為 Map.class
    AggregationResults<Map> result = mongoTemplate.aggregate(agg,Map.class);
    List<Map> datas = result.getMappedResults(); //將結果轉化成list<map> map描述一行數據
    for (Map data : datas) {
      //key為字段名,value為字段值
        CatalogVO vo = new CatalogVO();
        vo.setDestName(data.get("_id").toString());  //id
        List<Object> names = (List<Object>) data.get("names");  //攻略分類的名稱集合
        List<Object> ids = (List<Object>) data.get("ids");  //攻略分類的ids集合
        List<StrategyCatalog> mapList = new ArrayList<>(); //攻略分類對象
        for(int i = 0;i < names.size(); i++){
    
            StrategyCatalog sc = new StrategyCatalog();
            String name = names.get(i).toString();
            String id = ids.get(i).toString();
            sc.setId(id);
            sc.setName(name);
            mapList.add(sc);
        }
        vo.setMapList(mapList);
        list.add(vo);
    }
    return list;
}

controller方法

//分類下拉框 catalogs
model.addAttribute("catalogs",catalogService.groupList());

3、富文本編輯 CKEditor

可以插入除了文本之外的編輯器 ,這裏使用 CKEditor

該插件默認傳遞的參數名固定 upload ,且固定了需要的 返回數據格式

前端:

<textarea id="content" name="content" class="ckeditor">${(strategy.content)!}</textarea>
//富文本框圖片配置
var ck = CKEDITOR.replace('content',{
    
    filebrowserBrowseUrl: '/圖片服務器,假裝這裏有',
    filebrowserUploadUrl: '/uploadImg_ck'   //請求路徑
});

後端控制器方法

@RequestMapping("/uploadImg_ck")
@ResponseBody
public Map<String, Object> upload(MultipartFile upload, String module){
    
    Map<String, Object> map = new HashMap<String, Object>();
    String imagePath= null;
    if(upload != null && upload.getSize() > 0){
    
        try {
    
            //圖片保存, 返回路徑
            imagePath =  UploadUtil.uploadAli(upload);
            //錶示保存成功
            map.put("uploaded", 1);
            map.put("url",imagePath);
        }catch (Exception e){
    
            e.printStackTrace();
            map.put("uploaded", 0);
            Map<String, Object> mm = new HashMap<String, Object>();
            mm.put("message",e.getMessage() );
            map.put("error", mm);
        }
    }
    return map;
}

4、攻略明細的保存

添加

因為頁面錶單提交的數據,明顯少於數據庫集合中需要的數據。所以需要在執行保存操作的時候,將缺少的數據補充進去

點贊數等不需要補充,因為添加操作時,點贊數等會有默認值0

但是需要單獨設置創建時間,因為編輯不需要創建時間

修改service實現類中的方法

//......

編輯

實現編輯回顯

if (StringUtils.hasLength(id)){
    
    //編輯回顯
    model.addAttribute("strategy",strategyService.get(id));
}

注意,因為編輯的時候,對象裏面會有點贊數等數據,需要保持不變

因為 MongoDB 是全量更新

@Override
public void saveOrUpdate(Strategy strategy) {
    
    StrategyCatalog catalog = catalogService.get(strategy.getCatalogId());
    StrategyTheme theme = themeService.get(strategy.getThemeId());
    //目的地id
    strategy.setDestId(catalog.getDestId());
    //目的地名稱
    strategy.setDestName(catalog.getDestName());
    //主題名
    strategy.setThemeName(theme.getName());
    //分類名
    strategy.setCatalogName(catalog.getName());
    //是否國外,目的地id,通過導航吐司獲取到國家名
    List<Destination> toasts = destinationService.getToasts(catalog.getDestId());
    if (toasts != null && toasts.size()>0 ) {
    
        Destination dest = toasts.get(0);
        strategy.setIsabroad("中國".equals(dest.getName()) ? Strategy.ABROAD_NO : Strategy.ABROAD_YES);
    }

    if (StringUtils.hasLength(strategy.getId())){
     //編輯
        //先查詢
        Strategy straMDB = this.get(strategy.getId());
        //替換
        strategy.setCreateTime(straMDB.getCreateTime());
        strategy.setViewnum(straMDB.getViewnum());
        strategy.setReplynum(straMDB.getReplynum());
        strategy.setThumbsupnum(straMDB.getThumbsupnum());
        strategy.setSharenum(straMDB.getSharenum());
        strategy.setFavornum(straMDB.getFavornum());
        //更新
        this.update(strategy);
    }else {
      // 添加
        //創建時間
        strategy.setCreateTime(new Date());
        this.save(strategy);
    }
}

實現 上架、下架 (修改狀態)

@Override
public void changeState(String id, Integer state) {
    
    //全量更新
    /*Strategy strategy = this.get(id); strategy.setState(hot); this.update(region);*/

    //部分字段更新 :db.集合名.updateMany({_id: "123"},{$set: {state: 1}})
    Query query = new Query();
    query.addCriteria( Criteria.where("_id").is(id));
    Update update = new Update();
    update.set("state",state);
    mongoTemplate.updateMulti(query, update, Strategy.class);
}

前端目的地明細

項目啟動,訪問 /views/destination/detail.html?id=目的地id 進入明細頁面

導航吐司

var param = getParams();
//吐司
ajaxGet("/destinations/toasts",{
    destId:param.id}, function (data) {
    
    var list = data.data;  //中國 廣東 廣州
    vue.dest = list.pop();  //數組的最後一個元素賦值給dest
    vue.toasts = list;
})
@GetMapping("/toasts")
public Object toasts(String destId){
    
    return JsonResult.success(destinationService.getToasts(destId));
}

攻略分類

此處分類的情况和前面 分類分組下拉框的 情况 一致

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-L5FCE21h-1651834690348)(C:\Users\ASUS\AppData\Roaming\Typora\typora-user-images\image-20220330134021740.png)]

此處可以使用 List<StrategyCatalog> 接收參數,就不需要自定義 VO 類了

//目的下所有攻略分類
ajaxGet("/destinations/catalogs",{
    destId:param.id}, function (data) {
    
    vue.catalogs = data.data;
})

控制器方法

@GetMapping("/catalogs")
public Object catalogs(String destId){
    
    return JsonResult.success(catalogService.queryCatalogByDestId(destId));
}

service實現類

@Autowired
private IStrategyService strategyService;
@Override
public List<StrategyCatalog> queryCatalogByDestId(String destId) {
    
    //查找目的地下的攻略分類集合
    List<StrategyCatalog> catalogs = strategyCatalogRepository.findByDestId(destId);
    for (StrategyCatalog catalog : catalogs) {
    
        //遍曆每個攻略分類,查詢該分類下所有攻略集合
        List<Strategy> strategys = strategyService.queryByCatalogId(catalog.getId());
        catalog.setStrategies(strategys);
    }
    return catalogs;
}

repository接口

//攻略
@Repository
public interface StrategyRepository extends MongoRepository<Strategy, String> {
    
    /** * 通過分類id查找攻略集合 * @param id * @return */
    List<Strategy> findByCatalogId(String id);
}
//攻略分類
@Repository
public interface StrategyCatalogRepository extends MongoRepository<StrategyCatalog, String> {
    
    /** * 通過目的地id查找分類集合 * @param destId * @return */
    List<StrategyCatalog> findByDestId(String destId);

}

攻略明細

注意:左邊是攻略的分類, 右邊是分類下所有的攻略

右邊上部分顯示所有當前分類下的所有攻略標題,下部分顯示第一篇攻略的內容

使用上面兩個後端控制器方法:導航吐司、攻略分類

var param = getParams();
//吐司
ajaxGet("/destinations/toasts",{
    destId:param.destId}, function (data) {
    
    var list = data.data;
    vue.dest = list.pop();
    vue.toasts = list;
})
//概况
ajaxGet("/destinations/catalogs",{
    destId:param.destId}, function (data) {
    
    //[{攻略分類1}, {攻略分類2},{攻略分類3}]
    vue.catalogs = data.data;
    $.each(vue.catalogs, function(index, item){
    
        if(item.id == param.catalogId){
    
            vue.catalog = item;  //選中的攻略分類
            vue.strategy = item.strategies[0]
            //攻略分類下所有攻略文章第一篇, 需要在頁面顯示文章內容
        }
    })
})

目的地下攻略(點擊量前3)

查詢當前目的下所有的關聯攻略, 顯示點擊量最高3篇(這裏可以自定義,比如:回複數最高)

//點擊量前3的攻略文章
ajaxGet("/destinations/strategies/viewnumTop3",{
    destId:param.id}, function (data) {
    
    vue.strategies = data.data;
})

後端控制器方法

@GetMapping("/strategies/viewnumTop3")
public Object strategiesViewnumTop3(String destId){
    
    return JsonResult.success(strategyService.queryViewnumTop3ByDestId(destId));
}

service實現類

@Override
public List<Strategy> queryViewnumTop3ByDestId(String destId) {
    
    Pageable pageable = PageRequest.of(0, 3, Sort.Direction.DESC, "viewnum");
    return strategyRepository.findByDestId(destId, pageable);
}

repository接口

接口中可以加入分頁條件,其他操作交給 JPA

/** * 通過目的地id查找點擊量前三的攻略 * @param destId 目的地id * @param pageable 點擊量前三的條件 * @return */
List<Strategy> findByDestId(String destId, Pageable pageable);

查看詳情

點擊查看詳情,進入攻略明細顯示頁面 views/strategy/detail.html?id=攻略id

//攻略明細
ajaxGet("/strategies/detail",{
    id:param.id}, function (data) {
    
    vue.strategy = data.data;
})
@RestController
@RequestMapping("strategies")
public class StrategyController {
    
    @Autowired
    private IStrategyService strategyService;

    @GetMapping("/detail")
    public Object detail(String id){
    
        return JsonResult.success(strategyService.get(id));
    }
}

查看全部

點擊查看全部,進入攻略列錶頁面: /views/strategy/list.html?destId=目的地id

//攻略分頁
ajaxGet("/strategies/query",{
    destId:param.destId}, function (data) {
    
    vue.page =data.data;
    //構建分頁條
    buildPage(vue.page.number, vue.page.totalPages,vue.doPage);
})
doPage:function(page){
    
    var param = getParams();
    ajaxGet("/strategies/query",
            {
    destId:param.destId, currentPage:page}, function (data) {
    
        vue.page = data.data;
        buildPage(vue.page.number, vue.page.totalPages,vue.doPage);
    })
}

因為涉及到分頁,所以接收參數一般都是用 QueryObject

@Setter
@Getter
public class StrategyQuery extends QueryObject {
    
    private String destId;
}

後端控制器方法

@GetMapping("/query")
public Object query(StrategyQuery qo){
    
    return JsonResult.success(strategyService.query(qo));
}

修改service方法

@Override
public Page<Strategy> query(StrategyQuery qo) {
    
    // 創建查詢對象:理解為MQL語句拼接對象
    Query query = new Query();
    
    if (StringUtils.hasLength(qo.getDestId())){
    
        query.addCriteria(Criteria.where("destId").is(qo.getDestId()));
    }
    //分頁參數對象pageable
    Pageable pageable = PageRequest.of(qo.getCurrentPage()-1, qo.getPageSize(),
                                       Sort.Direction.ASC, "id");
    //需要三個參數:List<T> content, Pageable pageable, long total
    return DBHelper.query(Strategy.class,mongoTemplate,query,pageable,qo);
}

前端攻略首頁

與目的地下攻略列錶一致, 唯一區別是少了目的地id這個查詢條件

訪問 /views/strategy/list.html 進入

羅列所有的主題:

//攻略主題
ajaxGet("/strategies/themes",{
    }, function (data) {
    
    vue.themes = data.data;
})
@GetMapping("/themes")
public Object themes(){
    
    return JsonResult.success(themeService.list());
}

點擊單個主題,展示攻略

前端

//攻略分頁列錶
ajaxGet("/strategies/query",{
    }, function (data) {
    
    vue.page = data.data;
    //分頁條
    buildPage(vue.page.number, vue.page.totalPages,vue.doPage);  //vue.doPage
})
doPage:function(page){
    
    var themeId = $("._j_tag.on").data("tid");
    ajaxGet("/strategies/query",{
    themeId:themeId, currentPage:page}, function (data) {
    
        vue.page = data.data;
        //vue.page.number = page;
        buildPage(vue.page.number, vue.page.totalPages,vue.doPage);
    })
}

後端修改 service方法

@Setter
@Getter
public class StrategyQuery extends QueryObject {
    
    private String destId;
    private String themeId;
}
if (StringUtils.hasLength(qo.getThemeId())){
    
    query.addCriteria(Criteria.where("themeId").is(qo.getThemeId()));
}

7、旅遊日記

後端

錶設計

遊記 (travel) 錶設計 :

屬性名稱屬性類型屬性說明
IdString主鍵
destidString目的地id
destNameString目的地名
userIdString作者id
titleString遊記標題
coverUrlString遊記封面
travelTimeDate旅遊時間
perExpendString人均消費
dayInt出行天數
personint和誰旅遊
createTimeDate創建時間
releaseTimeDate發布時間
lastUpdateTimeData最後更新時間
isPublicboolean是否公開
viewnumInt閱讀人數
replynumInt回複人數
favernumint收藏人數
sharenumInt分享人數
thumbsupnumint點贊人數
stateInt狀態
contentString內容

CRUD准備

domain

QueryObject

repository

service

trip-mgrsite 模塊的 controller

遊記審核

注意審核的步驟:

  1. 判斷是否滿足審核條件
  2. 審核通過之後做什麼
  3. 審核拒絕之後做什麼

頁面:

//發布/拒絕
$(".updateStateBtn").click(function () {
    
    var id = $(this).data('id');
    var state = $(this).data('state');
    $.get('/travel/changeState',{
    id:id,state:state},function (data) {
    
        if(data.code == 200){
    
            window.location.reload();
        }else{
    
            $.messager.alert("溫馨提示", "操作失敗");
        }
    })
})

後端:

@RequestMapping("/changeState")
@ResponseBody
public Object changeState(String id, Integer state){
    
    travelService.changeState(id, state);
    return JsonResult.success();
}
@Override
public void changeState(String id, Integer state) {
    
    //判斷是否滿足審核條件
    Travel travel = this.get(id);
    if (travel == null) {
    
        throw new LogicException("參數异常");
    }
    if (state == Travel.STATE_RELEASE){
     //審核通過之後
        //遊記狀態改為審核通過
        travel.setState(state);
        //遊記發布時間為當前時間
        travel.setReleaseTime(new Date());
        //記錄最後更新時間
        travel.setLastUpdateTime(new Date());
        //記錄審核信息(審核人、審核備注、時間等等)
    }else if(state == Travel.STATE_REJECT){
     //審核拒絕之後
        //遊記狀態改為審核拒絕
        travel.setState(state);
        //遊記發布時間改為 null
        travel.setReleaseTime(null);
        //記錄最後更新時間
        travel.setLastUpdateTime(new Date());
        //記錄審核信息(審核人、審核備注、時間等等)
    }else{
    
        //下架遊記
        //遊記狀態改為下架
        travel.setState(state);
        //遊記發布時間改為 null
        travel.setReleaseTime(null);
        //記錄最後更新時間
        travel.setLastUpdateTime(new Date());
        //記錄審核信息(審核人、審核備注、時間等等)
        //考慮要不要清空點贊、評論等數據...
    }
    this.update(travel);  //這裏更新不會丟失數據,因為是先查、再改、再更新的
}

遊記查看

頁面:

//查看文章
$(".lookBtn").click(function () {
    
    var id = $(this).data('id');
    //根據id查詢遊記內容
    $.get('/travel/getContentById',{
    id:id},function (data) {
    
        $("#inputModal .modal-body").html(data.data);
        $("#inputModal").modal('show');
    })
})

後端:

@RequestMapping("/getContentById")
@ResponseBody
public Object getContentById(String id){
    
    return JsonResult.success(travelService.get(id).getContent());
}

前端目的地明細下遊記

分頁顯示

頁面:

//遊記分頁
ajaxGet("/travels/query",{
    destId:param.id}, function (data) {
    
    vue.page = data.data;
    buildPage(vue.page.number, vue.page.totalPages, vue.doPage)
})

後端:

注意,遊記分頁需要顯示作者信息,數據庫有 userId ,而 domain 裏是 userInfo 對象

所以需要在分頁之前,給每個遊記對象的作者屬性進行賦值

@Setter
@Getter
public class TravelQuery extends QueryObject {
    
    private String destId;
}
@Override
public Page<Travel> query(TravelQuery qo) {
    
    // 創建查詢對象:理解為MQL語句拼接對象
    Query query = new Query();
    
    if (StringUtils.hasLength(qo.getDestId())){
    
        query.addCriteria(Criteria.where("destId").is(qo.getDestId()));
    }
    
    Pageable pageable = PageRequest.of(qo.getCurrentPage()-1, qo.getPageSize(),
                                       Sort.Direction.ASC, "id");
    Page<Travel> page = DBHelper.query(Travel.class,mongoTemplate,query,pageable,qo);
    
    for (Travel travel : page) {
    
        travel.setAuthor(userInfoService.get(travel.getUserId()));
    }
    
    return page;
}
@GetMapping("/query")
public Object query(TravelQuery qo){
    
    return JsonResult.success(travelService.query(qo));
}

帶範圍條件查詢

頁面傳遞過來的是單個 value 值:

分析

將一個值轉換為另外的值:映射思想(map)

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-IoEoEVcO-1651834690349)(C:\Users\ASUS\AppData\Roaming\Typora\typora-user-images\image-20220331103649238.png)]

代碼

新增遊記查詢條件,並且需要在 靜態代碼塊中,初始化查詢條件

/** * 遊記條件 */
@Setter
@Getter
public class TravelCondition {
    
    public static final Map<Integer, TravelCondition> TRAVEL_DAYS; //旅遊天數
    public static final Map<Integer, TravelCondition> TRAVEL_PRE_EXPENDS; //旅遊人均消費
    static{
    
        //旅遊天數
        TRAVEL_DAYS = new HashMap<>();
        TRAVEL_DAYS.put(-1, new TravelCondition(-1, Integer.MAX_VALUE));
        TRAVEL_DAYS.put(1, new TravelCondition(0, 3));
        TRAVEL_DAYS.put(2, new TravelCondition(4, 7));
        TRAVEL_DAYS.put(3, new TravelCondition(8, 14));
        TRAVEL_DAYS.put(4, new TravelCondition(15,Integer.MAX_VALUE));
        //人均消費
        TRAVEL_PRE_EXPENDS = new HashMap<>();
        TRAVEL_PRE_EXPENDS.put(-1, new TravelCondition(-1, Integer.MAX_VALUE));
        TRAVEL_PRE_EXPENDS.put(1, new TravelCondition(1, 999));
        TRAVEL_PRE_EXPENDS.put(2, new TravelCondition(1000, 6000));
        TRAVEL_PRE_EXPENDS.put(3, new TravelCondition(6001, 200000));
        TRAVEL_PRE_EXPENDS.put(4, new TravelCondition(200001,Integer.MAX_VALUE));
    }
    private int min;
    private int max;
    private TravelCondition(int min, int max){
    
        this.min = min;
        this.max = max;
    }
}

頁面傳遞的參數,使用 QueryObject 接收,所以還需要修改遊記的頁面查詢參數對象

/** * 遊記查詢對象 */
@Setter
@Getter
public class TravelQuery extends QueryObject {
    
    public static final Integer ORDER_NEW = 1;
    public static final Integer ORDER_HOT = 2;
    
    private String destId;
    private  int state = -1; //遊記狀態
    private int orderType = -1; //排序類型
    private int dayType = -1;   //旅遊天數類型
    private TravelCondition days;
    private int perExpendType = -1;  //人均消費類型
    private TravelCondition perExpends;

    //頁面傳遞 dayType=2 時,TRAVEL_DAYS.get(2),得到旅遊天數 4,7
    public TravelCondition getDays(){
    
        return TravelCondition.TRAVEL_DAYS.get(dayType);
    }
    public TravelCondition getPerExpends(){
    
        return TravelCondition.TRAVEL_PRE_EXPENDS.get(perExpendType);
    }
}

同時還要修改 query 方法

@Override
public Page<Travel> query(TravelQuery qo) {
    
    // 創建查詢對象:理解為MQL語句拼接對象
    Query query = new Query();
    if (StringUtils.hasLength(qo.getDestId())){
    
        query.addCriteria(Criteria.where("destId").is(qo.getDestId()));
    }

    //查詢天數 TravelCondition.TRAVEL_DAYS.get(qo.getDayType())
    TravelCondition days = qo.getDays();
    if (days != null) {
    
        query.addCriteria(Criteria.where("day")
                          .gte(days.getMin()).lte(days.getMax()));
    }

    //人均消費 TravelCondition.TRAVEL_PRE_EXPENDS.get(qo.getPerExpendType())
    TravelCondition per = qo.getPerExpends();
    if (per != null) {
    
        query.addCriteria(Criteria.where("perExpend")
                          .gte(per.getMin()).lte(per.getMax()));
    }

    //最新、最熱排序:默認根據id昇序
    Pageable pageable = PageRequest.of(qo.getCurrentPage()-1, qo.getPageSize(),
                                       Sort.Direction.ASC, "_id");
    if(qo.getOrderType() == TravelQuery.ORDER_HOT){
    
        pageable = PageRequest.of(qo.getCurrentPage()-1, qo.getPageSize(),
                                  Sort.Direction.DESC, "viewnum");
    }else if (qo.getOrderType() == TravelQuery.ORDER_NEW){
    
        pageable = PageRequest.of(qo.getCurrentPage()-1, qo.getPageSize(),
                                  Sort.Direction.DESC, "createTime");
    }

    Page<Travel> page = DBHelper.query(Travel.class,mongoTemplate,query,pageable,qo);
    //給查詢出的每個遊記對象的作者屬性賦值
    for (Travel travel : page) {
    
        travel.setAuthor(userInfoService.get(travel.getUserId()));
    }
    return page;
}

前端遊記首頁

因為遊記首頁也是使用分頁查詢,且查詢條件與上面一致,所以直接調用後端控制器方法,無需改動

前端遊記首頁添加遊記

要求:

  1. 封面圖片上傳(這裏簡單起見沒有使用插件, 可以大家可以自己實現)
  2. 富文本框, 這裏選用的是umeditor, 迷你百度富文本編輯器

需要注意: 遊記具有4種狀態

草稿狀態:用戶添加遊記完遊記,在不點擊發布操作時,遊記為草稿狀態(默認)

待發布狀態:用戶寫完遊記,點擊發布操作時,遊記為待發布狀態,需要管理員審核

發布拒絕狀態:如果遊記不合法,管理員直接拒絕,此時遊記為發布拒絕狀態 注意:草稿狀態下進行遊記編輯,但不發布,遊記依然為草稿狀態, 如果遊記已經發錶,再進行編輯,此時遊記回 到草稿狀態或待發布狀態。

已删除狀態:用戶自己手動删除的遊記,記錄狀態為已删除狀態

編輯添加成功後要同步修改elasticsearch中遊記

上傳封面

頁面:此處沒有使用插件

function uploadPic () {
    
    var url = domainUrl +"/coverImageUpload";
    if($("#coverBtn").val()){
    
        $("#coverForm").ajaxSubmit({
    
            url:url,
            type:"post",
            success:function (data) {
    
                $("#choseBtn").html(" + 選擇封面");
                $("#coverImage").attr("src", data);
                $("#coverValue").val(data);
            }
        })
    }
}
<form method="post" id="coverForm" enctype="multipart/form-data">
    <input type="file" name="pic" id="coverBtn" style="display: none;" onchange="uploadPic()">
</form>

後端:

@Controller
public class UploadController {
    
    @Autowired
    private IStrategyService strategyService;

    @RequestMapping("/coverImageUpload")
    @ResponseBody
    public String coverImageUpload(MultipartFile pic) throws Exception {
    
        //執行文件保存,使用阿裏OSS
        //String path = UploadUtil.uploadAli(pic);
        return "https://img0.baidu.com/it/u=2659712525,3315272485&fm=253&fmt=auto&app=138&f=PNG?w=220&h=137";
    }
}

富文本編輯 UEditor

UEditor 這個插件比較舊,使用的是 JSP,所以插入圖片的操作也是 基於 JSP ,需要將其轉換為 SpringBoot 支持的方式

  1. 貼入工具類

    [外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-C6jfO8Zd-1651834690350)(C:\Users\ASUS\AppData\Roaming\Typora\typora-user-images\image-20220331121949402.png)]

  2. 貼入上傳圖片的方法

    [外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-mjFLC3iT-1651834690350)(C:\Users\ASUS\AppData\Roaming\Typora\typora-user-images\image-20220331122018713.png)]

目的地下拉框顯示: 編輯頁面需要選擇目的地,所以需要在下拉框回顯目的地數據

//查詢目的地
ajaxGet("/destinations/list",{
    },function (data) {
    
    vue.dests = data.data;
})
<select name="destId" data-placeholder="請選擇目的地" id="region" style="width: 150px;">
    <option v-for="d in dests" :value="d.id" :selected="tv.destId == d.id">
        {
   {d.name}}</option>
</select>
@GetMapping("/list")
public Object list(){
    
    return JsonResult.success(destinationService.list());
}

編輯回顯:

//數據回顯
ajaxGet("/travels/detail",{
    id:id},function (data) {
    
    var travel = data.data;
    if(travel){
     //travel,當id有值時需要回顯
        vue.tv = travel;
        ue.setContent( travel.content);
    }
})
//編輯回顯
@GetMapping("/detail")
public Object detail(String id){
    
    if (StringUtils.hasLength(id)){
    
        Travel travel = travelService.get(id);
        return JsonResult.success(travel);
    }
    return JsonResult.noLogin();
}

保存

對於 MongoDB 保存更新的時候,需要注意,前端頁面填入的數據和數據庫的列數是否能對等,因為 MongoDB 是全量更新。稍有不慎就會丟失數據,或者插入數據為空

注意,因為保存遊記需要保存用戶id,而用戶id獲取的來源就是當前登錄用戶

要獲取到當前登錄用戶,就必須要先登錄,所以此功能是必須登錄之後才可以運行的,就需要使用注解

後端:

//添加/編輯 保存
@RequireLogin
@PostMapping("/saveOrUpdate")
public Object saveOrUpdate(Travel travel, HttpServletRequest request){
    
    String token = request.getHeader("token");
    UserInfo user = userInfoRedisService.getUserByToken(token);
    travel.setUserId(user.getId());
    travelService.saveOrUpdate(travel);
    return JsonResult.success(travel.getId());
}
@Override
public void saveOrUpdate(Travel travel) {
    
    //目的地名稱
    travel.setDestName(destinationService.get(travel.getDestId()).getName());
    //用戶id,需要獲取當前登錄用戶的id
    //創建時間
    travel.setCreateTime(new Date());
    //最後更新時間
    travel.setLastUpdateTime(new Date());

    if (StringUtils.hasLength(travel.getId())){
     //編輯
        this.update(travel);
    }else {
      //注意save方法裏travel.setId(null),將id設置null,否則無法將自動生成的id注入進去
        this.save(travel);
    }
}
@Override
public void save(Travel travel) {
    
    travel.setId(null);  //如果id為"",spring-data-mongodb 不會將自動生成的id注入
    repository.save(travel);
}

頁面:

saveOrUpdate:function (state) {
    
    $("#state").val(state);
    var param = $("#editForm").serialize() ;
    ajaxPost("/travels/saveOrUpdate", param, function (data) {
    
        //還缺一參數:目的地id
        var destId = $("#region").val();
        window.location.href = "/views/travel/detail.html?id=" + data.data + "&destId=" + destId;
    })
}

前端遊記明細

從遊記列錶,點擊某個遊記進入遊記明細頁面 “/views/travel/detail.html?id=遊記id”

展示遊記

//遊記
ajaxGet("/travels/detail",{
    id:param.id}, function (data) {
    
    vue.detail = data.data;
})
@GetMapping("/detail")
public Object detail(String id){
    
    if (StringUtils.hasLength(id)){
    
        return JsonResult.success(travelService.get(id));
    }
    return null;
}

此時運行之後,頁面報錯:

報這個錯誤是因為頁面要顯示用戶信息,但是在 travel 對象屬性 user 並沒有數據,因為數據庫只有 userId 字段

所以需要手動賦值一下

@Override
public Travel get(String id) {
    
    Optional<Travel> opt = repository.findById(id);
    if (opt.isPresent()){
    
        Travel travel = opt.get();
        travel.setAuthor(userInfoService.get(travel.getUserId()));
        //dest屬性也是需要賦值
        travel.setDest(destinationService.get(travel.getDestId()));
        return travel;
    }
    return null;
}

關聯目的地

查詢當前篇遊記的目的地, 關聯查詢目的地的父目的地(導航吐司), 顯示當前目的地的封面與名稱

吐司:

//吐司
ajaxGet("/destinations/toasts",{
    destId:param.destId}, function (data) {
    
    vue.toasts =data.data
})
@GetMapping("/toasts")
public Object toasts(String destId){
    
    return JsonResult.success(destinationService.getToasts(destId));
}

顯示攻略前3

根據當前篇遊記的目的地查詢該目的地下閱讀量為前3的攻略,循環播放

//點擊量前3的攻略文章
ajaxGet("/destinations/strategies/viewnumTop3",{
    destId:param.destId} function(data) {
    
    vue.strategies = data.data;
})
@GetMapping("/strategies/viewnumTop3")
public Object strategiesViewnumTop3(String destId){
    
    return JsonResult.success(strategyService.queryViewnumTop3ByDestId(destId));
}

顯示遊記前3

根據當前篇遊記的目的地查詢該目的地下閱讀量為前3的遊記,循環播放

//點擊量前3的遊記文章
ajaxGet("/destinations/travels/viewnumTop3",{
    destId:param.destId}, function (data) {
    
    vue.travels = data.data;
})

後端:

@GetMapping("/travels/viewnumTop3")
public Object travelsViewnumTop3(String destId){
    
    return JsonResult.success(travelService.queryViewnumTop3ByDestId(destId));
}
@Override
public List<Travel> queryViewnumTop3ByDestId(String destId) {
    
    Pageable pageable = PageRequest.of(0,3, Sort.Direction.DESC, "destId");
    return repository.findByDestId(destId, pageable);
}
/** * 通過目的地id查找點擊量前三的遊記 * @param destId * @param pageable * @return */
List<Travel> findByDestId(String destId, Pageable pageable);

用戶對象注入

存在問題:

後端獲取當前登錄用戶信息,都需要進行以下操作:屬於重複

String token = request.getHeader("token");
UserInfo user = userInfoRedisService.getUserByToken(token);

需求:使用簡化的方式獲取當前登錄用戶信息

直接在請求映射方法中聲明 UserInfo 這個類型的形參,SpringMVC 就自動將當前登錄用戶對象注入

例如:

@GetMapping("/info")
public Object info(UserInfo userInfo){
    
    return JsonResult.success(userInfo);
}

怎麼做到的? 自定義SpringMVC參數解析器

SpringMVC 所有映射方法的形參的值的注入都是靠 SpringMVC 自帶的參數解析器實現的

現在想要實現將登錄的用戶自動注入,單靠 SpringMVC 是不行的,還需要自定義參數解析器

操作原理

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-kliNYKKz-1651834690351)(C:\Users\ASUS\AppData\Roaming\Typora\typora-user-images\image-20220402210911408.png)]

自定義參數解析器

自定義參數解析器:

/** * 用戶參數解析器, * 將請求映射方法中的 UserInfo 類型的形參解析成當前登錄用戶對象,並注入 */
public class UserInfoArgumentResolver implements HandlerMethodArgumentResolver {
    
    @Autowired
    private IUserInfoRedisService userInfoRedisService;

    // 指定當前解析器能解析的形參類型
    @Override
    public boolean supportsParameter(MethodParameter parameter) {
    
        return parameter.getParameterType() == UserInfo.class;
    }

    /** * 當上面方法 supportsParameter() 返回true時,才執行此方法 * 作用:獲取當前登錄用戶信息,並注入 UserInfo 的形參中 */
    @Override
    public Object resolveArgument(MethodParameter parameter,
                                  ModelAndViewContainer mavContainer,
                                  NativeWebRequest webRequest,
                                 WebDataBinderFactory binderFactory)throws Exception{
    
        //獲取request對象
        HttpServletRequest request
            = webRequest.getNativeRequest(HttpServletRequest.class);
        String token = request.getHeader("token");
        UserInfo user = userInfoRedisService.getUserByToken(token);
        return user;
    }
}

啟動類配置此解析器

@SpringBootApplication
public class WebSite implements WebMvcConfigurer{
    
    //用戶參數解析器
    @Bean
    public UserInfoArgumentResolver userInfoArgumentResolver(){
    
        return new UserInfoArgumentResolver();
    }

    @Override
    public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
    
        resolvers.add(userInfoArgumentResolver());
    }
    
    //......
}

自定義注解區分使用

現在還有一個問題,就是當用戶更改信息之後,怎麼實現用戶的更新操作,因為更新需要傳入舊的用戶,然後更新之後再保存新的

但是現在形參部分,被解析器注入的都是當前用戶

使用注解解决

/** * 定義 userInfo 參數注入注解 * 只有使用了這個注解的參數,自定義參數解析器才執行解析邏輯 */
@Target({
    ElementType.PARAMETER})  //是用在參數比特置
@Retention(RetentionPolicy.RUNTIME)
public @interface UserParam {
    
}

修改自定義參數解析器使用的條件

public class UserInfoArgumentResolver implements HandlerMethodArgumentResolver {
    

    // 指定當前解析器能解析的形參類型
    @Override
    public boolean supportsParameter(MethodParameter parameter) {
    
        return 
            parameter.getParameterType() == UserInfo.class 
            &&
            parameter.hasParameterAnnotation(UserParam.class);
    }
    
    //......
}

使用:

@GetMapping("/info")
public Object info(@UserParam UserInfo userInfo){
      //自動注入當前登錄用戶
    return JsonResult.success(userInfo);
}
/* 如果要進行用戶編輯,映射方法接收參數也是 UserInfo ,怎麼區分 定義一個參數級別的注解,當參數貼有這個注解,自定義參數解析器才進行解析邏輯 */
@GetMapping("/updateInfo")
public Object updateInfo(UserInfo userInfo){
      //需要在請求的時候傳入參數,不是當前登錄用戶
    return JsonResult.success(userInfo);
}

8、內容評論

評論分為 蓋樓式微信評論式

分析

對於評論,需要記錄用戶信息,以及點贊信息

對於點贊的用戶,能看到點贊的特效,以及增加後的點贊數

換成其他用戶之後,沒有點贊的特效,但是可以看到增加後的點贊數

如何區分不同用戶有不同的點贊特效?將點了贊的用戶與評論綁定。點過贊的用戶會留下印記給此評論

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-FnsIPOBq-1651834690351)(C:\Users\ASUS\AppData\Roaming\Typora\typora-user-images\image-20220401102336173.png)]

前端攻略評論

CRUD准備

對於有關於用戶的信息,沒有使用 UserInfo 對象進行封裝起來

因為使用對象封裝的,用戶更新的時候,因為對象是聯合查詢查出來的,更新較快

使用以下這種更新可能較慢,因為評論中主要關注點不在於用戶頭像的等信息

/** * 攻略評論 */
@Setter
@Getter
@Document("strategy_comment")
@ToString
public class StrategyComment extends BaseDomain {
    
    private String strategyId;  //攻略(明細)id
    private String strategyTitle; //攻略標題
    private String userId;    //用戶id
    private String nickname;  //用戶名
    private String city;
    private int level;
    private String headImgUrl;     //頭像
    private Date createTime;    //創建時間
    private String content;      //評論內容
    private int thumbupnum;     //點贊數
    private List<String> thumbuplist = new ArrayList<>();
}

QueryObject

repository

service

/** * 攻略評論服務接口 */
public interface StrategyCommentService {
    
    //單個查詢
    StrategyComment get(String id);
    //添加
    void save(StrategyComment strategyComment);
    //分頁查詢
    Page<StrategyComment> query(StrategyCommentQuery qo);
}

trip-website-api 模塊的 controller

添加評論

頁面:

addComment:function(){
     //添加評論
    var param = {
    }
    param.strategyId = vue.strategy.id;
    param.strategyTitle = vue.strategy.title;

    var content = $("#content").val();
    if(!content){
    
        popup("評論內容必填");
        return;
    }
    param.content = content;
    $("#content").val('');

    ajaxPost("/strategies/addComment",param, function (data) {
    
        vue.queryStatisVo(param.strategyId);
        vue.commentPage(1,param.strategyId);
    })
}

因為前端只傳遞了攻略、評論內容的信息,所以需要獲取當前登錄用戶信息

所以需要登陸了之後才可以進行此操作,就需要 @RequireLogin 注解

@RequireLogin
@PostMapping("/addComment")
public Object addComment(StrategyComment comment, @UserParam UserInfo user){
    
    //設置用戶信息
    /* bean屬性賦值 org.springframework.beans.BeanUtils 前提:屬性名一致時可以進行賦值 注意:comment裏有id屬性,user裏也有id屬性,也會進行賦值,所以save方法裏面要setId(null) 操作原理:JavaBean的內省機制 */
    BeanUtils.copyProperties(user, comment);
    comment.setUserId(user.getId()); //屬性名不一致的還需要手動賦值

    strategyCommentService.save(comment);
    return JsonResult.success();
}

注意再添加評論時,需要添加創建時間

@Override
public void save(StrategyComment strategyComment) {
    
    strategyComment.setId(null);  //如果id為"",spring-data-mongodb 不會將自動生成的id注入
    strategyComment.setCreateTime(new Date());
    repository.save(strategyComment);
}

顯示評論

頁面:

commentPage:function (page,strategyId) {
    //分頁
    strategyId = strategyId || vue.strategy.id;
    ajaxGet("/strategies/comments", {
    currentPage:page, strategyId:strategyId}, function(data){
    
        vue.page = data.data;
        buildPage(vue.page.number, vue.page.totalPages,vue.commentPage);
    })
}

後端:

因為是分頁操作,所以使用 QueryObject 類對象作為參數接收

@Setter
@Getter
public class StrategyCommentQuery extends QueryObject {
    
    private String strategyId;
}
@Override
public Page<StrategyComment> query(StrategyCommentQuery qo) {
    
    Query query = new Query();
    if (StringUtils.hasLength(qo.getStrategyId())){
    
        query.addCriteria(Criteria.where("strategyId").is(qo.getStrategyId()));
    }
    Pageable pageable = PageRequest.of(qo.getCurrentPage()-1, qo.getPageSize(), Sort.Direction.DESC, "createTime");
    return DBHelper.query(StrategyComment.class, mongoTemplate, query, pageable, qo);
}
@GetMapping("/comments")
public Object comments(StrategyCommentQuery qo){
    
    return JsonResult.success(strategyCommentService.query(qo));
}

點贊操作

頁面:

頁面實現點贊變紅,取消點贊變白

<span class="_j_comment_like_num">{
   {c.thumbupnum}}</span>&nbsp;
<a href="javascript:;" class="btn-comment-like _j_like_comment_btn " :class="c.thumbuplist.indexOf(user== undefined?-1:user.id) == -1? 'like':'liked'" @click="commentThumb(c.id)">
</a>
commentThumb:function(commentId){
    
    var page = $("#pagination").find("a.active").html()||1;
    ajaxPost("/strategies/commentThumb",{
    cid:commentId,sid:getParams().id}, function (data) {
    
        vue.commentPage(page,getParams().id);  //getParams() 獲取url上的請求參數
    })
}

代碼:

//評論點贊
@RequireLogin
@GetMapping("/commentThumb")
public Object commentThumb(String cid, @UserParam UserInfo user){
    
    strategyCommentService.commentThumb(cid, user.getId());
    return JsonResult.success();
}
@Override
public void commentThumb(String cid, String uid) {
    
    //獲取點贊 id 集合
    StrategyComment comment = this.get(cid);
    List<String> userIdList = comment.getThumbuplist();
    //判斷傳入的用戶id是否在集合中
    if (userIdList.contains(uid)){
    
        //如果在,點贊數 -1 移出
        comment.setThumbupnum(comment.getThumbupnum()-1);
        userIdList.remove(uid);
    }else {
    
        //如果不在,點贊數 +1 添加
        comment.setThumbupnum(comment.getThumbupnum()+1);
        userIdList.add(uid);
    }
    /* 注意:這裏不需要在將集合注入comment對象 因為本身這個集合指向的就是comment對象裏面的屬性,地址值一致 所以修改的其實就是此對象的屬性 comment.setThumbuplist(userIdList); */

    //更新操作
    repository.save(comment);
}

分析

需要實現 評論的回複 ,會引用被回複評論的信息及內容(沒有點贊需求)

所以 評論對象屬性 裏面要包含 被回複評論的對象

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-2Lti0oYu-1651834690352)(C:\Users\ASUS\AppData\Roaming\Typora\typora-user-images\image-20220401220407723.png)]

前端遊記評論

CRUD准備

domain

/** * 遊記評論 */
@Setter
@Getter
@Document("travel_comment")
public class TravelComment extends BaseDomain {
    
    public static final int TRAVLE_COMMENT_TYPE_COMMENT = 0; //普通評論
    public static final int TRAVLE_COMMENT_TYPE = 1; //評論的評論
    private String travelId;  //遊記id
    private String travelTitle; //遊記標題
    private String userId;    //用戶id
    private String nickname; //用戶名
    private String city;
    private int level;
    private String headImgUrl;   // 用戶頭像
    private int type = TRAVLE_COMMENT_TYPE_COMMENT; //評論類別
    private Date createTime; //創建時間
    private String content;  //評論內容
    private TravelComment refComment;  //關聯的評論
}

QueryObject

repository

service

trip-website-api 模塊的 controller

添加評論

評論必須要先登錄

頁面:

commentAdd:function (e) {
    
    var content = $("#commentContent").val();
    if(!content){
     popup("評論不能為空"); return; }
    var param = {
    };
    param.travelId = vue.detail.id;
    param.travelTitle = vue.detail.title;
    param.content = emoji(content);
    param.type =$("#commentTpye").val();
    if(param.type == 1){
    
        param["refComment.id"] = $("#refCommentId").val();
    }else{
    
        param["refComment.id"] = "";
    }
    $("#commentTpye").val(0);
    $("#refCommentId").val("");
    ajaxPost("/travels/commentAdd",param, function (data) {
    
        $("#commentContent").val("");
        $("#commentContent").attr("placeholder","");
        vue.queryComments( param.travelId);
    })
}

後端:

@RequireLogin
@PostMapping("/commentAdd")
public Object commentAdd(TravelComment comment, @UserParam UserInfo user){
    
    //需要注入當前登錄用戶信息
    BeanUtils.copyProperties(user, comment);
    comment.setUserId(user.getId());
    //保存
    travelCommentService.save(comment);
    return JsonResult.success();
}

注意再添加評論時,需要添加創建時間、需要維護關聯的評論

隱藏輸入框值默認為 0,前端判斷:0 錶示沒有回複,就不會注入關聯評論的 id

點擊回複的時候,會設置隱藏輸入框值為 1,前端會判斷:1 錶示回複,就會將關聯的評論 id 注入

@Override
public void save(TravelComment travelComment) {
    
    travelComment.setId(null);  //如果id為"",spring-data-mongodb 不會將自動生成的id注入
    //維護關聯的評論
    String refId = travelComment.getRefComment().getId();
    if (StringUtils.hasLength(refId)) {
    
        //第二層評論
        TravelComment refComment = this.get(refId);
        travelComment.setRefComment(refComment);
        travelComment.setType(TravelComment.TRAVLE_COMMENT_TYPE); //評論的評論
    }else {
    
        travelComment.setType(TravelComment.TRAVLE_COMMENT_TYPE_COMMENT); //普通評論
    }
    //設置創建時間
    travelComment.setCreateTime(new Date());
    repository.save(travelComment);
}

顯示評論

注意,此處的評論不分頁

queryComments:function (travelId) {
    
    //遊記評論不分頁
    ajaxGet("/travels/comments",{
    travelId:travelId}, function (data) {
    
        vue.comments = data.data;
    })
}

後端:

@GetMapping("/comments")
public Object comments(String travelId){
    
    return JsonResult.success(travelCommentService.queryByTravelId(travelId));
}
@Override
public List<TravelComment> queryByTravelId(String travelId) {
    
    return repository.findByTravelId(travelId);
}

評論錶情

將指定中文文字轉換為錶情圖片

//將 (中文) 格式數據替換成標簽圖片:
function emoji(str) {
    
    //錶情圖片資源, 從馬蜂窩扣出來的, 可以百度:
    //1:HttpURLConnection 使用 jdk自帶的 代碼中如何發起http請求
    // RestTemplate 項目發短信用到
    //2:HttpClient 第三方http請求發送的框架
    //RestTemplate(client)
    //3:webmagic 專門用於爬蟲框架

    //例如:傳入字符 str=現金付款山東省看得(大笑小蜂)見風科技適(大笑小蜂)得府君書(得意小蜂)的開發决勝巔峰
    //匹配中文
    var reg = /\([\u4e00-\u9fa5A-Za-z]*\)/g;
    var matchArr = str.match(reg);  //此處得到所有匹配字符[(大笑小蜂), (大笑小蜂),(得意小蜂)]
    
    if(!matchArr){
    
        return str;
    }
    for(var i = 0; i < matchArr.length; i++){
      //遍曆匹配的字符集合,將字符替換為圖片錶情
        str = str.replace(matchArr[i],
                          '<img src="'+EMOJI_MAP[matchArr[i]]
                          +'"style="width: width:28px;"/>')
    }
    return str;
}

EMOJI_MAP 多個鍵值對,可以通過指定字符,找到指定錶情圖片

var EMOJI_MAP = {
    
    "(憤怒小蜂)": "/images/emoji/brands_v3/[email protected]",
    
    //......
}

回複評論操作

toComment:function(nickname, refId){
    
    $("#commentTpye").val(1);
    $("#refCommentId").val(refId);
    $("#commentContent").focus();
    $("#commentContent").attr("placeholder","回複:" + nickname );
}

最後調用的還是評論的 controller 方法

9、數據統計

分析

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-Fu7mIVaR-1651834690353)(C:\Users\ASUS\AppData\Roaming\Typora\typora-user-images\image-20220401220619214.png)]

攻略的數據統計

閱讀數分析

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-QBRj7NRL-1651834690354)(C:\Users\ASUS\AppData\Roaming\Typora\typora-user-images\image-20220401220639965.png)]

閱讀數+1

先創建統計的 vo 對象

因為和 redis 相關,所以放在 redis 的包內

/** * 攻略redis中統計數據 * 運用模塊: * 1:數據統計(回複,點贊,收藏,分享,查看) */
@Getter
@Setter
public class StrategyStatisVO implements Serializable {
    
    private String strategyId;  //攻略id

    private int viewnum;  //點擊數
    private int replynum;  //攻略評論數
    private int favornum; //收藏數
    private int sharenum; //分享數
    private int thumbsupnum; //點贊個數
}

因為從頁面加載了攻略之後就會 閱讀數 +1,所以是在訪問攻略的接口裏面進行操作

@Autowired
private IStrategyStatisVORedisService strategyStatisVORedisService;

@GetMapping("/detail")
public Object detail(String id){
    
    Strategy strategy = strategyService.get(id);
    //閱讀數 +1
    strategyStatisVORedisService.viewnumIncrease(id , 1);
    return JsonResult.success(strategy);
}

service

@Override
public void viewnumIncrease(String id, int num) {
    
    //拼接vo對象的key
    String key = RedisKeys.STRATEGY_STATIS_VO.join(id);
    //判斷key是否存在
    StrategyStatisVO vo = null;
    if (!template.hasKey(key)){
    
        //如果不存在,初始化 vo —— 需要從數據庫中查詢數據
        vo = new StrategyStatisVO();
        Strategy strategy = strategyService.get(id);
        BeanUtils.copyProperties(strategy, vo);
        vo.setStrategyId(strategy.getId());
    }else {
    
        //如果存在,獲取vo對象,注意返回的是JSON格式數據
        String voStr = template.opsForValue().get(key);
        vo = JSON.parseObject(voStr, StrategyStatisVO.class);
    }
    //更新 vo,直接 +num
    vo.setViewnum(vo.getViewnum() + num);
    template.opsForValue().set(key, JSON.toJSONString(vo));
}

因為使用 Redis,需要生成 key

/** * redis key管理 * 約定:一個枚舉實例就是一個 key */
@Getter
public enum  RedisKeys{
    
    //攻略統計對象:-1L 錶示不需要指定過期時間
    STRATEGY_STATIS_VO("strategy_statis_vo", -1L),
    //短信驗證碼
    VERIFY_CODE("verify_code", Consts.VERIFY_CODE_VAI_TIME * 60L),
    //登錄token
    LOGIN_TOKEN("user_login_token", Consts.USER_INFO_TOKEN_VAI_TIME * 60L);

    private String prefix;  //redis的key的前綴
    private Long time;  //redis的key的有效時間,-1L 錶示不需要指定過期時間,單比特 秒
    private RedisKeys(String prefix, Long time){
    
        this.prefix = prefix;
        this.time = time;
    }

    //拼接完整的redis的 key
    public String join(String ...keys){
    
        StringBuilder sb = new StringBuilder();
        sb.append(prefix);
        for (String key : keys) {
    
            sb.append(":").append(key);
        }
        return sb.toString();
    }
}

頁面顯示統計數

頁面:

queryStatisVo:function (sid) {
    
    //統計數據
    ajaxGet("/strategies/statisVo",{
    sid:sid}, function (data) {
    
        vue.vo =data.data;
    })
}

注意,因為异步請求,在初始化頁面數據的時候,不一定會根據代碼順序來初始化頁面數據

為了保證查詢統計數據的時候,不會出現空指針异常,所以建議將統計數據的函數執行放在,攻略明細函數裏面

如下

var _this = this;
//攻略明細
ajaxGet("/strategies/detail",{
    id:param.id}, function (data) {
    
    vue.strategy = data.data;
    //統計數據
    _this.queryStatisVo(param.id);
})

後端

@GetMapping("/statisVo")
public Object statisVo(String sid){
    
    return JsonResult.success(strategyStatisVORedisService.getStrategyStatisVo(sid));
}
@Override
public StrategyStatisVO getStrategyStatisVo(String sid) {
    
    //拼接vo對象的key
    String key = RedisKeys.STRATEGY_STATIS_VO.join(sid);
    String voStr = template.opsForValue().get(key);
    StrategyStatisVO vo = JSON.parseObject(voStr, StrategyStatisVO.class);
    return vo;
}

數據封裝優化一下:

@Override
public void viewnumIncrease(String id, int num) {
    
    StrategyStatisVO vo = this.getStrategyStatisVo(id);
    vo.setViewnum(vo.getViewnum() + num);
    this.setStrategyStatisVo(vo);
}

@Override
public StrategyStatisVO getStrategyStatisVo(String strategyId) {
    
    //拼接vo對象的key
    String key = RedisKeys.STRATEGY_STATIS_VO.join(strategyId);
    //判斷key是否存在
    StrategyStatisVO vo = null;
    if (!template.hasKey(key)){
    
        //如果不存在,初始化 vo —— 需要從數據庫中查詢數據
        vo = new StrategyStatisVO();
        Strategy strategy = strategyService.get(strategyId);
        BeanUtils.copyProperties(strategy, vo);
        vo.setStrategyId(strategy.getId());
        template.opsForValue().set(key, JSON.toJSONString(vo));
    }else {
    
        //如果存在,獲取vo對象,注意返回的是JSON格式數據
        String voStr = template.opsForValue().get(key);
        vo = JSON.parseObject(voStr, StrategyStatisVO.class);
    }
    return vo;
}

@Override
public void setStrategyStatisVo(StrategyStatisVO vo) {
    
    String key = RedisKeys.STRATEGY_STATIS_VO.join(vo.getStrategyId());
    template.opsForValue().set(key, JSON.toJSONString(vo));
}

評論數分析

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-8tJE74Py-1651834690354)(C:\Users\ASUS\AppData\Roaming\Typora\typora-user-images\image-20220402151016924.png)]

評論數+1

@Override
public void replynumIncrease(String strategyId, int num) {
    
    StrategyStatisVO vo = this.getStrategyStatisVo(strategyId);
    vo.setReplynum(vo.getReplynum() + num);
    this.setStrategyStatisVo(vo);
}
@RequireLogin
@PostMapping("/addComment")
public Object addComment(StrategyComment comment, @UserParam UserInfo user){
    
    //設置用戶信息
    /* bean屬性賦值 org.springframework.beans.BeanUtils 前提:屬性名一致時可以進行賦值 注意:comment裏面有id屬性,user裏面也有id屬性,也會進行賦值,所以save方法裏面要setId(null) 操作原理:JavaBean的內省機制 */
    BeanUtils.copyProperties(user, comment);
    comment.setUserId(user.getId()); //屬性名不一致的還需要手動賦值

    //評論數+1
    strategyStatisVORedisService.replynumIncrease(comment.getStrategyId(), 1);

    strategyCommentService.save(comment);
    return JsonResult.success();
}

收藏分析

因為需要顯示用戶收藏的內容,所以建議在用戶的角度,實現記號緩存

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-YbGtofp7-1651834690355)(C:\Users\ASUS\AppData\Roaming\Typora\typora-user-images\image-20220402151222519.png)]

收藏數+1

頁面:

favor:function(){
    
    ajaxPost("/strategies/favor",{
    sid:vue.strategy.id}, function (data) {
    
        if(data.data){
    
            popup("收藏成功");
        }else{
    
            popup("已取消收藏");
        }
        vue.queryStatisVo(vue.strategy.id);  //顯示統計數據
        if(user){
       //顯示用戶收藏攻略id集合
            vue.queryUserFavor(vue.strategy.id,user.id);
        }
    })
}

收藏接口:

//攻略收藏
@RequireLogin
@PostMapping("/favor")
public Object favor(String sid, @UserParam UserInfo user){
    
    //攻略收藏 : ret true 收藏成功, false錶示取消收藏
    boolean ret = strategyStatisVORedisService.favor(sid, user.getId());
    return JsonResult.success(ret);
}

設計key

//用戶攻略收藏
USER_STRATEGY_FAVOR("user_strategy_favor", -1L)
@Override
public boolean favor(String strategyId, String userId) {
    
    //獲取記號(攻略id集合)
    String signkey = RedisKeys.USER_STRATEGY_FAVOR.join(userId);
    //記號不存在:初始化
    List<String> sidList = new ArrayList<>();
    if (template.hasKey(signkey)){
    
        //記號存在:直接獲取
        String sidListStr = template.opsForValue().get(signkey);
        sidList = JSON.parseArray(sidListStr, String.class);
    }
    //通過記號判斷當前操作是收藏還是取消收藏
    StrategyStatisVO vo = this.getStrategyStatisVo(strategyId);
    if (sidList.contains(strategyId)){
    
        //取消收藏:獲取vo,收藏數-1,將strategyId移出記號集合中
        vo.setFavornum(vo.getFavornum() - 1);
        sidList.remove(strategyId);
    }else {
    
        //收藏:獲取vo,收藏數+1,將strategyId加入記號集合中
        vo.setFavornum(vo.getFavornum() + 1);
        sidList.add(strategyId);
    }
    //更新記號、更新vo對象
    template.opsForValue().set(signkey, JSON.toJSONString(sidList));
    this.setStrategyStatisVo(vo);
    return sidList.contains(strategyId);
}

收藏按鈕變色

頁面:

<a href="javascript:void(0);" title="收藏" class="bs_btn btn-collect" @click="favor">
    
    <!--判斷有無收藏的攻略id-->
    <i class="collect_icon i02 " :class="sids.indexOf(strategy.id) == -1?'':'on-i02'"></i>
    
    <em class="favorite_num ">{
   {vo.favornum}}</em>
</a>
queryUserFavor:function (sid,userId) {
    
    ajaxGet("/users/strategies/favor",{
    sid:sid, userId:userId}, function (data) {
    
        vue.sids = data.data;
    })
}

接口:

//查詢某個用戶收藏的攻略id集合
@GetMapping("/strategies/favor")
public Object strategiesFavor(String userId){
    
    return JsonResult.success(
        strategyStatisVORedisService.getUsersStrategyFavor(userId));
}
@Override
public List<String> getUsersStrategyFavor(String userId) {
    
    //獲取記號(攻略id集合)
    String signkey = RedisKeys.USER_STRATEGY_FAVOR.join(userId);
    //記號不存在:初始化
    List<String> sidList = new ArrayList<>();
    if (template.hasKey(signkey)){
    
        //記號存在:直接獲取
        String sidListStr = template.opsForValue().get(signkey);
        sidList = JSON.parseArray(sidListStr, String.class);
    }
    return sidList;
}

點贊數分析

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-mdgJxRGz-1651834690355)(C:\Users\ASUS\AppData\Roaming\Typora\typora-user-images\image-20220402170007638.png)]

點贊數+1

頁面:

strategyThumbup:function(){
    
    ajaxPost("/strategies/strategyThumbup",{
    sid:vue.strategy.id}, function (data) {
    
        if(data.data){
    
            popup("頂成功啦");
        }else{
    
            popup("今天你已經定過了");
        }
        vue.queryStatisVo(vue.strategy.id);
    })
}

設計key: Ctrl+Shift+X 切換大小寫

//用戶攻略頂
USER_STRATEGY_THUMB("user_strategy_thumb", -1L)

接口:

//攻略點贊
@RequireLogin
@PostMapping("/strategyThumbup")
public Object strategyThumbup(String sid, @UserParam UserInfo userInfo){
    
    //攻略點贊 : ret true 點贊成功, false錶示今天已經點過
    boolean ret = strategyStatisVORedisService.strategyThumbup(sid, userInfo.getId());
    return JsonResult.success(ret);
}

需要獲取時間間隔,引入工具類

public class DateUtil {
    
    /** * 獲取兩個時間的間隔(秒) * @param d1 * @param d2 * @return */
    public static long getDateBetween(Date d1, Date d2){
    
       return Math.abs((d1.getTime()-d2.getTime())/1000);//取絕對值
    }
    public static Date getEndDate(Date date) {
    
        if (date == null) {
    
            return null;
        }
        Calendar c = Calendar.getInstance();
        c.setTime(date);
        c.set(Calendar.HOUR_OF_DAY,23);
        c.set(Calendar.MINUTE,59);
        c.set(Calendar.SECOND,59);
        return c.getTime();
    }
}

service 實現類:

@Override
public boolean strategyThumbup(String strategyId, String userId) {
    
    //拼接記號 key
    String key = RedisKeys.USER_STRATEGY_THUMB.join(userId, strategyId);
    //判斷key是否存在
    if (!template.hasKey(key)){
    
        //如果不存在,獲取 vo,點贊數+1,緩存記號到redis中,設置過期時間
        StrategyStatisVO vo = this.getStrategyStatisVo(strategyId);
        vo.setThumbsupnum(vo.getThumbsupnum() + 1);
        this.setStrategyStatisVo(vo);
        //設置過期時間:今天最後一秒 - 當前時間
        Date now =new Date();
        Date end =DateUtil.getEndDate(now);
        Long time = DateUtil.getDateBetween(now, end);
        template.opsForValue().set(key,"1", time);
        return true;
    }
    //如果存在,不作任何操作
    return false;
}

完整 redis 緩存操作步驟

初始化 redis 數據

分析

數據預熱,從 MongoDB 初始化進入 Redis

初始化處理邏輯放置在 listener 監聽器中

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-Jf0xrTT7-1651834690356)(C:\Users\ASUS\AppData\Roaming\Typora\typora-user-images\image-20220402211054871.png)]

Spring 事件監聽

Spring框架中有哪些不同類型的事件 ?

Spring 提供了以下 5 種標准的事件:

  1. 上下文更新事件(ContextRefreshedEvent)

    在調用ConfigurableApplicationContext 接口中的 refresh() 方法時被觸發

    是容器啟動之後,所有 IOC、DI、等操作執行完之後,開始監聽

  2. 上下文開始事件(ContextStartedEvent)

    當容器調用ConfigurableApplicationContext的 Start() 方法開始/重新開始容器時觸發該事件

    剛開始啟動,容器准備創建的時候,開始監聽

  3. 上下文停止事件(ContextStoppedEvent)

    當容器調用 ConfigurableApplicationContext 的 Stop() 方法停止容器時觸發該事件

    容器死掉了之後,開始監聽

  4. 上下文關閉事件(ContextClosedEvent)

    當 ApplicationContext 被關閉時觸發該事件。容器被關閉時,其管理的所有單例Bean都被銷毀

    死掉之後,並且銷毀關燈了之後,開始監聽

  5. 請求處理事件(RequestHandledEvent)

    在Web應用中,當一個http請求(request)結束觸發該事件

如果一個bean實現了 ApplicationListener 接口,當一個 ApplicationEvent 被發布以後,bean會自動被通知

定義監聽器,實現接口:ApplicationListener<ContextRefreshedEvent>

redis緩存數據初始化監聽器

配置文件配置 redis

#redis
spring.redis.host=127.0.0.1

定義監聽器,實現接口:ApplicationListener<ContextRefreshedEvent>

注意: 一般不會查詢 MongoDB 所有攻略,一般選擇近一個月、近三個月、等等有範圍的數據,以防內存不足的情况

並且在業務邏輯中,獲取緩存對象 vo 的時候,判斷如果 vo 不存在,就進行初始化,也是因為一般 redis 中不會緩存數據庫中所有的數據

/** * @Component:使用了此注解注入容器管理之後才會開始工作 * redis緩存數據初始化監聽器 * * MongoDB ---> redis * 1、攻略統計對象 vo * 2、用戶攻略收藏列錶 [拓展] */
@Component
public class RedisDataInitListener implements ApplicationListener<ContextRefreshedEvent> {
    
    @Autowired
    private IStrategyService strategyService;
    @Autowired
    private IStrategyStatisVORedisService strategyStatisVORedisService;

    //當 Spring 容器啟動並初始化之後馬上執行該方法
    @Override
    public void onApplicationEvent(ContextRefreshedEvent contextRefreshedEvent) {
    
        System.out.println("--------------攻略vo對象初始化-begin---------------------");
        //1、查詢 MongoDB 所有攻略
        List<Strategy> list = strategyService.list();
        //2、遍曆所有攻略,封裝成 vo 對象
        for (Strategy strategy : list) {
    
            /** * 存在問題: * 如果第一次初始化後,前端進行vo對象統計,redis中統計數據跟數據庫中數據不一致了 * 如果第二次再進行初始化,後面操作會覆蓋之前redis緩存的vo對象 * 解决方案:如果 vo 已經存在了,直接跳過,不需要初始化 */
            if (strategyStatisVORedisService.isStrategyVoExist(strategy.getId())){
    
                continue;
            }
            StrategyStatisVO vo = new StrategyStatisVO();
            BeanUtils.copyProperties(strategy, vo);
            vo.setStrategyId(strategy.getId());
            //3、將 vo 緩存到 redis 中
            strategyStatisVORedisService.setStrategyStatisVo(vo);
        }
        System.out.println("--------------攻略vo對象初始化-end---------------------");
    }
}
//判斷指定攻略id的vo對象是否存在redis緩存中
@Override
public boolean isStrategyVoExist(String strategyId) {
    
    String key = RedisKeys.STRATEGY_STATIS_VO.join(strategyId);
    return template.hasKey(key);
}

redis 數據持久化

分析

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-A81JglAQ-1651834690357)(C:\Users\ASUS\AppData\Roaming\Typora\typora-user-images\image-20220402220244199.png)]

spring定時器

SpringBoot 三種方式實現定時任務:

  1. Timer:

    這是java自帶的java.util.Timer類,這個類允許你調度一個 java.util.TimerTask 任務。使用這種方式可以讓你的程序按照某一個頻度執行(每 3 秒執行一次),但不能在指定時間運行。一般用的較少

  2. ScheduledExecutorService:

    也jdk自帶的一個類,是基於線程池設計的定時任務類,每個調度任務都會分配到線程池中的一個線程去執行,也就是說,任務是並發執行,互不影響

  3. Spring Task:

    Spring3.0以後自帶的 task,可以將它看成一個輕量級的Quartz,而且使用起來比Quartz簡單許多

https://www.cnblogs.com/javahr/p/8318728.html

http://cron.qqe2.com/

定義任務執行器

/** * @Component:使用了此注解注入容器管理之後才會開始工作 * redis 緩存數據持久化定時任務 */
//@Component
public class RedisDataPersistenceJob {
    
    @Autowired
    private IStrategyService strategyService;
    @Autowired
    private IStrategyStatisVORedisService strategyStatisVORedisService;

    /** * cron:cron錶達式 -> 任務計劃 * 秒 分 小時 幾號 月份 周幾 年份 [spring支持的沒有年] * 0 0 2 1 * ? * 錶示每年每月1號的淩晨2點 * 0 15 10 ? * MON-FRI 錶示每月周一至周五每天上午10:15 * 0/10 錶示從0開始,每隔10觸發一次 * "0/10 * * * * ?" 錶示每10秒執行一次,從0秒(現在)開始 */
    @Scheduled(cron="0/10 * * * * ?")  //定時任務標簽
    public void redisDataPersistence(){
    
        System.out.println("--------------攻略vo對象持久化-begin---------------------");
        //1、從 redis 中獲取所有 vo 對象
        List<StrategyStatisVO> list = strategyStatisVORedisService.queryStrategyStatisVOs();
        //遍曆vo對象集合,執行攻略統計數據更新,更新至 MongoDB
        for (StrategyStatisVO vo : list) {
    
            strategyService.updateStrategyStatisVO(vo);
        }
        System.out.println("--------------攻略vo對象持久化-end---------------------");
    }
}

需要獲取 redis 中所有緩存數據,因為 redis 中的 key 都是規定好的格式,可以通過 指定字符 * 實現查詢所有,如圖:

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-ahaqdcAF-1651834690357)(C:\Users\ASUS\AppData\Roaming\Typora\typora-user-images\image-20220402220209778.png)]

所以在業務實現類中,也可以使用此字符串,來進行所有 redis 緩存數據的查詢

@Override
public List<StrategyStatisVO> queryStrategyStatisVOs() {
    
    // keys strategy_statis_vo:*
    String pattern = RedisKeys.STRATEGY_STATIS_VO.join("*");
    //獲取所有vo對象的key值
    Set<String> voKeys = template.keys(pattern);
    List<StrategyStatisVO> list = new ArrayList<>();
    if (voKeys != null && voKeys.size() > 0){
    
        //循環通過vo的key獲取vo對象
        for (String vo : voKeys) {
    
            String voStr = template.opsForValue().get(vo);
            list.add(JSON.parseObject(voStr, StrategyStatisVO.class));
        }
    }
    return list;
}

將指定的攻略vo統計數據對象,更新至MongoDB中

@Override
public void updateStrategyStatisVO(StrategyStatisVO vo) {
    
    Query query = new Query();
    query.addCriteria(Criteria.where("_id").is(vo.getStrategyId()));
    Update update = new Update();
    update.set("viewnum",vo.getViewnum());
    update.set("favornum",vo.getFavornum());
    update.set("replynum",vo.getReplynum());
    update.set("thumbsupnum",vo.getThumbsupnum());
    update.set("sharenum",vo.getSharenum());
    mongoTemplate.updateMulti(query, update, Strategy.class);
}

啟動springboot 定時任務支持:在啟動類上,貼注解 @EnableScheduling

@SpringBootApplication
@EnableScheduling     // SpringBoot開啟定時任務功能
public class MgrSite {
    
	
    public static void main(String[] args) {
    
        SpringApplication.run(MgrSite.class,args);
    }
    
 	//....
}

10、網站首頁

後端

banner 錶設計

性名稱屬性類型屬性說明
IdString主鍵,自增長
refIdString關聯的id
titleString標題
subTitleString副標題
coverUrlString封面
stateInt狀態
sequenceint序號
typeint約定關聯的id是遊記id還是攻略id

CRUD

domain、QueryObject、repository、service、controller

/** * 遊記推薦 */
@Setter
@Getter
@Document("banner")
public class Banner  extends BaseDomain {
    
    public static final int STATE_NORMAL = 0;   //正常
    public static final int STATE_DISABLE = 1;  //禁用
    public static final int TYPE_TRAVEL = 1;  //遊記
    public static final int TYPE_STRATEGY = 2;  //攻略
    private String refId;  //關聯id
    private String title;  //標題
    private String subTitle; //副標題
    private String coverUrl; //封面
    private int state = STATE_NORMAL; //狀態
    private int sequence; //排序
    private int type;
    public String getJsonString(){
    
        Map<String, Object> map = new HashMap<>();
        map.put("id",id);
        map.put("title",title);
        map.put("subTitle",subTitle);
        map.put("coverUrl",coverUrl);
        map.put("state",state);
        map.put("sequence",sequence);
        map.put("refId",refId);
        map.put("type",type);
        return JSON.toJSONString(map);
    }
    public String getStateDisplay(){
    
        return state == STATE_NORMAL?"正常":"禁用";
    }
    public String getTypeDisplay(){
    
        return type == TYPE_STRATEGY?"攻略":"遊記";
    }
}

banner 添加

在類型、關聯文章的兩個下拉框中,當類型選擇 攻略 、或者遊記 時,關聯文章應該顯示對應的 攻略、 或者遊記

頁面:

先查詢出所有的攻略、遊記,緩存放進變量

var sts;
var ts;
$(function () {
    
    $.get("/banner/getAllType", function (data) {
    
        if(data.code == 200){
    
            var map = data.data;
            sts = map.sts;  //攻略
            ts = map.ts;  //遊記

            console.log(sts);
            console.log(ts);
        }
    })
})

當類型的下拉框選擇 1 時,遍曆剛剛遊記的變量。當選擇 2 時,遍曆剛剛攻略的變量

$("#typeId").change(function () {
     //當類型的下拉框內容改變的時候執行
    $("#refId").html('');
    var html = '<option value="-1">--請選擇--</option>';
    if(this.value == 1){
    
        //遊記
        $.each(ts, function (index, item) {
    
            html += '<option value="'+item.id+'">'+item.title+'</option>'
        })
    }else if(this.value == 2){
    
        //攻略
        $.each(sts, function (index, item) {
    
            html += '<option value="'+item.id+'">'+item.title+'</option>'
        })
    }
    $("#refId").html(html);
})

接口:

@RequestMapping("/getAllType")
@ResponseBody
public Object getAllType(){
    
    List<Strategy> sts = strategyService.list();
    List<Travel> ts = travelService.list();
    Map<String, Object> map = new HashMap<>();
    map.put("sts",sts);
    map.put("ts",ts);
    return JsonResult.success(map);
}

前端首頁推薦 banner

首頁封面類似banner的組件,使用推薦遊記前4篇進行列錶

接口:

//首頁推薦遊記5篇
@GetMapping("/banners/travel")
public Object bannersTravel(){
    
    List<Banner> list = bannerService.queryBannerByType(Banner.TYPE_TRAVEL);
    return JsonResult.success(list);
}

//首頁推薦攻略1篇
@GetMapping("/banners/strategy")
public Object bannersStrategy(){
    
    List<Banner> list = bannerService.queryBannerByType(Banner.TYPE_STRATEGY);
    return JsonResult.success(list.get(0));
}

service 實現類:

@Override
public List<Banner> queryBannerByType(int type) {
    
    //注意,查詢出的內容狀態應是正常
    Pageable pageable = PageRequest.of(0,5, Sort.Direction.ASC,"sequence");
    return bannerRepository.findByTypeAndState(type, Banner.STATE_NORMAL, pageable);
}

持久化接口:

//Banner持久化操作接口,類似 Mapper 接口
@Repository
public interface BannerRepository extends MongoRepository<Banner, String> {
    
    /** * 根據類型和狀態查詢 banner * @param type * @param state * @param pageable * @return */
    List<Banner> findByTypeAndState(int type, int state, Pageable pageable);
}

使用es

依賴、配置文件

trip-core 中添加依賴

<!--elasticsearch-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-elasticsearch</artifactId>
</dependency>
<dependency>
    <groupId>commons-beanutils</groupId>
    <artifactId>commons-beanutils</artifactId>
    <version>1.9.3</version>
</dependency>

trip-website-api 、trip-mgrsite 中都要添加以下配置

# 配置集群名稱,名稱寫錯會連不上服務器,默認elasticsearch
spring.data.elasticsearch.cluster-name=elasticsearch
# 節點的地址,注意api模式下端口號是9300,千萬不要寫成9200
# 配置集群節點。集群時,用逗號隔開,es會自動尋找節點
spring.data.elasticsearch.cluster-nodes=127.0.0.1:9300
#是否開啟本地存儲
spring.data.elasticsearch.repositories.enable=true

注意,兩個模塊中都要添加 es 配置,否則可能會報錯

實體類、CRUD

數據初始化

數據初始化:即將 MongoDB 的數據 初始化進 elasticsearch 庫中

一般初始化的操作放在後臺管理的模塊,即 trip-mgrsite 模塊

此處為了方便控制數據初始化,放在 trip-website-api 模塊

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-Qv0grIow-1651834690358)(C:\Users\ASUS\AppData\Roaming\Typora\typora-user-images\image-20220406205344244.png)]

需要注意一個問題

注意: 因為當添加數據的時候,如果沒有對應的索引存在,es 會推測索引及類型,自己去創建索引。就會與我們要求需要的索引類型不一致,在後續操作中可能會出錯

所以在項目啟動之後,一定要等 索引、類型 加載好(根據domain生成好)之後,在進行數據初始化操作

@RestController
public class DataInitController {
    
    //...
}

關鍵字搜索

頁面:

//搜索相關
function searchByType(type, keyword) {
    
    if(!keyword){
    
        popup("請先輸入搜索關鍵字");
        return;
    }
    var html = '';
    if(type == 0){
    
        html = 'searchDest.html';
    }else if(type == 1){
    
        html = 'searchStrategy.html';
    }else if(type == 2){
    
        html = 'searchTravel.html';
    }else if(type == 3){
    
        html = 'searchUser.html';
    }else{
    
        html = 'searchAll.html';
    }
    window.location.href="/views/search/"+html+"?type=" +type+ "&keyword=" + keyword;
}
mounted:function () {
    
    var params = getParams();
    ajaxGet("/q", params, function (data) {
    
        var map = data.data;
        vue.result = map.result;
        vue.qo = map.qo;
        $("#_j_search_input").val(vue.qo.keyword);
        $("#searchType").val(-1);  //所有
    })
}

定義查詢參數類 SearchQueryObject

@Setter
@Getter
public class SearchQueryObject extends QueryObject {
    
    public static final int TYPE_ALL = -1;
    public static final int TYPE_DEST = 0;
    public static final int TYPE_STRATEGY = 1;
    public static final int TYPE_TRAVEL = 2;
    public static final int TYPE_USER = 3;
    private int type = -1;
}

定義結果封裝vo對象 SearchResultVO

@Setter
@Getter
public class SearchResultVO implements Serializable {
    
    private Long total = 0L;
    private List<Strategy> strategys = new ArrayList<>();
    private List<Travel> travels = new ArrayList<>();
    private List<UserInfo> users = new ArrayList<>();
    private List<Destination> dests = new ArrayList<>();
}

接口:

window.location.href =  "/views/search/"+html+"?type=" + type +"&keyword=" + keyword;

注意: 因為最開始的地方是將用戶輸入的關鍵字,傳入了 url 地址中,後面獲取參數都是從 url 上進行獲取,就會有瀏覽器的編碼,所以在接口中獲取的關鍵字是編碼後的

所以在使用之前需要先解碼

@RestController
public class SearchController {
    
    
    @GetMapping("/q")
    public Object search(SearchQueryObject qo){
    
        //url解碼
        String kw = URLDecoder.decode(qo.getKeyword(), "UTF_8");
        qo.setKeyword(kw);
        
        switch (qo.getType()){
    
            case  SearchQueryObject.TYPE_DEST :
                //目的地
                return searchDest(qo);
            case  SearchQueryObject.TYPE_STRATEGY :
                //攻略
                return searchStrategy(qo);
            case  SearchQueryObject.TYPE_TRAVEL :
                //遊記
                return searchTravel(qo);
            case  SearchQueryObject.TYPE_USER :
                //用戶
                return searchUser(qo);
            default:
                //全部
                return searchAll(qo);
        }
    }
    
    //...
}

目的地搜索

需求: 查詢目的地,如果找到, 顯示該目的地下所有 攻略, 遊記, 用戶

頁面:

mounted:function () {
    
    var params = getParams();
    ajaxGet("/q", params, function (data) {
    
        var map = data.data;
        vue.result = map.result;
        vue.dest = map.dest;
        vue.qo = map.qo;
        $("#_j_search_input").val(vue.qo.keyword);
        $("#searchType").val(0);  //目的地
    })
}

接口:

//查詢目的地
private Object searchDest(SearchQueryObject qo) {
    
    //1、查詢keyword對應的目的地是否存在
    Destination dest = destinationService.queryByName(qo.getKeyword());
    SearchResultVO vo = new SearchResultVO();
    if(dest != null){
    
        //2、如果存在,查詢該目的地下所有攻略、遊記、用戶。 約定前5篇
        List<Strategy> sts = strategyService.queryByDestId(dest.getId());
        List<Travel> ts = travelService.queryByDestId(dest.getId());
        List<UserInfo> us = userInfoService.queryByCity(dest.getName());
        vo.setStrategys(sts);
        vo.setTravels(ts);
        vo.setUsers(us);
        vo.setTotal(sts.size() + ts.size() + us.size() + 0L);
    }
    //3、返回
    Map<String,Object> map = new HashMap<>();
    map.put("result",vo);
    map.put("dest",dest);
    map.put("qo",qo);
    return JsonResult.success(map);
}

數據封裝對象

定義結果封裝vo對象 SearchResultVO

@Setter
@Getter
public class SearchResultVO implements Serializable {
    
    private Long total = 0L;
    private List<Strategy> strategys = new ArrayList<>();
    private List<Travel> travels = new ArrayList<>();
    private List<UserInfo> users = new ArrayList<>();
    private List<Destination> dests = new ArrayList<>();
}

攻略查詢(高亮)

僅僅對攻略進行全文搜索,攻略: 標題(title),副標題(subTitle),概要(summary)

需要高亮,需要分頁

高亮服務類

高亮顯示,需要先引入 service 高亮服務類

/** * 所有es公共服務 */
public interface ISearchService {
    
    /** * 全文搜索 + 高亮顯示 * @param index 索引 * @param type 類型 * @param clz 返回的對象類型 * @param qo * @param fields 需要高亮的字段 * @param <T> * @return 帶有分頁的全文搜索(高亮顯示)結果集 */
    <T> Page<T>  searchWithHighlight(String index, String type, Class<T> clz,
                                     SearchQueryObject qo, String... fields);
}

參數查詢對象 QueryObject 裏面添加分頁參數屬性

private Pageable pageable;  //分頁設置對象
public Pageable getPageable(){
    
    if(pageable == null){
    
        //沒有指定分頁對象值, 默認id倒序
        return  PageRequest.of(currentPage - 1, pageSize,
                Sort.Direction.ASC, "_id");
    }
    return pageable;
}

頁面:

var params = getParams();
ajaxGet("/q", params, function (data) {
    
    var map = data.data;
    vue.page = map.page;
    vue.qo = map.qo;
    $("#_j_search_input").val(vue.qo.keyword);
    $("#searchType").val(1);  //攻略
    buildPage(vue.page.number, vue.page.totalPages, vue.doPage)
})

接口:

注意: 這裏能實現高亮的列,必須和 elasticsearch 的索引中的字段名一致

//搜索攻略
private Object searchStrategy(SearchQueryObject qo) {
    
    //攻略的全文搜索+高亮
    Page<Strategy> spage = searchService.searchWithHighlight(
                            StrategyEs.INDEX_NAME, StrategyEs.TYPE_NAME,
                            Strategy.class, qo, "title", "subTitle", "summary"
    );
    //返回
    Map<String,Object> map = new HashMap<>();
    map.put("page",spage);
    map.put("qo",qo);
    return JsonResult.success(map);
}

抽取

public class ParamMap extends HashMap<String ,Object> {
    
    //鏈
    public ParamMap put(String key, Object value) {
    
        super.put(key, value);
        return this;
    }

    public static ParamMap newInstance() {
    
        return new ParamMap();
    }
}
//攻略的全文搜索+高亮
private Page<Strategy> queryStrategyPage(SearchQueryObject qo) {
    
    return searchService.searchWithHighlight(StrategyEs.INDEX_NAME, StrategyEs.TYPE_NAME, Strategy.class, qo, "title", "subTitle", "summary");
}
//用戶的全文搜索+高亮
private Page<UserInfo> queryUserInfoPage(SearchQueryObject qo) {
    
    return searchService.searchWithHighlight(UserInfoEs.INDEX_NAME, UserInfoEs.TYPE_NAME, UserInfo.class, qo, "info", "city");
}
//遊記的全文搜索+高亮
private Page<Travel> queryTravelPage(SearchQueryObject qo) {
    
    return searchService.searchWithHighlight(TravelEs.INDEX_NAME, TravelEs.TYPE_NAME, Travel.class, qo, "title","summary");
}
//目的地的全文搜索+高亮
private Page<Destination> queryDestinationPage(SearchQueryObject qo) {
    
    return searchService.searchWithHighlight(DestinationEs.INDEX_NAME, DestinationEs.TYPE_NAME, Destination.class, qo, "name", "info");
}

遊記、用戶查詢

同攻略:發現有重複,所以抽取工具類

用戶: 簡介(info), 城市(city)

遊記:標題(title), 概要(summary)

//搜索用戶
private Object searchUser(SearchQueryObject qo) {
    
    return JsonResult.success(new ParamMap()
                              .put("page",this.queryUserInfoPage(qo))
                              .put("qo",qo));
}

//搜索遊記
private Object searchTravel(SearchQueryObject qo) {
    
    return JsonResult.success(ParamMap.newInstance()
                              .put("page",this.queryTravelPage(qo))
                              .put("qo",qo));
    //return JsonResult.success(new ParamMap().put("page",page).put("qo",qo));
}

全部搜索(高亮)

//查詢所有
private Object searchAll(SearchQueryObject qo) {
    
    //1、全文搜索+高亮
    Page<UserInfo> us = this.queryUserInfoPage(qo);
    Page<Travel> ts = this.queryTravelPage(qo);
    Page<Strategy> sts = this.queryStrategyPage(qo);
    Page<Destination> ds = this.queryDestinationPage(qo);
    //2、將每個部分的第1頁數據,封裝為結果對象
    SearchResultVO vo = new SearchResultVO();
    vo.setDests(ds.getContent());
    vo.setStrategys(sts.getContent());
    vo.setTravels(ts.getContent());
    vo.setUsers(us.getContent());
    vo.setTotal(sts.getTotalElements() + ts.getTotalElements() 
                + us.getTotalElements() + ds.getTotalElements());
    //3、返回
    //鏈式編程
    return JsonResult.success(new ParamMap().put("result",vo).put("qo",qo));
}

高亮顯示分析

高亮就是在查詢出的結果的基礎上,對關鍵字部分加上前後標簽,然後進行返回。

如果沒有指定前後標簽,則默認會將關鍵字進行加粗顯示

數據同步與更新【拓展】

數據同步問題

使用消息中間件:kafka / 各類MQ

分析:

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-YKXMnAxp-1651834690359)(C:\Users\ASUS\AppData\Roaming\Typora\typora-user-images\image-20220406205230276.png)]

問題

解决 netty 沖突

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-bUAmnPhT-1651834690360)(C:\Users\ASUS\AppData\Roaming\Typora\typora-user-images\image-20220406205427058.png)]

因為使用的 redis、es,二者都會使用到 netty ,導入依賴時可能存在版本不一致,就使得使用的時候,底層創建了兩個 netty ,產生沖突

解决:

在啟動類裏面加上代碼

public static void main(String[] args) {
    
    //解决netty沖突
    System.setProperty("es.set.netty.runtime.available.processors", "false");
    SpringApplication.run(WebSite.class,args);
}

11、接口

接口文檔 swagger2

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-OVT7rt1t-1651834690361)(C:\Users\ASUS\AppData\Roaming\Typora\typora-user-images\image-20220408092649291.png)]

作用

  1. api一定需要開發文檔配合,移動端只需要根據開發文檔進行開發即可;
  2. 傳統的開發文檔問題:格式隨意,更新不及時;

https://www.jianshu.com/p/d7b13670e0eb

ShowDoc :https://www.showdoc.cc/

swagger2

Swagger能够根據代碼中的注解自動生成api文檔,並且提供測試接口;

依賴

依賴放在 trip-website-api 模塊,因為使用的是這裏的接口

<dependency>
    <groupId>io.springfox</groupId>
    <artifactId>springfox-swagger2</artifactId>
    <version>2.9.2</version>
</dependency>
<dependency>
    <groupId>io.springfox</groupId>
    <artifactId>springfox-swagger-ui</artifactId>
    <version>2.9.2</version>
</dependency>

配置文件

配置類

@Configuration
@EnableSwagger2
public class SwaggerConfig implements WebMvcConfigurer {
    
    @Bean
    public Docket productApi() {
    
        //添加head參數start
        ParameterBuilder tokenPar = new ParameterBuilder();
        List<Parameter> pars = new ArrayList<Parameter>();
        tokenPar.name("token").description("令牌").modelRef(new ModelRef("string")).parameterType("header").required(false).build();
        pars.add(tokenPar.build());
        return new Docket(DocumentationType.SWAGGER_2).select()
                // 掃描的包路徑
                .apis(RequestHandlerSelectors.basePackage("com.controller"))
                // 定義要生成文檔的Api的url路徑規則
                .paths(PathSelectors.any())
                .build()
                .globalOperationParameters(pars)
                // 設置swagger-ui.html頁面上的一些元素信息。
                .apiInfo(metaData());
    }
    private ApiInfo metaData() {
    
        return new ApiInfoBuilder()
                // 標題
                .title("SpringBoot集成Swagger2")
                // 描述
                .description("項目接口文檔")
                // 文檔版本
                .version("1.0.0")
                .license("Apache License Version 2.0")
                .licenseUrl("https://www.apache.org/licenses/LICENSE-2.0")
                .build();
    }
    //ui頁面
    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
    
        registry.addResourceHandler("swagger-ui.html")
                .addResourceLocations("classpath:/META-INF/resources/");
        registry.addResourceHandler("/webjars/**")
                .addResourceLocations("classpath:/META-INF/resources/webjars/");
    }
}

訪問

http://localhost:8080/swagger-ui.html

常見標簽

@Api/@ApiOperation

  • @Api:用在類上,說明該類的作用

    @Api(value = "用戶資源",description = "用戶資源控制器")
    public class UserInfoController {
           ... }
    
  • @ApiOperation:用在方法上,說明方法的作用

    @ApiOperation(value = "注册功能",notes = "其實就是新增用戶")
    @PostMapping("/regist")
    public Object regist( ... ){
           ... }
    

@ApiImplicitParams/@ApiImplicitParam 參數說明

@ApiImplicitParams:用在方法上包含一組參數說明

@ApiImplicitParam:用在@ApiImplicitParams注解中,指定一個請求參數的各個方面

  • paramType:參數放在哪個地方
  • header–>請求參數的獲取
  • query–>請求參數的獲取
  • path–>請求參數的獲取(用於restful接口):
  • body–>請求實體中
@ApiOperation(value = "注册功能",notes = "其實就是新增用戶")
@ApiImplicitParams({
    
     @ApiImplicitParam(value="昵稱",name="nickName",dataType ="String",required=true),
  @ApiImplicitParam(value="驗證碼",name="verifyCode",dataType="String",required=true), 
      @ApiImplicitParam(value="密碼",name="password",dataType="String",required= true)
})
@PostMapping("/regist")
public Object regist(String phone, String nickname,String password,String rpassword,String verifyCode){
    
    //...
}

@ApiModel/@ApiModelProperty 實體類上

需要配合 get、set 方法

@ApiModel:描述一個 Model 的信息

(這種一般用在 post 創建的時候,使用@RequestBody這樣的場景,請求參數無法使用@ApiImplicitParam注解進行描述的時候)

@ApiModelProperty:描述一個 model 的屬性

@Setter
@Getter
@ApiModel(value="用戶",description="平臺注册用戶模型")
public class UserInfo extends BaseDomain{
    
    
    @ApiModelProperty(value="昵稱",name="nickName",dataType = "String",required = true)
    private String nickname;  //昵稱
}

@ApiResponses/@ApiResponse

@ApiResponses:用於錶示一組響應

@ApiResponse:用在@ApiResponses中,一般用於錶達一個錯誤的響應信息(200相應不寫在這裏面)

  • code:數字,例如400
  • message:信息,例如"請求參數沒填好"
  • response:拋出的异常類
@ApiResponses({
    
	@ApiResponse(code=200,message="用戶注册成功")
})

@ApiIgnore 不顯示

有些接口不想顯示,就貼上去,可以貼在類上,也可以貼在方法上。

接口安全

api接口分類:

1> 公共接口:你查快遞,你查天氣預報,你查飛機,火車班次等,這些都是有公共的接口

2>私密接口:需要登錄訪問或者公司內部接口

接口安全要求:

  1. 防偽裝攻擊(案例:在公共網絡環境中,第三方 有意或惡意 的調用我們的接口):接口防刷

  2. 防篡改攻擊(案例:在公共網絡環境中,請求頭/查詢字符串/內容 在傳輸過程被修改):接口防篡改(簽名機制)

  3. 防重放攻擊(案例:在公共網絡環境中,請求被截獲,稍後被重放或多次重放):接口時效性

  4. 防數據信息泄漏(案例:截獲用戶登錄請求,截獲到賬號、密碼等):接口加密(https/對稱加解密)

接口防刷

分析:

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-X0TYLteN-1651834690361)(C:\Users\ASUS\AppData\Roaming\Typora\typora-user-images\image-20220407130729700.png)]

思路:

  1. 設計一個redis臨時key, 有效時間是1分鐘,1分鐘內只允許10訪問

    key> url : ip

    value>訪問的次數

  2. 設置攔截器,攔截需要防刷的接口url

  3. 攔截邏輯

    1. 攔截 url,拼接 key 查詢 redis 中是否存在
    2. 如果不存在 setnx url:ip 10
    3. 如果存在 derc url:ip
    4. 如果次數减到0,攔截返回:請勿頻繁訪問
    5. 其他情况直接放行。

代碼:

因為操作是針對接口,所以放在 trip-website-api 模塊中

引入一個工具類,用於獲取用戶的 ip

public class RequestUtil {
    
    public static String getIPAddress() {
    
        //...
    }
}    

因為需要存放在 redis 中,所以設計 RedisKey

//接口防刷key,單比特秒
BRUSH_PROOF("brush_proof", 10L)

攔截器:

/** * 防刷攔截器 */
public class BrushProofInterceptor implements HandlerInterceptor {
    

    @Autowired
    private ISecurityRedisService securityRedisService;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
    
        //解决攔截器跨域問題
        if(!(handler instanceof HandlerMethod)){
    
            return  true;
        }
        //request.getRequestURL() : http://localhost:8088/users/xxx
        //request.getRequestURI() : /users/xxx
        //防刷驗證
        String url = request.getRequestURI().substring(1);  //去掉開頭斜杠
        String ip = RequestUtil.getIPAddress();
        String key = RedisKeys.BRUSH_PROOF.join(url, ip);
        if(!securityRedisService.isAllowBrush(key)){
    
            response.setContentType("application/json;charset=UTF-8");
            response.getWriter().write(JSON.toJSONString(JsonResult.error(500, "請勿頻繁訪問","謝謝咯")));
            return false;
        }
        return true;
    }
}

redis 服務:

@Service
public class SecurityRedisServiceImpl implements ISecurityRedisService {
    
    @Autowired
    private StringRedisTemplate template;
    @Override
    public boolean isAllowBrush(String key) {
    
        /* 方式一:不推薦 判斷key是否存在 如果存在,對應的value -1,更新 如果不存在,創建 key,設置value為 10-1,有效性1分鐘 */

        /** * 如果有不做 任何操作,如果沒有添加 * setIfAbsent:若key不存在,執行創建key操作,若存在不做任何操作 * 類似於redis命令中的 setnx:錶示如果key不存在就創建並執行,若存在不做任何操作 * setnx key1 value1 */
        template.opsForValue().setIfAbsent(key, "10", RedisKeys.BRUSH_PROOF.getTime(), TimeUnit.SECONDS);
        Long decrement = template.opsForValue().decrement(key);
        return decrement >= 0;
    }
}

攔截器配置:啟動類中

@SpringBootApplication
public class WebSite implements WebMvcConfigurer{
    
    
    //接口防刷攔截器
    @Bean
    public BrushProofInterceptor brushProofInterceptor(){
    
        return  new BrushProofInterceptor();
    }
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
    
        //防刷
        registry.addInterceptor(brushProofInterceptor())
                .addPathPatterns("/**");
    }
    //...
}    

接口防篡改

簽名機制

分析:

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-JWDzW8ep-1651834690362)(C:\Users\ASUS\AppData\Roaming\Typora\typora-user-images\image-20220407130809453.png)]

思路:

  1. 前端傳參數時,對參數的名進行字典排序,然後按照參數名的屬性對參數值進行有序拼接

    ​ 比如:

    ​ 參數列錶: c=參數1, f=參數2, a=參數3, b=參數4

    ​ 參數排名: a b c f

    ​ 參數值拼接:a=參數3 & b=參數4 & c=參數1& f=參數2

  2. 使用 MD5 對拼接參數值串進行加密得到參數簽名 sign_client

  3. 將所有參數與 sign 一同發送到後端

  4. 後端獲取到所有參數, 按照同樣的邏輯,MD5加密得到sign_server

    ​ 參數列錶: c=參數1, f=參數2, a=參數3, b=參數4 sign_client

    ​ 參數排名: a b c f

    ​ 參數值拼接:a=參數3 & b=參數4 & c=參數1& f=參數2

  5. 對比 sign_server 跟 sign_client 2個簽名是否一致, 一致錶示參數沒變篡改,否則提示參數被改,不合法

注意:

  1. 簽名算法不能被泄露——js 混淆與加密
  2. 需要傳人很多參數/大數據量參數(比如上傳),不能使用這種方式——針對性處理

頁面:

頁面改動:common.js

//執行前端參數加密操作 返回sign前面
//{aa:1, cc:2, bb:3}
function getSignString(param) {
    
    //1:參數排序:字典順序
    var sdic=Object.keys(param).sort();
    //2:參數拼接
    var signStr = "";
    for(var i in sdic){
    
        if(i == 0){
    
            signStr +=sdic[i]+"="+param[sdic[i]];
        }else{
    
            signStr +="&"+sdic[i]+"="+param[sdic[i]];
        }
    }
    //signStr = aa=1&bb=3&cc=2
    console.log(signStr);
    //3:md5加密
    console.log(hex_md5(signStr));
    return hex_md5(signStr).toUpperCase();
}

請求方法:ajaxGet 底層調用 數據防篡改函數

//通過js操作將加密之後的簽名手動添加到參數中去
param.sign = getSignString(param);  //使用邏輯處理

所有頁面添加md5.js

 <script src="js/md5/md5.js"></script>

請求測試:

//防篡改操作
ajaxGet("/test2", {
    aa:1, cc:2, bb:3}, function (data) {
    
    console.log(data);
})

後端:

提供工具類,使得傳入的參數,進行排序、MD5加密、大寫 操作

/** * Md5工具類 */
public class Md5Utils {
    
    /** * @Description: 簽名:請求參數排序並後面補充key值,最後進行MD5加密,返回大寫結果 * @param params 參數內容 */
    public static String signatures(Map<String, Object> params){
    
        //...
    }
}   

簽名攔截器:

/** * 簽名攔截(防篡改) */
public class SignInterceptor implements HandlerInterceptor {
    
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
    
        if(!(handler instanceof HandlerMethod)){
    
            return  true;
        }
        /** * 此處接收參數,value比特置是數組String[] * 因為前端可能有多選下拉框等,就會是是多個數據 * aa:["1"] * bb:["2"] */
        Map<String, String[]> map = request.getParameterMap();
        Set<String> keys = map.keySet();
        Map<String, Object> param = new HashMap<>();
        for (String s : map.keySet()) {
    
            if("sign".equalsIgnoreCase(s)){
    
                continue;
            }
            param.put(s, arrayToString(map.get(s)));
        }
        //簽名驗證
        String signatures = Md5Utils.signatures(param);  //sign_server
        String sign = request.getParameter("sign");   //sign_client
        if(sign == null || !sign.equalsIgnoreCase(signatures)){
    
            response.setContentType("text/json;charset=UTF-8");
            response.getWriter().write(JSON.toJSONString(new JsonResult(501,"簽名校驗失敗","不好意思咯")));
            return false;
        }
        return true;
    }
    private String arrayToString(String [] array){
    
        StringBuilder sb = new StringBuilder(10);
        for (String s : array) {
    
            sb.append(s);
        }
        return sb.toString();
    }
}

配置攔截器:

@SpringBootApplication
public class WebSite implements WebMvcConfigurer{
    
    //接口防篡改攔截器
    @Bean
    public SignInterceptor signInterceptor(){
    
        return  new SignInterceptor();
    }
    //接口防刷攔截器
    @Bean
    public BrushProofInterceptor brushProofInterceptor(){
    
        return  new BrushProofInterceptor();
    }
    //將攔截器注入Spring容器中,交給SpringBoot管理
    @Bean
    public CheckLoginInterceptor checkLoginInterceptor(){
    
        return new CheckLoginInterceptor();
    }
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
    
        //登錄權限驗證
        registry.addInterceptor(checkLoginInterceptor())
                .addPathPatterns("/**")
        //防刷
        registry.addInterceptor(brushProofInterceptor())
                .addPathPatterns("/**");
        //簽名
        InterceptorRegistration it = registry.addInterceptor(signInterceptor())
                .addPathPatterns("/**");
    }
    //...
}    

接口時效性

分析:

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-4Q7CAoAV-1651834690363)(C:\Users\ASUS\AppData\Roaming\Typora\typora-user-images\image-20220407130825266.png)]

簽名機制+有效時間

思路:

  1. 前端傳參數時,對參數的名進行字典排序,然後按照參數名的屬性對參數值進行有序拼接

    ​ 比如:

    ​ 參數列錶: c=參數1, f=參數2, a=參數3, b=參數4 timestamp=188888

    ​ 參數排名: a b c f timestamp

    ​ 參數值拼接:a=參數3&數4& c=參數1& f=參數2&timestamp=188888

  2. 使用MD5對拼接參數值串進行加密得到參數簽名sign_client

  3. 將所有參數與sign一同發送到後端

  4. 後端獲取到所有參數, 按照同樣的邏輯,MD5加密得到sign_server

    ​ 參數列錶: c=參數1, f=參數2, a=參數3, b=參數4 sign_client

    ​ 參數排名: a b c f

    ​ 參數值拼接:a=參數3&數4& c=參數1& f=參數2&timestamp=188888

  5. 獲取timestamp與當前時間對比是否在有效時間內, 比如1分鐘, 在執行下一步, 不在,提示接口訪問失效

  6. 對比sign_server 跟sign_client 2個簽名是否一致, 一致錶示參數沒變篡改,否則提示參數被改,不合法

接口加密

https 分析:

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-eNOKNcYb-1651834690363)(C:\Users\ASUS\AppData\Roaming\Typora\typora-user-images\image-20220408092611395.png)]

https 密文傳輸 http 明文傳輸

阿裏雲服務申請https加密證書

https://www.cnblogs.com/shibaolong/p/9837247.html

https 本地證書

SpringBoot 整合 SSL 證書

12、mongodb事務

MongoDB複制集

複制集概念

Mongodb複制集由一組Mongod實例(進程)組成,包含一個Primary節點和多個Secondary節點,Mongodb Driver(客戶端)的所有數據都寫入Primary,Secondary從Primary同步寫入的數據,以保持複制集內所有成員存儲相同的數據集,提供數據的高可用。

為什麼存在:

  • 高可用
  • 數據備份
  • 讀寫分離

主從選舉機制

一個典型的複制集,有3個以上 (一般是2n+1個) 具有投票權的節點組成:

  • 一個主節點(Primary): 接收寫入操作和選擇時投票 【主寫】
  • 2個(或多個)從節點(secondary):複制主節點上的新數據和選舉是投票【主讀】

選舉細節:

1>集群中大部分節點是活的

2>稱為主節點必須能跟從節點建立連接

3>具有較新的 oplog 文件

4>具有較高的優先級

主從複制

  1. 主節點主寫

    當在主節點執行一個DML操作時,主節點會對數據的操作過程做記錄, 這些記錄稱為oplog

    oplog詳解: https://www.cnblogs.com/Joans/p/7723554.html

  2. 從節點主讀

    從節點通過在主節點上打開一個tailable 遊標不斷讀取主節點的oplog,執行上面的數據命令(命令回放),以此保持跟主節點的數據一致。

配置

看文檔 mongodb的複制集配置.docx

事務測試(java)

操作前注意:必須要先創建集合再操作

配置文件

spring.data.mongodb.uri=mongodb://127.0.0.1:27017,127.0.0.1:27018,127.0.0.1:27019/monodemo?replicaSet=rs

啟動類

	//mongodb事務
    @Bean
    public MongoTransactionManager transactionManager(MongoDbFactory dbFactory) {
    
        return new MongoTransactionManager(dbFactory);
    }

服務類添加事務注解

服務類
@Transactional
------------------------------------------------------------------------------------------
@Setter
@Getter
@Document(collection = "user")
@ToString
public class User implements Serializable{
    
    @Id
    private String id;
    private String name;
    private int age;
}
@Setter
@Getter
@Document(collection = "person")
@ToString
public class Person {
    
    @Id
    private String id;
    private String name;
    private int age;
}

版權聲明
本文為[磊哥的小迷妹]所創,轉載請帶上原文鏈接,感謝
https://cht.chowdera.com/2022/133/202205131248124852.html

隨機推薦