<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
說到 DDD,繞不開 MVC,在 MVC 三層架構中,我們進行功能開發的之前,拿到需求,解讀需求。往往最先做的一步就是先設計表結構,在逐層設計上層 dao,service,controller。對於產品或者使用者的需求都做了一層自我理解的轉化。
使用者需求在被提出之後經過這麼多層的轉化後,特別是研發需求在資料庫結構這一層轉化後,將業務以主觀臆斷行為進行了轉化。一旦業務邊界劃分模糊,考慮不全,大量的邏輯補充堆積到了程式碼層實現,變得越來越難維護。
假如我們現在要做一個電商訂單下單的需求,涉及到使用者選定商品,下訂單、支付訂單、對使用者下單時的訂單發貨:
DDD 整體作用總結如下:
嚴格分層架構:某層只能與直接位於的下層發生耦合。
鬆散分層架構:允許上層與任意下層發生耦合。
在領域驅動設計(DDD)中採用的是鬆散分層架構,層間關係不那麼嚴格。每層都可能使用它下面所有層的服務,而不僅僅是下一層的服務。每層都可能是半透明的,這意味著有些服務只對上一層可見,而有些服務對上面的所有層都可見。
分層的作用,從上往下:
在設計和開發時,不要將本該放在領域層的業務邏輯放到應用層中實現,因為龐大的應用層會使領域模型失焦,時間一長你的服務就會演化為傳統的三層架構,業務邏輯會變得混亂。
每一層都有自己特定的資料,可以做如下區分:
學習 DDD 前,有很多基礎概念需要掌握,這幅圖總結的很全,他把 DDD 劃分不同的層級:
在研究和解決業務問題時,DDD 會按照一定的規則將業務領域進行細分,當領域細分到一定的程度後,DDD 會將問題範圍限定在特定的邊界內,在這個邊界內建立領域模型,進而用程式碼實現該領域模型,解決相應的業務問題。簡言之,DDD 的領域就是這個邊界內要解決的業務問題域。
領域可以進一步劃分為子領域。我們把劃分出來的多個子領域稱為子域,每個子域對應一個更小的問題域或更小的業務範圍。
領域的核心思想就是將問題域逐級細分,來降低業務理解和系統實現的複雜度。通過領域細分,逐步縮小服務需要解決的問題域,構建合適的領域模型。
舉個簡單的例子,對於保險領域,我們可以把保險細分為承保、收付、再保以及理賠等子域,而承保子域還可以繼續細分為投保、保全(壽險)、批改(財險)等子子域。
子域可以根據重要程度和功能屬性劃分為如下:
核心域、支撐域和通用域的主要目標:通過領域劃分,區分不同子域在公司內的不同功能屬性和重要性,從而公司可對不同子域採取不同的資源投入和建設策略,其關注度也會不一樣。
很多公司的業務,表面看上去相似,但商業模式和戰略方向是存在很大差異的,因此公司的關注點會不一樣,在劃分核心域、通用域和支撐域時,其結果也會出現非常大的差異。
比如同樣都是電商平臺的淘寶、天貓、京東和蘇寧易購,他們的商業模式是不同的。淘寶是 C2C 網站,個人賣家對個人買家,而天貓、京東和蘇寧易購則是 B2C 網站,是公司賣家對個人買家。即便是蘇寧易購與京東都是 B2C 的模式,蘇寧易購是典型的傳統線下賣場轉型成為電商,京東則是直營加部分平臺模式。
因此,在公司建立領域模型時,我們就要結合公司戰略重點和商業模式,重點關注核心域。
通用語言是團隊統一的語言,不管你在團隊中承擔什麼角色,在同一個領域的軟體生命週期裡都使用統一的語言進行交流。那麼,通用語言的價值也就很明瞭,它可以解決交流障礙這個問題,使領域專家和開發人員能夠協同合作,從而確保業務需求的正確表達。
這個通用語言到場景落地,大家可能還很模糊,其實就是把領域物件、屬性、程式碼模型物件等,通過程式碼和文字建立對映關係,可以通過 Excel 記錄這個關係,這樣研發可以通過程式碼知道這個含義,產品或者業務方可以通過文字知道這個含義,溝通起來就不會有歧義,說的簡單一點,其實就是統一產品和研發的話術。
直接看下面這幅圖(來源於極客時間歐創新的 DDD 實戰課):
通用語言也有它的上下文環境,為了避免同樣的概念或語意在不同的上下文環境中產生歧義,DDD 在戰略設計上提出了“限界上下文”這個概念,用來確定語意所在的領域邊界。
限界上下文是一個顯式的語意和語境上的邊界,領域模型便存在於邊界之內。邊界內,通用語言中的所有術語和片語都有特定的含義。把限界上下文拆解開看,限界就是領域的邊界,而上下文則是語意環境。
通過領域的限界上下文,我們就可以在統一的領域邊界內用統一的語言進行交流。
實體 = 唯一身份標識 + 可變性【狀態 + 行為】
DDD 中要求實體是唯一的且可持續變化的。意思是說在實體的生命週期內,無論其如何變化,其仍舊是同一個實體。唯一性由唯一的身份標識來決定的。可變性也正反映了實體本身的狀態和行為。
實體以 DO(領域物件)的形式存在,每個實體物件都有唯一的 ID。我們可以對一個實體物件進行多次修改,修改後的資料和原來的資料可能會大不相同。
但是,由於它們擁有相同的 ID,它們依然是同一個實體。比如商品是商品上下文的一個實體,通過唯一的商品 ID 來標識,不管這個商品的資料如何變化,商品的 ID 一直保持不變,它始終是同一個商品。
值物件 = 將一個值用物件的方式進行表述,來表達一個具體的固定不變的概念。
當你只關心某個物件的屬性時,該物件便可作為一個值物件。我們需要將值物件看成不變物件,不要給它任何身份標識,還應該儘量避免像實體物件一樣的複雜性。
還是舉個訂單的例子,訂單是一個實體,裡面包含地址,這個地址可以只通過屬性嵌入的方式形成的訂單實體物件,也可以將地址通過 json 序列化一個 string 型別的資料,存到 DB 的一個欄位中,那麼這個 Json 串就是一個值物件,是不是很好理解?
下面給個簡單的圖(同樣是源於極客時間歐創新的 DDD 實戰課):
聚合:我們把一些關聯性極強、生命週期一致的實體、值物件放到一個聚合裡。聚合是領域物件的顯式分組,旨在支援領域模型的行為和不變性,同時充當一致性和事務性邊界。
聚合有一個聚合根和上下文邊界,這個邊界根據業務單一職責和高內聚原則,定義了聚合內部應該包含哪些實體和值物件,而聚合之間的邊界是鬆耦合的。按照這種方式設計出來的服務很自然就是“高內聚、低耦合”的。
聚合在 DDD 分層架構裡屬於領域層,領域層包含了多個聚合,共同實現核心業務邏輯。跨多個實體的業務邏輯通過領域服務來實現,跨多個聚合的業務邏輯通過應用服務來實現。
比如有的業務場景需要同一個聚合的 A 和 B 兩個實體來共同完成,我們就可以將這段業務邏輯用領域服務來實現;而有的業務邏輯需要聚合 C 和聚合 D 中的兩個服務共同完成,這時你就可以用應用服務來組合這兩個服務。
如果把聚合比作組織,那聚合根就是這個組織的負責人。聚合根也稱為根實體,它不僅是實體,還是聚合的管理者。
上面講的還是有些抽象,下面看一個圖就能很好理解(同樣是源於極客時間歐創新的 DDD 實戰課):
簡單概括一下:
當一些邏輯不屬於某個實體時,可以把這些邏輯單獨拿出來放到領域服務中,理想的情況是沒有領域服務,如果領域服務使用不恰當,慢慢又演化回了以前邏輯都在 service 層的局面。
可以使用領域服務的情況:
應用層作為展現層與領域層的橋樑,是用來表達用例和使用者故事的主要手段。
應用層通過應用服務介面來暴露系統的全部功能。在應用服務的實現中,它負責編排和轉發,它將要實現的功能委託給一個或多個領域物件來實現,它本身只負責處理業務用例的執行順序以及結果的拼裝。通過這樣一種方式,它隱藏了領域層的複雜性及其內部實現機制。
應用層相對來說是較“薄”的一層,除了定義應用服務之外,在該層我們可以進行安全認證,許可權校驗,持久化事務控制,或者向其他系統發生基於事件的訊息通知,另外還可以用於建立郵件以傳送給客戶等。
領域事件 = 事件釋出 + 事件儲存 + 事件分發 + 事件處理。
領域事件是一個領域模型中極其重要的部分,用來表示領域中發生的事件。忽略不相關的領域活動,同時明確領域專家要跟蹤或希望被通知的事情,或與其他模型物件中的狀態更改相關聯。
下面簡單說明領域事件:
比如下訂單後,給使用者增長積分與贈送優惠券的需求。如果使用瀑布流的方式寫程式碼。一個個邏輯呼叫,那麼不同使用者,贈送的東西不同,邏輯就會變得又臭又長。
這裡的比較好的方式是,使用者下訂單成功後,釋出領域事件,積分聚合與優惠券聚合監聽訂單釋出的領域事件進行處理。
倉儲介於領域模型和資料模型之間,主要用於聚合的持久化和檢索。它隔離了領域模型和資料模型,以便我們關注於領域模型而不需要考慮如何進行持久化。
我們將暫時不使用的領域物件從記憶體中持久化儲存到磁碟中。當日後需要再次使用這個領域物件時,根據 key 值到資料庫查詢到這條記錄,然後將其恢復成領域物件,應用程式就可以繼續使用它了,這就是領域物件持久化儲存的設計思想。
是不是感覺這塊內容比較抽象?直接對著 Demo 學習吧,很多東西你就會豁然開朗。
專案劃分為使用者介面層、應用層、領域層和基礎服務層,每一層的程式碼結構都非常清晰,包括每一層 VO、DTO、DO、PO 的資料定義,對於每一層的公共程式碼,比如常數、介面等,都抽離到 ddd-common 中。
./ddd-application // 應用層 ├── pom.xml └── src └── main └── java └── com └── ddd └── applicaiton ├── converter │ └── UserApplicationConverter.java // 型別轉換器 └── impl └── AuthrizeApplicationServiceImpl.java // 業務邏輯 ./ddd-common ├── ddd-common // 通用類庫 │ ├── pom.xml │ └── src │ └── main │ └── java │ └── com │ └── ddd │ └── common │ ├── exception // 異常 │ │ ├── ServiceException.java │ │ └── ValidationException.java │ ├── result // 返回結果集 │ │ ├── BaseResult.javar │ │ ├── Page.java │ │ ├── PageResult.java │ │ └── Result.java │ └── util // 通用工具 │ ├── GsonUtil.java │ └── ValidationUtil.java ├── ddd-common-application // 業務層通用模組 │ ├── pom.xml │ └── src │ └── main │ └── java │ └── com │ └── ddd │ └── applicaiton │ ├── dto // DTO │ │ ├── RoleInfoDTO.java │ │ └── UserRoleDTO.java │ └── servic // 業務介面 │ └── AuthrizeApplicationService.java ├── ddd-common-domain │ ├── pom.xml │ └── src │ └── main │ └── java │ └── com │ └── ddd │ └── domain │ ├── event // 領域事件 │ │ ├── BaseDomainEvent.java │ │ └── DomainEventPublisher.java │ └── service // 領域介面 │ └── AuthorizeDomainService.java └── ddd-common-infra ├── pom.xml └── src └── main └── java └── com └── ddd └── infra ├── domain // DO │ └── AuthorizeDO.java ├── dto │ ├── AddressDTO.java │ ├── RoleDTO.java │ ├── UnitDTO.java │ └── UserRoleDTO.java └── repository ├── UserRepository.java // 領域倉庫 └── mybatis └── entity // PO ├── BaseUuidEntity.java ├── RolePO.java ├── UserPO.java └── UserRolePO.java ./ddd-domian // 領域層 ├── pom.xml └── src └── main └── java └── com └── ddd └── domain ├── event // 領域事件 │ ├── DomainEventPublisherImpl.java │ ├── UserCreateEvent.java │ ├── UserDeleteEvent.java │ └── UserUpdateEvent.java └── impl // 領域邏輯 └── AuthorizeDomainServiceImpl.java ./ddd-infra // 基礎服務層 ├── pom.xml └── src └── main └── java └── com └── ddd └── infra ├── config │ └── InfraCoreConfig.java // 掃描Mapper檔案 └── repository ├── converter │ └── UserConverter.java // 型別轉換器 ├── impl │ └── UserRepositoryImpl.java └── mapper ├── RoleMapper.java ├── UserMapper.java └── UserRoleMapper.java ./ddd-interface ├── ddd-api // 使用者介面層 │ ├── pom.xml │ └── src │ └── main │ ├── java │ │ └── com │ │ └── ddd │ │ └── api │ │ ├── DDDFrameworkApiApplication.java // 啟動入口 │ │ ├── converter │ │ │ └── AuthorizeConverter.java // 型別轉換器 │ │ ├── model │ │ │ ├── req // 入參 req │ │ │ │ ├── AuthorizeCreateReq.java │ │ │ │ └── AuthorizeUpdateReq.java │ │ │ └── vo // 輸出 VO │ │ │ └── UserAuthorizeVO.java │ │ └── web // API │ │ └── AuthorizeController.java │ └── resources // 系統設定 │ ├── application.yml │ └── resources // Sql檔案 │ └── init.sql └── ddd-task └── pom.xml ./pom.xml
包括 3 張表,分別為使用者、角色和使用者角色表,一個使用者可以擁有多個角色,一個角色可以分配給多個使用者。
create table t_user ( id bigint auto_increment comment '主鍵' primary key, user_name varchar(64) null comment '使用者名稱', password varchar(255) null comment '密碼', real_name varchar(64) null comment '真實姓名', phone bigint null comment '手機號', province varchar(64) null comment '使用者名稱', city varchar(64) null comment '使用者名稱', county varchar(64) null comment '使用者名稱', unit_id bigint null comment '單位id', unit_name varchar(64) null comment '單位名稱', gmt_create datetime default CURRENT_TIMESTAMP not null comment '建立時間', gmt_modified datetime default CURRENT_TIMESTAMP not null on update CURRENT_TIMESTAMP comment '修改時間', deleted bigint default 0 not null comment '是否刪除,非0為已刪除' )comment '使用者表' collate = utf8_bin; create table t_role ( id bigint auto_increment comment '主鍵' primary key, name varchar(256) not null comment '名稱', code varchar(64) null comment '角色code', gmt_create datetime default CURRENT_TIMESTAMP not null comment '建立時間', gmt_modified datetime default CURRENT_TIMESTAMP not null on update CURRENT_TIMESTAMP comment '修改時間', deleted bigint default 0 not null comment '是否已刪除' )comment '角色表' charset = utf8; create table t_user_role ( id bigint auto_increment comment '主鍵id' primary key, user_id bigint not null comment '使用者id', role_id bigint not null comment '角色id', gmt_create datetime default CURRENT_TIMESTAMP not null comment '建立時間', gmt_modified datetime default CURRENT_TIMESTAMP not null comment '修改時間', deleted bigint default 0 not null comment '是否已刪除' )comment '使用者角色關聯表' charset = utf8;
倉儲(資源庫)介於領域模型和資料模型之間,主要用於聚合的持久化和檢索。它隔離了領域模型和資料模型,以便我們關注於領域模型而不需要考慮如何進行持久化。
比如儲存使用者,需要將使用者和角色一起儲存,也就是建立使用者的同時,需要新建使用者的角色許可權,這個可以直接全部放到倉儲中:
public AuthorizeDO save(AuthorizeDO user) { UserPO userPo = userConverter.toUserPo(user); if(Objects.isNull(user.getUserId())){ userMapper.insert(userPo); user.setUserId(userPo.getId()); } else { userMapper.updateById(userPo); userRoleMapper.delete(Wrappers.<UserRolePO>lambdaQuery() .eq(UserRolePO::getUserId, user.getUserId())); } List<UserRolePO> userRolePos = userConverter.toUserRolePo(user); userRolePos.forEach(userRoleMapper::insert); return this.query(user.getUserId()); }
倉儲對外暴露的介面如下:
// 使用者領域倉儲 public interface UserRepository { // 刪除 void delete(Long userId); // 查詢 AuthorizeDO query(Long userId); // 儲存 AuthorizeDO save(AuthorizeDO user); }
基礎服務層不僅僅包括資源庫,與第三方的呼叫,都需要放到該層,Demo 中沒有該範例,我們可以看一個小米內部具體的實際專案,他把第三方的呼叫放到了 remote 目錄中:
我們有使用者和角色兩個實體,可以將使用者、角色和兩者關係進行聚合,然後使用者就是聚合根,聚合之後的屬性,我們稱之為“許可權”。
對於地址 Address,目前是作為欄位屬性儲存到 DB 中,如果對地址無需進行檢索,可以把地址作為“值物件”進行儲存,即把地址序列化為 Json 存,儲存到 DB 的一個欄位中。
public class AuthorizeDO { // 使用者ID private Long userId; // 使用者名稱 private String userName; // 真實姓名 private String realName; // 手機號 private String phone; // 密碼 private String password; // 使用者單位 private UnitDTO unit; // 使用者地址 private AddressDTO address; // 使用者角色 private List<RoleDTO> roles; }
Demo 中的領域服務比較薄,通過單位 ID 後去獲取單位名稱,構建單位資訊:
@Service public class AuthorizeDomainServiceImpl implements AuthorizeDomainService { @Override // 設定單位資訊 public void associatedUnit(AuthorizeDO authorizeDO) { String unitName = "武漢小米";// TODO: 通過第三方獲取 authorizeDO.getUnit().setUnitName(unitName); } }
我們其實可以把領域服務再進一步抽象,可以抽象出領域能力,通過這些領域能力去構建應用層邏輯,比如賬號相關的領域能力可以包括授權領域能力、身份認證領域能力等,這樣每個領域能力相對獨立,就不會全部揉到一個檔案中,下面是實際專案的領域層截圖:
領域事件 = 事件釋出 + 事件儲存 + 事件分發 + 事件處理。
這個 Demo 中,對領域事件的處理非常簡單,還是一個應用內部的領域事件,就是每次執行一次具體的操作時,把行為記錄下來。
Demo 中沒有記錄事件的庫表,事件的分發還是同步的方式,所以 Demo 中的領域事件還不完善,後面我會再繼續完善 Demo 中的領域事件,通過 Java 訊息機制實現解耦,甚至可以藉助訊息佇列,實現非同步。
/** * 領域事件基礎類別 * * @author louzai * @since 2021/11/22 */ @Getter @Setter @NoArgsConstructor public abstract class BaseDomainEvent<T> implements Serializable { private static final long serialVersionUID = 1465328245048581896L; /** * 發生時間 */ private LocalDateTime occurredOn; /** * 領域事件資料 */ private T data; public BaseDomainEvent(T data) { this.data = data; this.occurredOn = LocalDateTime.now(); } } /** * 使用者新增領域事件 * * @author louzai * @since 2021/11/20 */ public class UserCreateEvent extends BaseDomainEvent<AuthorizeDO> { public UserCreateEvent(AuthorizeDO user) { super(user); } }
/** * 領域事件釋出實現類 * * @author louzai * @since 2021/11/20 */ @Component @Slf4j public class DomainEventPublisherImpl implements DomainEventPublisher { @Autowired private ApplicationEventPublisher applicationEventPublisher; @Override public void publishEvent(BaseDomainEvent event) { log.debug("釋出事件,event:{}", GsonUtil.gsonToString(event)); applicationEventPublisher.publishEvent(event); } }
應用層就非常好理解了,只負責簡單的邏輯編排,比如建立使用者授權:
@Transactional(rollbackFor = Exception.class) public void createUserAuthorize(UserRoleDTO userRoleDTO){ // DTO轉為DO AuthorizeDO authorizeDO = userApplicationConverter.toAuthorizeDo(userRoleDTO); // 關聯單位單位資訊 authorizeDomainService.associatedUnit(authorizeDO); // 儲存使用者 AuthorizeDO saveAuthorizeDO = userRepository.save(authorizeDO); // 釋出使用者新建的領域事件 domainEventPublisher.publishEvent(new UserCreateEvent(saveAuthorizeDO)); }
查詢使用者授權資訊:
@Override public UserRoleDTO queryUserAuthorize(Long userId) { // 查詢使用者授權領域資料 AuthorizeDO authorizeDO = userRepository.query(userId); if (Objects.isNull(authorizeDO)) { throw ValidationException.of("UserId is not exist.", null); } // DO轉DTO return userApplicationConverter.toAuthorizeDTO(authorizeDO); }
細心的同學可以發現,我們應用層和領域層,通過 DTO 和 DO 進行資料轉換。
最後就是提供 API 介面:
@GetMapping("/query") public Result<UserAuthorizeVO> query(@RequestParam("userId") Long userId){ UserRoleDTO userRoleDTO = authrizeApplicationService.queryUserAuthorize(userId); Result<UserAuthorizeVO> result = new Result<>(); result.setData(authorizeConverter.toVO(userRoleDTO)); result.setCode(BaseResult.CODE_SUCCESS); return result; } @PostMapping("/save") public Result<Object> create(@RequestBody AuthorizeCreateReq authorizeCreateReq){ authrizeApplicationService.createUserAuthorize(authorizeConverter.toDTO(authorizeCreateReq)); return Result.ok(BaseResult.INSERT_SUCCESS); }
資料的互動,包括入參、DTO 和 VO,都需要對資料進行轉換。
新建庫表:通過檔案 "ddd-interface/ddd-api/src/main/resources/init.sql" 新建庫表。
修改 SQL 設定:修改 "ddd-interface/ddd-api/src/main/resources/application.yml" 的資料庫設定。
啟動服務:直接啟動服務即可。
測試用例:
請求 URL:http://127.0.0.1:8087/api/user/save
Post body:{"userName":"louzai","realName":"樓","phone":13123676844,"password":"***","unitId":2,"province":"湖北省","city":"鄂州市","county":"葛店開發區","roles":[{"roleId":2}]}
DDD Demo 程式碼已經上傳到 GitHub 中:
https://github.com/lml200701158/ddd-framewor
或者通過下面命令直接獲取:
git clone git@github.com:lml200701158/ddd-framework.git
最後,談談二哥對 DDD 的理解,我覺得 DDD 不像一門技術,我理解的技術比如高並行、快取、訊息佇列等,DDD 更像是一項軟技能,一種方法論,包含了很多設計理念。
大家不要認為,掌握了一些概念,以及 DDD 的基本思想,就掌握了 DDD,然後做專案時,照葫蘆畫瓢,這樣你會死的很慘!
只掌握 DDD 表面的東西,其實是不夠的,我覺得 DDD 最複雜的地方,其實是在它的領域設計部分,專案啟動前,你一定要設計各個領域物件,以及它們直接的互動關係。
比如我們之前做過一個專案,因為這塊沒有做好,大家一邊寫程式碼,一邊還在思考,這個領域物件該如何構造,嚴重影響開發效率,最後又不得不回退到 MVC 的模式。
不要為了炫技,啥都要搞個 DDD,兩者如何選擇:
以上就是DDD框架落地實戰的詳細內容,更多關於DDD框架的資料請關注it145.com其它相關文章!
相關文章
<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
综合看Anker超能充系列的性价比很高,并且与不仅和iPhone12/苹果<em>Mac</em>Book很配,而且适合多设备充电需求的日常使用或差旅场景,不管是安卓还是Switch同样也能用得上它,希望这次分享能给准备购入充电器的小伙伴们有所
2021-06-01 09:31:42
除了L4WUDU与吴亦凡已经多次共事,成为了明面上的厂牌成员,吴亦凡还曾带领20XXCLUB全队参加2020年的一场音乐节,这也是20XXCLUB首次全员合照,王嗣尧Turbo、陈彦希Regi、<em>Mac</em> Ova Seas、林渝植等人全部出场。然而让
2021-06-01 09:31:34
目前应用IPFS的机构:1 谷歌<em>浏览器</em>支持IPFS分布式协议 2 万维网 (历史档案博物馆)数据库 3 火狐<em>浏览器</em>支持 IPFS分布式协议 4 EOS 等数字货币数据存储 5 美国国会图书馆,历史资料永久保存在 IPFS 6 加
2021-06-01 09:31:24
开拓者的车机是兼容苹果和<em>安卓</em>,虽然我不怎么用,但确实兼顾了我家人的很多需求:副驾的门板还配有解锁开关,有的时候老婆开车,下车的时候偶尔会忘记解锁,我在副驾驶可以自己开门:第二排设计很好,不仅配置了一个很大的
2021-06-01 09:30:48
不仅是<em>安卓</em>手机,苹果手机的降价力度也是前所未有了,iPhone12也“跳水价”了,发布价是6799元,如今已经跌至5308元,降价幅度超过1400元,最新定价确认了。iPhone12是苹果首款5G手机,同时也是全球首款5nm芯片的智能机,它
2021-06-01 09:30:45