溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

寫了10年JAVA代碼,為何還是給人一種亂糟糟的感覺?

發(fā)布時間:2020-07-23 11:21:17 來源:網(wǎng)絡(luò) 閱讀:384 作者:wx5d9ed7c8443c3 欄目:編程語言

接觸過不少號稱寫了10多年代碼的程序員,可經(jīng)常還是會發(fā)現(xiàn)他們的代碼給人一種亂糟糟的感覺,那么如何才能寫出讓同事感覺不那么亂的代碼呢?

一、為什么要寫這篇文章

在開篇之前先說明下為什么要寫這篇文章?在Java的世界里MVC軟件架構(gòu)模式絕對是經(jīng)典的存在(PS:MVC是一種軟件架構(gòu)方式并不只有Java有),如果你是在最近十年前后進入Java的編程世界,那么你會發(fā)現(xiàn)自己這些年似乎從來沒有逃離MVC架構(gòu)模式的牢籠,只不過換著使用了不同的MVC框架,如早期的Struts1、Struts2以及現(xiàn)在幾乎一統(tǒng)江湖的Spring MVC(少數(shù)自行封裝MVC框架的公司除外)。

而隨著互聯(lián)網(wǎng)技術(shù)的發(fā)展,特別是Ajax等富客戶端技術(shù)的發(fā)展,前端技術(shù)逐步形成了一套體系,并且逐步從后端代碼(如JSP)中剝離出來,從而形成了現(xiàn)在普遍流行的前后端分離模式(這也是一段時間內(nèi)為什么前端工程師會出現(xiàn)大量需求的原因),而這也對傳統(tǒng)的MVC模式產(chǎn)生了一點小的改變,因為現(xiàn)在基于Java的后端服務(wù)中很少會有大量處理復(fù)雜界面邏輯的代碼出現(xiàn),因此MVC中的V(View)這一層就逐步被各類前端技術(shù)所替代,如AngularJS、React等。

所以現(xiàn)在的Java服務(wù)端絕大部分情況下只是在處理M(Model)+C(Controller)的邏輯,而從概念上來看,好像Model代表的就是數(shù)據(jù)模型、而C則是一種控制層邏輯,所以很多人(甚至包括一些寫了很多年Java代碼的人)有時候都會被這個概念所迷惑而在Model和Controller層之間搖擺不定,在這里我們需要明確MVC模式中的M不僅僅代表的是數(shù)據(jù)模型,而是包括了數(shù)據(jù)模型之內(nèi)的所有業(yè)務(wù)邏輯相關(guān)的代碼,而C則是比較輕的,它被賦予只有處理輸入/輸出參數(shù)以及對該請求進行邏輯流程控制的職能,如果你的代碼中對Controller層有過重的邏輯代碼侵入,要知道這是不符合MVC架構(gòu)規(guī)范的!

在MVC架構(gòu)定義中,由于M代表了所有業(yè)務(wù)邏輯相關(guān)的代碼,所以M是要重點設(shè)計和規(guī)范的,其代碼的結(jié)構(gòu)和規(guī)范直接決定了軟件的可維護性及質(zhì)量,從本質(zhì)上來說就是如何進行"代碼結(jié)構(gòu)+軟件設(shè)計原則+設(shè)計模式"的組合運用。當(dāng)然上面只是一句話,而其內(nèi)涵則是一件非常考驗編程水平的事情。關(guān)于軟件設(shè)計原則+設(shè)計模式的內(nèi)容非常豐富也需要時間+經(jīng)驗的積累!而代碼結(jié)構(gòu)則是可以通過一定規(guī)范進行約定,結(jié)合Spring MVC框架至少我們可以寫出層次結(jié)構(gòu)盡可能一致的代碼!

二、應(yīng)用分層怎么搞?

事實上關(guān)于Java如何規(guī)范開發(fā)的問題,不同公司的規(guī)范略有不同,不過作為國內(nèi)Java語言應(yīng)用最為廣泛的公司——阿里巴巴發(fā)布的《阿里巴巴Java開發(fā)手冊》中對應(yīng)用的分層結(jié)構(gòu)已經(jīng)做了比較合理的劃分!這里作者并不想標新立異,只是在此基礎(chǔ)上做更為詳細的解釋和說明從而讓使用Spring MVC框架的同學(xué)能夠更好地明確其分層的對應(yīng)關(guān)系!

分層結(jié)構(gòu)

以下分層結(jié)構(gòu)基于Spring MVC框架,總體上與阿里巴巴開發(fā)手冊應(yīng)用分層方式一致,分層結(jié)構(gòu)示意圖如下:

寫了10年JAVA代碼,為何還是給人一種亂糟糟的感覺?

在基于Spring MVC框架的開發(fā)中,Controller層作為服務(wù)的入口主要承擔(dān)接收和轉(zhuǎn)換由終端層或者其他服務(wù)發(fā)送的網(wǎng)絡(luò)請求,并將其轉(zhuǎn)化為Java數(shù)據(jù)對象,然后對數(shù)據(jù)對象進行參數(shù)合法性校驗(如字段長度、類型、數(shù)值的合法性等等)。之后通過在Controller依賴注入對應(yīng)Service層服務(wù)接口,并進行業(yè)務(wù)邏輯層方法調(diào)用,如果業(yè)務(wù)邏輯并不復(fù)雜(是否復(fù)雜判斷標準可通過方法代碼行數(shù)、條件邏輯復(fù)雜度以及站在旁者角度看看是否便于維護等指標進行判斷)那么可以直接操作數(shù)據(jù)庫持久層完成業(yè)務(wù)邏輯;而如果Service層方法寫著寫著發(fā)現(xiàn)非常的多,邏輯條件也比較多,并且每個條件所需要處理的代碼量超過一定的規(guī)模,那么此時你就要考慮是否需要要對該方法進行優(yōu)化了!

而關(guān)于優(yōu)化的方式依據(jù)邏輯的復(fù)雜程度可以做不同等級的拆分,例如簡單點可以拆分一個私有方法處理該方法中的某一部分邏輯,從而減少主業(yè)務(wù)方法的代碼量。而如果該業(yè)務(wù)層方法后面對應(yīng)的是一個龐大的邏輯,例如在交易支付系統(tǒng)中,Controller層定義了一個支付的入口服務(wù),而進入Service層方法后根據(jù)不同的業(yè)務(wù)接入方、不同的支付方式及支付渠道,都需要進行大量不同邏輯的處理,那么此時就需要考慮對這些不同場景的業(yè)務(wù)邏輯進行類級別的拆分,如通過工廠模式拆分不同的支付渠道處理類邏輯,而對于公共的處理邏輯則可以通過抽象類定義抽象方法進行抽象。例如私有方法拆分代碼示例:

@Override
public SearchCouponNameBO searchCouponNameList(SearchCouponNameDTO searchCouponNameDTO) {
    SearchCouponNameBO searchCouponNameBO = SearchCouponNameBO.builder().total(0).build();
    SearchResult searchResult;
    try {
        BoolQueryCondition boolQueryCondition = searchCouponNameListConditionBuild(searchCouponNameDTO);
        SearchBuilderConstructor searchBuilderConstructor = new SearchBuilderConstructor(boolQueryCondition);
        searchBuilderConstructor.addFieldSort("id", SortOrderEnum.DESC);
        searchBuilderConstructor.setFrom(searchCouponNameDTO.getOffset());
        searchBuilderConstructor.setSize(searchCouponNameDTO.getLimit());
        searchResult = salesCouponEsMapper.selectCouponNameByCondition(searchBuilderConstructor);
    } catch (Exception e) {
        throw new SalesCouponNameException(SalesCouponNameErrorCode.COUPON_NAME_ES_QUERY_ERROR.getCode(),
                SalesCouponNameErrorCode.COUPON_NAME_ES_QUERY_ERROR.getMessage(),
                searchCouponNameDTO);
    }
    if (searchResult != null && searchResult.getHits().getHits().length > 0) {
        List<Integer> idList = getIdListFromEsSearchResult(searchResult);
        List<SalesCouponNamePO> salesCouponNamePOList = salesCouponNameMapper.selectByIdList(idList);
        List<SalesCouponNameBO> couponNameBOList = SalesCouponNameConvert.INSTANCE
                .convertCouponNameBOList(salesCouponNamePOList);
        searchCouponNameBO.setList(couponNameBOList);
        searchCouponNameBO.setTotal((int) searchResult.getTotalHits());
    }
    return searchCouponNameBO;
}

在該Service入口方法中,需要根據(jù)從ES查詢的分頁ID去真實的MySQL中進行數(shù)據(jù)獲?。‥S數(shù)據(jù)存儲不全,只是為了進行優(yōu)化性能將分頁邏輯放入ES),而在處理ES數(shù)據(jù)時,需要從ES數(shù)據(jù)結(jié)果集中抽象ID列表,對于這部分邏輯出于代碼量的考慮,這里我們抽象一個Service層私有方法,如:

private List<Integer> getIdListFromEsSearchResult(SearchResult searchResult) {
    SearchHit[] searchHits = searchResult.getHits().getHits();
    List<Integer> idList = Arrays.asList(searchHits).stream().map(SearchHit::getSourceAsMap)
            .map(o -> Integer.parseInt(String.valueOf(o.get("id"))))
            .collect(Collectors.toList());
    return idList;
}

以上代碼示例,本質(zhì)上是一種最簡單的方法抽象(別的語言叫函數(shù)),如果在代碼量略大,但是邏輯本身復(fù)雜度還不是特別高的情況下,這種方式是最常用的!也是在你不知道怎么拆分,讓代碼不那么難以維護的一種非常有效的手段。

而工廠+責(zé)任鏈等也是業(yè)務(wù)層拆分常用的手段,此時需要基于Service層業(yè)務(wù)入口方法進行代碼結(jié)構(gòu)的二次拆分,在分層結(jié)構(gòu)上這部分介于Service層和Dao層之間的代碼稱之為通用業(yè)務(wù)處理層(Manager)。關(guān)于這部分由于可以發(fā)揮空間非常大,很難有一套標準的答案,但作為一名優(yōu)秀的程序設(shè)計者要時刻有抽象的思維,不管拆分得是否足夠合理,至少要讓你的代碼不至于過于臃腫!這里我們將Service層拆分層次定義為以下三個等級:

  • 等級1:私有方法拆分;
  • 等級2:工廠+責(zé)任鏈運用(有效的類的拆分);
  • 等級3:高級設(shè)計模式(優(yōu)雅的類的拆分);
分層領(lǐng)域模型約定

聊完分層結(jié)構(gòu)接下來我們說一下分層領(lǐng)域數(shù)據(jù)模型的約定,注意這里的分層領(lǐng)域并不是指“DDD(領(lǐng)域驅(qū)動設(shè)計)模式”,而是對以上分層結(jié)構(gòu)中各層之間交互數(shù)據(jù)對象的定義約定。在上述分層結(jié)構(gòu)圖中已經(jīng)標識了DTO、BO、PO的使用范圍(本規(guī)范只約定三種領(lǐng)域?qū)ο螅聦嵣弦呀?jīng)足夠,并不需要搞的太復(fù)雜)。具體如下:

寫了10年JAVA代碼,為何還是給人一種亂糟糟的感覺?

在Controller層接收網(wǎng)絡(luò)請求數(shù)據(jù)后,由于Controller層并不需要處理額外的邏輯,所以大部分情況下直接將DTO對象傳送給Service層;而Service層如果邏輯不復(fù)雜只是需要根據(jù)DTO的數(shù)據(jù)進行數(shù)據(jù)庫操作,那么此時根據(jù)需要將DTO轉(zhuǎn)換為PO進行操作,完成后由于大部分場景下Service的輸出參數(shù)與輸入DTO對象都存在差異,因此為了區(qū)分我們將Service層的輸出數(shù)據(jù)對象統(tǒng)一定義為BO。

而Service層拆分時對于Manager層方法的輸入/輸出對象則統(tǒng)一為BO,包括Manager層操作第三方數(shù)據(jù)接口的數(shù)據(jù)對象轉(zhuǎn)換也統(tǒng)一為BO。以上劃分并沒有什么特別的強制約定,而過分人為的去揣摩其含義本質(zhì)上也沒什么意義,只是大家共同遵守一個約定,這樣代碼風(fēng)格看起來會更加統(tǒng)一一點。

三、如何保持代碼的簡潔性

作為一名對代碼有追求的程序員,能少些一行代碼就絕對不要啰嗦,而Java豐富的開源生態(tài)體系也給了我們這種懶惰很多便利,所以在編程的過程中其實是有很多工具可以幫助節(jié)省代碼的。這里給大家分別介紹三種方式:

MapStruct

在前面介紹的分層結(jié)構(gòu)中,無論是DTO到BO,還是BO到PO亦或BO到BO,都會有很多的數(shù)據(jù)對象轉(zhuǎn)換的邏輯,傳統(tǒng)的方法是需要通過一堆Setter方法來完成的,而高級一點的lombok包提供的@Builder注解也是需要你寫一堆".build()"來完成數(shù)據(jù)的轉(zhuǎn)換,這樣的代碼寫到Service層中顯然很浪費很多代碼行,而MapStruct是一種更優(yōu)雅的完成這件事的工具,使用方法如下:

項目pom.xml中引入依賴:

<!--MapStruct Java實體映射工具依賴-->
<dependency>
    <groupId>org.mapstruct</groupId>
    <artifactId>mapstruct-jdk8</artifactId>
    <version>1.3.1.Final</version>
</dependency>
<dependency>
    <groupId>org.mapstruct</groupId>
    <artifactId>mapstruct-processor</artifactId>
    <version>1.3.1.Final</version>
</dependency>

也需要在pom.xml引入一下Maven插件:

<!--提供給MapStruct使用 -->
<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-compiler-plugin</artifactId>
</plugin>

之后編寫數(shù)據(jù)對象映射轉(zhuǎn)換接口:

package com.mafengwo.sales.sp.coupon.convert;

import com.mafengwo.sales.sp.coupon.client.bo.SalesCouponChannelBO;
import com.mafengwo.sales.sp.coupon.client.dto.SalesCouponChannelsDTO;
import com.mafengwo.sales.sp.coupon.dao.model.SalesCouponChannelsPO;
import java.util.List;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.Mappings;
import org.mapstruct.factory.Mappers;

/**
 * @author qiaojiang
 */
@Mapper
public interface SalesCouponChannelsConvert {

    SalesCouponChannelsConvert INSTANCE = Mappers.getMapper(SalesCouponChannelsConvert.class);

    @Mappings({
            @Mapping(target = "flag", expression = "java(java.lang.Integer.valueOf(\"0\"))"),
            @Mapping(target = "ctime", expression = "java(com.mafengwo.sales.sp.coupon.util.DateUtils.getCurrentTimestamp())"),
            @Mapping(target = "mtime", expression = "java(com.mafengwo.sales.sp.coupon.util.DateUtils.getCurrentTimestamp())")
    })
    SalesCouponChannelsPO convertSalesCouponChannelsPO(SalesCouponChannelsDTO salesCouponChannelsDTO);

    @Mappings({})
    List<SalesCouponChannelBO> convertCouponChannelBOList(List<SalesCouponChannelsPO> salesCouponChannelsPO);
}

以上方法的入?yún)樵磾?shù)據(jù)對象,而返回對象則為目標數(shù)據(jù)對象,如果兩個對象的字段名稱完成一致,那么其實是不需要進行任何單獨映射的,直接 @Mappings({})即可;而如果映射對象之間字段名稱有差異則可以通過@Mappings({@Mapping(target = "ctime", source = "createTime")})進行指定映射。而在業(yè)務(wù)層方法具體操作時使用方法如下:

//實體數(shù)據(jù)轉(zhuǎn)換
SalesCouponChannelsPO salesCouponChannelsPO = SalesCouponChannelsConvert.INSTANCE
        .convertSalesCouponChannelsPO(salesCouponChannelsDTO);

這樣對象數(shù)據(jù)之間的拷貝將變得非常容易,從某種層面上看無論代碼層次結(jié)構(gòu)多么繞,至少數(shù)據(jù)對象之間的拷貝將不再是一件麻煩的事!

lambada表達式

在Java8種提供了lambada表達式,在Java8中如果操作List相關(guān)數(shù)據(jù)結(jié)構(gòu),如果能夠使用lambada表達式也可以省一些代碼,例如:

private List<Integer> getIdListFromEsSearchResult(SearchResult searchResult) {
    SearchHit[] searchHits = searchResult.getHits().getHits();
    List<Integer> idList = Arrays.asList(searchHits).stream().map(SearchHit::getSourceAsMap)
            .map(o -> Integer.parseInt(String.valueOf(o.get("id"))))
            .collect(Collectors.toList());
    return idList;
}

有關(guān)lambada表達式更多的用法,大家有時間可以多看看相關(guān)語法知識,這里就不再贅述!

tk.mybatis

在使用Mybatis框架作為數(shù)據(jù)庫開發(fā)框架時,相比較于Hibernate或其他JPA框架,Mybatis具有較強的對原生SQL的支持能力,因而會顯得比較靈活。但在大部分互聯(lián)網(wǎng)系統(tǒng)中,對數(shù)據(jù)庫的操作很多時候都是單表的操作,在這種情況下使用Mybatis也需要在Mapper代碼和映射.xml文件中編寫大量的SQL,而這些單表SQL本質(zhì)上大同小異,完全可以通用化。

因此在Mybatis領(lǐng)域為了減少開發(fā)量很多項目會使用mybatis-generator插件生成一份完整的映射代碼,但是這樣的方式也會增加大量的無用代碼,看起來并不是那么的簡潔。而tk.mybatis則是考慮到了這個問題,可以兼顧對單表操作的便捷性(不需要再寫額外的代碼)、多表聯(lián)合查詢的靈活性以及代碼的簡潔性。具體用法如下:

項目pom.xml文件引入相關(guān)依賴:

<!--Mybatis通用Mapper集成-->
<dependency>
    <groupId>tk.mybatis</groupId>
    <artifactId>mapper-spring-boot-starter</artifactId>
    <version>2.1.3</version>
    <exclusions>
        <exclusion>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis</artifactId>
        </exclusion>
        <exclusion>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis-spring</artifactId>
        </exclusion>
    </exclusions>
</dependency>

<dependency>
    <groupId>tk.mybatis</groupId>
    <artifactId>mapper</artifactId>
    <version>4.1.3</version>
</dependency>

主類@MapperScan注解換成tk.mybatis的:

import org.springframework.boot.SpringApplication;
import org.springframework.boot.actuate.autoconfigure.elasticsearch.ElasticSearchRestHealthIndicatorAutoConfiguration;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.servlet.ServletComponentScan;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
//不要使用Mybatis原生注解,用tk.mybatis的
import tk.mybatis.spring.annotation.MapperScan;

import java.util.Date;

@SpringBootApplication(exclude = {ElasticSearchRestHealthIndicatorAutoConfiguration.class})
@ServletComponentScan
@EnableDiscoveryClient
@EnableWebMvc
@MonitorEnableAutoConfiguration
@MapperScan("com.mafengwo.sales.sp.coupon.dao.mapper")
@EnableTransactionManagement
public class SpCouponApplication {

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

編寫映射接口,單表操作將不再需要額外定義操作方法及映射SQL代碼,而是可以直接用tk.mybatis提供的通用方法,代碼如下:

import com.mafengwo.sales.sp.coupon.dao.model.CouponNameScopeRelationPO;
import org.springframework.stereotype.Repository;
import tk.mybatis.mapper.common.Mapper;

@Repository
public interface CouponNameScopeRelationMapper extends Mapper<CouponNameScopeRelationPO> {

}

而在Mybatis SQL映射文件*.xml中單表也只需要定義簡單的字段映射即可,而不在需要定義通篇的SQL代碼了,如下:

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.mafengwo.sales.sp.coupon.dao.mapper.SalesCouponChannelsMapper">
    <resultMap id="BaseResultMap" type="com.mafengwo.sales.sp.coupon.dao.model.SalesCouponChannelsPO">
        <id column="ID" property="id" jdbcType="INTEGER"/>
        <result column="NAME" property="name" jdbcType="VARCHAR"/>
        <result column="DESC" property="desc" jdbcType="VARCHAR"/>
        <result column="ADMIN_UID" property="adminUid" jdbcType="INTEGER"/>
        <result column="FLAG" property="flag" jdbcType="INTEGER"/>
        <result column="CTIME" property="ctime" jdbcType="TIMESTAMP"/>
        <result column="MTIME" property="mtime" jdbcType="TIMESTAMP"/>
        <result column="SCENEID" property="sceneId" jdbcType="INTEGER"/>
    </resultMap>
</mapper>

除以上工具外,在實際的開發(fā)過程中還有很多開源或通過自定義組件的方式能夠讓代碼寫的更簡潔,大家可以保持探索!

四、Java程序設(shè)計原則與設(shè)計模式

構(gòu)建復(fù)雜的軟件系統(tǒng)只有遵循一定的設(shè)計原則并合適地運用相應(yīng)地設(shè)計模式,這樣的代碼才不至于在復(fù)雜的邏輯中迷失方向。關(guān)于設(shè)計原則及設(shè)計模式的話題是一個需要時間打磨和反復(fù)歷練的修行,因此這里只是為大家簡單陳列,在Java程序設(shè)計時應(yīng)該遵循的一些原則以及可用的設(shè)計原則,做到心中有劍!

設(shè)計原則

單一職責(zé)(一個蘿卜一個坑)、里氏替換(繼承復(fù)用)、依賴倒置(面向接口編程)、接口隔離(高內(nèi)聚、低耦合)、迪米特法則(降低類與類之間的耦合)、開閉原則(對擴展開發(fā)、對修改關(guān)閉)。

設(shè)計模式

在Java領(lǐng)域,大概有23種設(shè)計模式,它們分別是:

  • 創(chuàng)建型模式:單例模式、抽象工廠模式、建造者模式、工廠模式、原型模式
  • 結(jié)構(gòu)型模式:適配器模式、橋接模式、裝飾模式、組合模式、外觀模式、享元模式、代理模式
  • 行為型模式:模板方法模式、命令模式、迭代器模式、觀察者模式、中介者模式、備忘錄模式、解釋器模式、狀態(tài)模式、策略模式

以上這些模式或多或少在我們?nèi)粘5木幊讨卸紩姷交蛘呗犨^,但在平時能夠用到的卻并不多,很多原因在于目前Java領(lǐng)域的開發(fā)框架如Spring已經(jīng)給我們做了很多的限定,而在大部分互聯(lián)網(wǎng)系統(tǒng)中,編程模式又很固定。在多數(shù)情況下,工廠模式的運用就能搞定大多數(shù)業(yè)務(wù)編程場景,因此很多模式只有在很多中間件系統(tǒng)等基礎(chǔ)軟件中被使用得比較多。通過羅列上述設(shè)計模式,并不是要大家為了設(shè)計而生硬的使用設(shè)計模式,而是要努力向著“心中有丘壑,眉目作山河”目標境界前進!只有這樣才能不至于日復(fù)一日的碼磚生涯中,迷失自我,失去方向!

后記

隨著時光的流逝,越來越多的程序員步入中年,寫了10多年代碼的人也越來越多,而行業(yè)的發(fā)展卻在走下坡路,種種因素讓越來越多的人感到焦慮!個人覺得作為一名程序員,我們的核心能力還在于代碼,因此在日復(fù)一日的碼磚生涯中不斷修煉自己的代碼能力才是關(guān)鍵!否則可能就會出現(xiàn)被年輕人鄙視了!

向AI問一下細節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI