訂單優(yōu)惠計(jì)算是指買家選擇商品加入購(gòu)物車,交易系統(tǒng)根據(jù)會(huì)員等級(jí),會(huì)員資產(chǎn)(優(yōu)惠券/碼、積分、權(quán)益卡),商家優(yōu)惠活動(dòng),計(jì)算出訂單實(shí)際需要支付的金額。
在有贊零售業(yè)務(wù)板塊中,線上線下都有訂單優(yōu)惠計(jì)算場(chǎng)景。線上使用場(chǎng)景是買家在H5/小程序端選品加車、下單結(jié)算,中臺(tái)在這部分已經(jīng)有很充分的沉淀,所以主要使用中臺(tái)提供的能力實(shí)現(xiàn)。而在線下使用場(chǎng)景深度契合垂直行業(yè),業(yè)務(wù)場(chǎng)景比較特殊,不適合放在中臺(tái)去實(shí)現(xiàn),所以這部分能力由零售業(yè)務(wù)自己完成。
1.2 業(yè)務(wù)場(chǎng)景
在線下開單收銀場(chǎng)景,零售提供了多種客戶端供商家選擇,買家使用的端:門店小程序、自助收銀大屏版。商家收銀的端:PC 收銀(瀏覽器/桌面),Phone/Pad 收銀端等。
總結(jié)下零售線下場(chǎng)景優(yōu)惠計(jì)算的難點(diǎn)和痛點(diǎn):
1.3 前世
零售移動(dòng)端團(tuán)隊(duì)在每次營(yíng)銷項(xiàng)目迭代中,、iOS兩端小組都需要投入開發(fā)資源,影響團(tuán)隊(duì)整體的項(xiàng)目迭代效率。
于是,移動(dòng)端團(tuán)隊(duì)基于 開發(fā)了第一版跨平臺(tái)訂單優(yōu)惠計(jì)算,它統(tǒng)一了 /iOS 訂單本地優(yōu)惠計(jì)算和優(yōu)惠詳情展示的邏輯,還有動(dòng)態(tài)熱更的能力。
在后續(xù)迭代中,后端也希望能夠接入這套能力,并共建這套系統(tǒng),但是發(fā)現(xiàn)了有一些問題急需解決。
計(jì)算過程中依賴了共享全局變量,有并發(fā)問題,無法同時(shí)計(jì)算多筆訂單,對(duì)后端使用場(chǎng)景來說,雖然可以通過多個(gè)執(zhí)行引擎實(shí)例來實(shí)現(xiàn)并發(fā)安全的計(jì)算,但此方案實(shí)屬下策
沒有領(lǐng)域模型,營(yíng)銷活動(dòng)模型各不相同,實(shí)現(xiàn)的計(jì)算邏輯差異較大,導(dǎo)致代碼重用度不高
沒有設(shè)計(jì)活動(dòng)互斥,互斥邏輯是硬編碼在活動(dòng)處理類中的
訂單的數(shù)據(jù)結(jié)構(gòu)冗余,商品和活動(dòng)模型應(yīng)該是獨(dú)立的,但實(shí)際上商品模型下掛載了可以使用的活動(dòng),這樣即增加理解成本,又增加了數(shù)據(jù)序列化的開銷
沒有類型約束,開發(fā)起來,代碼提示全憑記憶,對(duì)于初次接觸該系統(tǒng)的人,代碼理解成本較高,開發(fā)新功能也束手束腳
處理邏輯繁瑣,在商品特別多的情況下,性能不太理想
二、新生2.1設(shè)計(jì)目標(biāo)
新的方案需要滿足以下幾種需求:
其實(shí),最重要的還是提升研發(fā)效率,相同的營(yíng)銷計(jì)算邏輯不需要在多端都開發(fā)一遍。
2.2 重構(gòu)還是重寫?
方案 1: 重構(gòu)活動(dòng)模型成本巨大,改動(dòng)貫穿所有文件,加上動(dòng)態(tài)語言一時(shí)爽。
方案 2: 從長(zhǎng)遠(yuǎn)看,用 重寫對(duì)后期開發(fā)效率提升會(huì)很大,同時(shí)也會(huì)大大降低代碼理解成本。?
簡(jiǎn)單介紹下 特點(diǎn):
2.3靜態(tài)類型玩得更好
在 這邊的泛型,經(jīng)過序列化之后,在 JS 反序列化得到的是普通對(duì)象,沒有了自身行為和類型約束。
當(dāng)然這不是語言層面的問題,但我們?nèi)匀豢梢栽O(shè)計(jì)得更完善。
我們可以通過合并對(duì)象的方式,讓對(duì)象實(shí)例既有數(shù)據(jù),又有行為和類型檢查。
2.4業(yè)務(wù)模型分析2.4.1 營(yíng)銷活動(dòng)模型
“滿 300 減 30、2 件 8折,3 件 7折、全場(chǎng) 100 元任選 3 件……”
其實(shí)營(yíng)銷活動(dòng)本身最核心的三個(gè)部分是:
仔細(xì)想想,對(duì)這個(gè)門檻和優(yōu)惠擴(kuò)展一下,然后組合起來,就是一個(gè)新的營(yíng)銷活動(dòng)玩法。
除此之外,營(yíng)銷活動(dòng)優(yōu)惠計(jì)算處理邏輯還有:
2.4.2 擴(kuò)展性
通過對(duì)營(yíng)銷活動(dòng)的模型分析,可以預(yù)見的是,未來營(yíng)銷活動(dòng)需求迭代,會(huì)出現(xiàn)以下幾種場(chǎng)景:
商家可以任意配置門檻和優(yōu)惠來創(chuàng)建活動(dòng),萬能的營(yíng)銷插件
商家可以任意配置優(yōu)惠活動(dòng)的使用順序和使用策略
增加優(yōu)惠方式,如現(xiàn)有抹零分為抹分、抹角、四舍五入到角,商家想要新增四舍、五入等
2.4.3 商品模型
商品本質(zhì)是一個(gè)純數(shù)據(jù)的模型,包含一些基本屬性:標(biāo)識(shí)符、類型、單位、數(shù)量、單價(jià)等,但是在實(shí)際開發(fā)過程中,需要為其增加自身能力。
2.4.4 活動(dòng)優(yōu)先級(jí)問題
將營(yíng)銷活動(dòng)的計(jì)算邏輯抽象成處理器響應(yīng)式網(wǎng)站靜態(tài)頁(yè)面多少錢,串聯(lián)起來使用,這樣的方式可以解決活動(dòng)優(yōu)先級(jí)問題,也比較適合我們的業(yè)務(wù)場(chǎng)景,可以很好地實(shí)現(xiàn)以下目標(biāo):
規(guī)范了活動(dòng)處理流程
活動(dòng)處理順序可配置化
活動(dòng)處理之間可以任意插入邏輯節(jié)點(diǎn)
在實(shí)際開發(fā)中,可以插入 2 個(gè) 「數(shù)據(jù)調(diào)整」 的處理器。
2.4.5 活動(dòng)互斥模型
活動(dòng)之間有一定的使用策略:疊加、互斥、選最優(yōu)。
目前的使用策略主要是由產(chǎn)品設(shè)計(jì)決定的,部分活動(dòng)互斥情況如下所示:
對(duì)于活動(dòng)之間的互斥關(guān)系,需要一個(gè)合適的數(shù)據(jù)結(jié)構(gòu)來存儲(chǔ),然后封裝起來,簡(jiǎn)化外部對(duì)其的使用。最終選擇使用無向圖來存儲(chǔ),在實(shí)際開發(fā)中,使用鄰接鏈表的方式實(shí)現(xiàn)。
無侵入的活動(dòng)互斥
為了避免活動(dòng)互斥的邏輯硬編碼在活動(dòng)處理類中,在執(zhí)行營(yíng)銷活動(dòng)計(jì)算的處理方法時(shí),排除掉了已經(jīng)參與互斥活動(dòng)的商品,這樣活動(dòng)處理器不用感知活動(dòng)互斥,只需要關(guān)心自己的處理邏輯。
大致代碼如下:
// 活動(dòng)互斥容器
class PromotionMutex {
test(a: PromotionType, b: PromotionType): boolean;
}
const promotionMutex = new PromotionMutex();
// 活動(dòng)優(yōu)惠計(jì)算處理
abstract class Processor<T> {
process({ skuWrappers }) {
// 獲取處理器關(guān)心的活動(dòng)類型
const type = this.getType()
// 迭代SKU列表, 篩選出可用的商品(沒有參與互斥活動(dòng))
const availableSkuList = skuWrappers.filter(
sku =>
!sku.allAvailablePlan.some(plan =>
promotionMutex.test(plan.type, type)
)
);
// 交給處理器
this._process({ availableSkuList });
}
}
2.5整體設(shè)計(jì)2.5.1 分層設(shè)計(jì)
輸入層
主要把外部傳入的數(shù)據(jù)做整理轉(zhuǎn)換。這部分是可選的,可以在 層就做好適配,不同的端可以通過擴(kuò)展 Entry來實(shí)現(xiàn)自己的處理。
核心計(jì)算層
構(gòu)建領(lǐng)域模型,實(shí)際是為輸入層的數(shù)據(jù)增加了自身能力的處理邏輯。如商品應(yīng)有的能力:使用改價(jià)價(jià)格、計(jì)算總價(jià)、拆分一部分?jǐn)?shù)量出來、應(yīng)用優(yōu)惠等
將合適的商品和活動(dòng)交給處理器,計(jì)算出優(yōu)惠結(jié)果
結(jié)果導(dǎo)出層
端不再需要做多余的模型轉(zhuǎn)換,減少了很多工作量。JS 這邊針對(duì)不同場(chǎng)景,數(shù)據(jù)直出。JS 做起來簡(jiǎn)單且合適(擁有所有數(shù)據(jù))
例如:移動(dòng)端需要的不僅僅是訂單優(yōu)惠詳情,還有移動(dòng)端兩端之間約定的渲染模板(什么地方用啥顏色,字體大小等)
通過擴(kuò)展輸入層和結(jié)果導(dǎo)出層,共享核心計(jì)算層的方式,滿足不同端的業(yè)務(wù)場(chǎng)景需求。
2.5.2 核心類圖
2.6細(xì)節(jié)設(shè)計(jì)2.6.0 寫在前面
總結(jié)下幾個(gè)設(shè)計(jì)原則
2.6.1 內(nèi)聚的模型
將核心邏輯放在對(duì)應(yīng)的模型上,模型聚焦自身能力,隱藏實(shí)現(xiàn)細(xì)節(jié),簡(jiǎn)化外部的使用。
這里舉幾個(gè)栗子:
// SKU的包裝類
class SkuWrapper {
// 應(yīng)用SKU級(jí)別的優(yōu)惠方案
applySkuPlan(skuPlan: SkuPlan);
// 計(jì)算SKU的總價(jià)
reCalcTotalPrice();
}
// 對(duì)一組商品參與活動(dòng)的統(tǒng)計(jì)
interface ItemStats {
// 數(shù)量
totalCount: number;
// 價(jià)格
totalPrice: number;
// 使用原價(jià)?
useOriginPrice: boolean;
// 可以參與商品的列表
suitableSkuList: SkuWrapper[];
// 源數(shù)據(jù)
sourceSkuLit: SkuWrapper[];
}
// 活動(dòng)門檻
class Condition {
// 包含 SKU
isContains(sku:Sku);
}
// 組合級(jí)活動(dòng)的門檻
class CombineCondition extends Condition {
// 是否滿足門檻
hasMeet(itemStats: ItemStats);
// 超過門檻多少倍
overTimes(itemStats:ItemStats);
// 還缺多少滿足門檻
calcRemainValue(itemStats: ItemStats);
}
// 活動(dòng)優(yōu)惠
class Preferential {
// 計(jì)算優(yōu)惠價(jià)格
calcPreferentialPrice(originPrice: number);
}
通過這些核心模型的設(shè)計(jì),處理一個(gè) SKU 級(jí)別活動(dòng)將變得非常簡(jiǎn)單,核心代碼不會(huì)超過 20行, 大致如下:
_process({ skuList, promotions }) {
// 迭代活動(dòng)
promotions.forEach(p => {
// 取出活動(dòng)門檻和優(yōu)惠
const {
conditionPreferentialPairs: [{ condition, preferential }]
} = p;
// 迭代SKU列表
skuList.forEach(sku => {
// 如果門檻包含SKU
if (condition.isContains(sku)) {
// 計(jì)算優(yōu)惠后的價(jià)格
const preferentialPrice = preferential.calcPreferentialPrice(
sku.salePrice
);
// 生成優(yōu)惠方案
const plan = {
preferentialPrice
// other properties
};
// 應(yīng)用SKU級(jí)別優(yōu)惠方案
sku.applySkuPlan(plan);
}
});
});
}
2.6.2 處理器抽象模板類
對(duì)于不同的活動(dòng),需要實(shí)現(xiàn)活動(dòng)處理模板類中的抽象方法:
關(guān)心的活動(dòng)類型
處理活動(dòng)數(shù)據(jù)(基礎(chǔ)信息 + 門檻 + 優(yōu)惠)到活動(dòng)泛型的映射
處理自身活動(dòng)泛型和商品,生成和應(yīng)用優(yōu)惠方案
abstract class Processor<T> {
abstract types(): PromotionType[];
abstract ownModelMappings(promotion: Promotion): T;
abstract _process(ctx: ProcessorContext<T>): void;
}
活動(dòng)模型的擴(kuò)展性:各個(gè)活動(dòng)總是有差異的,不需要全部按照一個(gè)固定的模型去設(shè)計(jì)。把通用的部分定義出來,允許出現(xiàn)特性,同時(shí)不會(huì)對(duì)外部傳入的數(shù)據(jù)做限制。
這里主要通過增加中間層來實(shí)現(xiàn)活動(dòng)模型的擴(kuò)展性。()會(huì)將數(shù)據(jù)封裝為自身所需的泛型,即使外部活動(dòng)的門檻或優(yōu)惠有變化,之前的計(jì)算邏輯也不用修改。
例如有這么一個(gè)場(chǎng)景:有門檻和優(yōu)惠關(guān)系是 1:1的活動(dòng) Foo,定義如下:
// 門檻和優(yōu)惠比例 1:1
{
condition: {type, value},
preferential: {type, value}
}
// 優(yōu)惠計(jì)算處理
class FooProcessor extends Processor<Foo> {
// 將數(shù)據(jù)轉(zhuǎn)換為活動(dòng)對(duì)應(yīng)的泛型
ownModelMappings(p: Promotion): Foo {
return new Foo(p);
}
_process({foo}){
// do sth
}
}
// 門檻模型
class Condition {
constructor(c) {
// 合并數(shù)據(jù)和行為
Object.assign(this, c);
}
isContains(sku:Sku);
}
class Foo {
constructor(p: Promotion) {
this.condition = new Condition(p.cs.condition)
}
}
需求變更為:多個(gè)門檻滿足一個(gè)即可享受優(yōu)惠。那么,其實(shí)只需要擴(kuò)展原有 的封裝方式,實(shí)際對(duì)原來的計(jì)算邏輯沒有任何影響。
// 門檻和優(yōu)惠變更為 n:1
{
conditions: [{type, value}, ...],
preferential: {type, value}
}
// 一個(gè)門檻滿足即可
const anyCondition = conditions => ({
isContains: s => conditions.some(c => new Condition(c).isContains(s))
});
class Foo {
constructor(p: Promotion) {
// 活動(dòng)門檻的匹配方式修改為 anyCondition 即可
this.condition = anyCondition(p.cs.conditions)
}
}
2.6.3 商品活動(dòng)匹配
一個(gè)商品能不能使用活動(dòng)的優(yōu)惠,主要有以下幾種匹配方式:
通過以上的幾種情況可以看出,如果純粹按照需求來開發(fā)這塊功能,會(huì)有很大的冗余。為了減少重復(fù)開發(fā)量,使用組合的方式來實(shí)現(xiàn)。
// 定義匹配函數(shù)
type Matcher = (condition: Condition, sku: Sku) => boolean;
// 原價(jià)才能使用
const originPriceMatcher = (condition: Condition, sku: Sku) => true
// SKU維度標(biāo)識(shí)匹配
const skuIdentityMatcher = (condition: Condition, { goodsId, skuId }: Sku) => false
// 組合匹配
const composeMatcher = (a: Matcher, b: Matcher): Matcher => (
condition: Condition,
sku: Sku
) => a(condition, sku) && b(condition, sku);
// SKU維度標(biāo)識(shí)匹配且使用原價(jià)
const originPriceWithSkuIdentityMatcher = composeMatcher(originPriceMatcher, skuIdentityMatcher)
2.6.4 性能優(yōu)化
對(duì)于系統(tǒng)的性能優(yōu)化,做了幾點(diǎn)微小的事:
以下是 iOS客戶端生產(chǎn)環(huán)境采集新老計(jì)算耗時(shí)的數(shù)據(jù)統(tǒng)計(jì)。為了避免影響觀感,去除了極端場(chǎng)景下老版本計(jì)算超時(shí)的記錄
2.6.5 測(cè)試覆蓋
開發(fā)一個(gè)項(xiàng)目,測(cè)試代碼是必須要有的,更何況是涉及到資產(chǎn),一定要穩(wěn)。
除了在開發(fā)功能階段編寫的單元測(cè)試,測(cè)試同學(xué)還提供了一系列核心用例,加上線上真實(shí)訂單計(jì)算場(chǎng)景的數(shù)據(jù),都補(bǔ)充到了集成測(cè)試當(dāng)中。
項(xiàng)目的測(cè)試率覆蓋如下圖:
三、后端計(jì)算場(chǎng)景3.1 運(yùn)行環(huán)境選型
J2V8 V8 高性能 引擎的 Java 封裝
JDK 內(nèi)置輕量級(jí)高性能 運(yùn)行環(huán)境 ?
基于不折騰和性能不差的原則,選擇了 JVM內(nèi)置的 引擎作為后端 運(yùn)行環(huán)境.
3.2 熱更新
后端服務(wù)感知到有新版本的 JS 發(fā)布,需要?jiǎng)?chuàng)建新的 ,并加載 JS 文件,然后通過靜態(tài)的訂單數(shù)據(jù)預(yù)熱,預(yù)熱結(jié)束后替換掉老的版本,對(duì)外提供服務(wù).
值得注意的是: 假如服務(wù)正在使用 處理計(jì)算,同時(shí)又有新版本發(fā)布,創(chuàng)建了新的,此時(shí)直接暴露出去使用,會(huì)導(dǎo)致腳本未加載完成的錯(cuò)誤。所以需要 所有準(zhǔn)備過程(創(chuàng)建, 加載腳本和預(yù)熱)封閉在工廠方法內(nèi),準(zhǔn)備階段完成,得到的就是完全可用的 。
3.3 版本發(fā)布
各端的版本發(fā)布流程大致相同:
將工程通過以區(qū)分Entry的方式進(jìn)行打包,并上傳至內(nèi)部文件服務(wù)器
在發(fā)布管理頁(yè)面操作,創(chuàng)建一個(gè)新的版本,綁定文件下載地址
將新的版本信息發(fā)布到配置中心
當(dāng)前環(huán)境的服務(wù)端感知到配置變化,去文件服務(wù)器拉取腳本
加載新版本到計(jì)算服務(wù)中,預(yù)熱,替換老版本,開始對(duì)外提供服務(wù)
當(dāng)前環(huán)境確認(rèn)服務(wù)穩(wěn)定,同步至下個(gè)環(huán)境。跳轉(zhuǎn)至 4
當(dāng)前環(huán)境服務(wù)不穩(wěn)定,通過配置中心歷史記錄回滾。跳轉(zhuǎn)至 4
3.4后續(xù)的挑戰(zhàn)3.4.1 支持校驗(yàn)不同版本的計(jì)算結(jié)果
對(duì)于不同版本腳本計(jì)算出來的結(jié)果,后端應(yīng)該用什么版本去校驗(yàn)?zāi)兀?/p>
不同版本的差異可能體現(xiàn)在以下幾個(gè)情況:
3.4.2 如何向前兼容
方案 1:最新版本兼容所有老版本,需要很多 -flag。歷史包袱會(huì)越來越重,維護(hù)成本太高了。靠人腦去維護(hù)版本的兼容是不可靠的
方案 2:服務(wù)端按需加載相應(yīng)版本在內(nèi)存中響應(yīng)式網(wǎng)站靜態(tài)頁(yè)面多少錢,使用請(qǐng)求對(duì)應(yīng)的版本計(jì)算。無歷史包袱,內(nèi)存占用會(huì)越來越大 ?
3.4.3 內(nèi)存壓力
先看看目前的 JS 文件大小和內(nèi)存占用情況,JS 編譯到 ES5 之后,文件大了一倍多。文件大小約187K
創(chuàng)建了 2 個(gè)計(jì)算引擎,加載完腳本占用內(nèi)存 22.2M。經(jīng)過粗略的計(jì)算, 本身占用約 3M,加載一個(gè) JS 計(jì)算腳本需要 7M 左右的內(nèi)存成本
3.4.4 目前 JS 腳本 的模塊分析
在 打包文件時(shí),可以通過 -- 插件,分析出各個(gè)模塊文件大小。統(tǒng)計(jì)如下:
文件占比大頭在 第三方包上,當(dāng)加載多個(gè)腳本時(shí),其實(shí)有很大的冗余,它們?cè)趦?nèi)存中的表現(xiàn)如下:
3.4.5 優(yōu)化
可以通過 作用域隔離 的方式,分離不同的版本。第三方依賴的包,是所有版本共享的。前提是后面依賴不會(huì)有變化,以訂單優(yōu)惠計(jì)算的業(yè)務(wù)來講,不會(huì)需要新的依賴了。
優(yōu)化之后,加載了 11 個(gè)版本。內(nèi)存占用 13M,除去 占用的 3M, 加載一個(gè) JS 計(jì)算腳只需不到 1M 內(nèi)存成本。
結(jié)合目前的各端發(fā)版周期和版本覆蓋率的情況來看,后端按需加載對(duì)應(yīng)版本,不會(huì)有太大的內(nèi)存壓力。
四、已經(jīng)遇到的問題4.1 版本管理
一個(gè)代碼庫(kù),多個(gè)平臺(tái)發(fā)布。一般開發(fā)新功能,先拉特性分支,開發(fā)結(jié)束后合并到 ,然后用 來發(fā)布版本。但是當(dāng)兩端的開發(fā)需求同時(shí)進(jìn)行,想要發(fā)布的內(nèi)容,時(shí)間節(jié)點(diǎn)也不一樣。那么代碼如何合并、發(fā)布就是個(gè)問題。目前采用的方式是,代碼仍然合并到 ,各端拉發(fā)布分支的方式去發(fā)布。有新的特性或者修復(fù),可以摘取過來,然后定期和 同步。缺點(diǎn)就是端的負(fù)責(zé)人需要關(guān)注新代碼的合并,需不需要合并到發(fā)布分支,有沒有沖突問題。這種方式只能算折中之舉,后續(xù)還需要繼續(xù)思考和探索如何處理會(huì)更好、更省事。
4.2 風(fēng)險(xiǎn)
收益與風(fēng)險(xiǎn)總是并存的。各客戶端統(tǒng)一的核心邏輯是:開發(fā)一次,到處運(yùn)行。這樣可以很大程度上提升迭代速度和一致性。但是,如果有新功能開發(fā),通常需要評(píng)估對(duì)不同場(chǎng)景的影響,回歸核心用例確保穩(wěn)定性。雖然系統(tǒng)本身有單測(cè)/集成測(cè)試覆蓋,但依然增加了測(cè)試同學(xué)的工作量。慶幸的是,隨著客戶端自動(dòng)化測(cè)試和后端沙盒錄制回放的用例覆蓋率增長(zhǎng),風(fēng)險(xiǎn)和工作量會(huì)逐漸減小。
五、總結(jié)與展望
截止目前,移動(dòng)端和后端都已經(jīng)穩(wěn)定上線,投入使用。也就是說,有贊零售所有的線下收銀場(chǎng)景都使用了這套計(jì)算框架。在訂單優(yōu)惠計(jì)算方面的研發(fā)效率,至少提升了4倍人效。后面需要做的是,將自身平臺(tái)有能力,但目前依賴后端計(jì)算的場(chǎng)景(PC 收銀、自助收銀大屏版),集成這套計(jì)算框架。實(shí)現(xiàn)本地計(jì)算,優(yōu)化用戶體驗(yàn)。
對(duì)于上線后近期的產(chǎn)品迭代,目前的模型設(shè)計(jì)和擴(kuò)展性能夠優(yōu)雅的實(shí)現(xiàn)需求,如在「營(yíng)銷疊加互斥項(xiàng)目」中,對(duì)業(yè)務(wù)來講屬于大改的,其實(shí)對(duì)訂單優(yōu)惠計(jì)算的影響很小,很容易就實(shí)現(xiàn)了,得益于設(shè)計(jì)之初就將使用策略與計(jì)算邏輯分離。
在今后的迭代中,希望能保持項(xiàng)目的代碼質(zhì)量和良好設(shè)計(jì) (附上郵箱 可內(nèi)推、聊技術(shù)和代碼整潔)