最近做了一版需求,和淘寶等商城App有些不一樣,我們的商品單個因子就可以構(gòu)成一個SKU,特地記錄下來,給也有這種需求的App提供一個思路。

需求
名詞解釋:
- 規(guī)格:即SKU。
- 因子組:構(gòu)成商品規(guī)格的一個維度,比如:"座位數(shù)"。
- 因子:因子組中具體的一個因子,比如說:"5座及以下","6座","7座及以上"。
靈活的因子
每一個因子組最多只能選擇一個因子,來構(gòu)成一個規(guī)格。
舉個例子,假設(shè)鈑金噴漆欄目里面有一個商品叫做 "前保險杠",該商品有兩個因子組:
- 漆類
- 普通漆
- 金屬漆
- 鈑金
- 普通鈑金
- 復(fù)雜鈑金
那么一共可以有8種規(guī)格:
- 普通漆
- 金屬漆
- 普通鈑金
- 復(fù)雜鈑金
- 普通漆-普通鈑金
- 普通漆-復(fù)雜鈑金
- 金屬漆-普通鈑金
- 金屬漆-復(fù)雜鈑金
在實際情況中,在配置的時候可能會刪除幾種不需要的規(guī)格,比如說刪除規(guī)格:"普通鈑金" 、 "復(fù)雜鈑金" 和 "金屬漆-復(fù)雜鈑金"。
對用戶來說,只選一個 "漆類" 可以構(gòu)成一個規(guī)格,也可以再搭配一個 "鈑金" 構(gòu)成另一種規(guī)格。只是選擇 "金屬漆" 的時候只能選擇 "普通鈑金",不能選擇 "復(fù)雜鈑金"。

默認選中
在顯示選擇規(guī)格彈窗的時候,默認勾選一組可以構(gòu)成一個規(guī)格的因子,盡量讓構(gòu)成選中的因子位于因子組中靠前的位置。
舉個例子,假設(shè)后一個商品 "后保險杠" ,它也有 "漆類" 和 "鈑金" 兩個因子組,刪除規(guī)格:"普通漆" 和 "普通漆-普通鈑金",可用規(guī)格如下:
- 金屬漆
- 普通鈑金
- 復(fù)雜鈑金
- 普通漆-復(fù)雜鈑金
- 金屬漆-普通鈑金
- 金屬漆-復(fù)雜鈑金
那么默認選中的就是 "普通漆-復(fù)雜鈑金"。

后端返回數(shù)據(jù)
后臺返回的一個商品的 JSON 數(shù)據(jù)結(jié)構(gòu)如下:
{
"id": 1,
"name": "前保險杠",
"factorGroupList": [
{
"id": 10,
"name": "漆類",
"list": [
{
"id": 11,
"name": "普通漆"
}
]
}
],
"specificationList": [
{
"id": 101,
"originalPrice": 1000,
"discountPrice": 1000,
"factorIdList": [
11
]
}
]
}
-
factorGroupList是一個數(shù)組,里面的每一項都代表該商品的一個因子組,比如 "漆類"。在因子組中,list字段中是具體的因子,比如:"普通漆"、"金屬漆"。 -
specificationList是一個數(shù)組,里面的每一項都代表該商品的一種可用規(guī)格。-
originalPrice是原價,discountPrice是折扣價,如果折扣價等于原價,則沒有折扣價,這里的單位為分。 -
factorIdList是一個數(shù)組,里面的每一項代表著該規(guī)格所包含的因子id。
-
算法思路
因子一共有三種狀態(tài),所以在 FactorEntity 里創(chuàng)建了一個枚舉類 Status,并新增一個 Status 類型的 status 字段來標識當前因子的狀態(tài)。
/**
* 因子狀態(tài)
*/
enum class Status {
/**
* 不可用
*/
DISABLED,
/**
* 可用
*/
AVAILABLE,
/**
* 選中
*/
SELECTED
}
考慮到選擇規(guī)格的邏輯其實和UI邏輯相對獨立,所以專門新建了一個類 ChooseSpecificationCalculator 來處理選擇規(guī)格的計算。
初始化
創(chuàng)建一個列表來裝當前選中的因子,在選中和取消選中因子的時候?qū)υ摿斜磉M行增加或者刪除因子的操作;
// 選中因子列表
private val selectedFactorList = ArrayList<FactorEntity>()
在 FactorEntity 中新增一個字段 specificationList 放所有包含該因子的規(guī)格,這樣后續(xù)處理該因子的時候不必每次都遍歷整個規(guī)格列表,只需要在初始化的時候?qū)γ恳粋€因子遍歷一次整個規(guī)格列表就好。
product.factorGroupList.forEach { factorGroup ->
factorGroup.list.forEach { factor ->
factor.specificationList.addAll(product.specificationList.filter {
it.factorIdList.contains(factor.id)
})
}
}
對每一個因子組遍歷一次,刪除 specificationList 為空的因子,然后再遍歷因子組列表,刪除因子數(shù)為空的因子組,給剩余的每一個因子 status 賦值為 AVAILABLE。
// 移除沒有規(guī)格的因子
product.factorGroupList.forEach { factorGroup ->
factorGroup.list = factorGroup.list.filter {
it.specificationList.isNotEmpty()
} as ArrayList<FactorEntity>
}
// 移除沒有因子的因子組
product.factorGroupList = product.factorGroupList.filter {
it.list.isNotEmpty()
} as ArrayList<FactorGroupEntity>
// 默認所有因子可用(經(jīng)過上一步的篩選,剩下的因子至少包含一個規(guī)格,在沒有選中因子的時候,所有的因子都是可用的)
product.factorGroupList.forEach { factorGroup ->
factorGroup.list.forEach { factor ->
factor.status = FactorEntity.Status.AVAILABLE
}
}
注意:這里之所以這么處理是因為需求要求刪除無用的因子,如果你們的需求需要保留不可用因子的話,可以判斷
specificationList是否為空,為空的status賦值為DISABLED,不為空的status賦值為AVAILABLE。
選中因子
只處理處理可用狀態(tài)的因子,如果當前因子組存在選中因子,則將該因子變?yōu)榭捎脿顟B(tài)。然后將本次操作的因子狀態(tài)置為選中狀態(tài),已選中因子列表加入該因子,然后更新所有因子的狀態(tài)。
fun selectedFactor(factor: FactorEntity) {
if (factor.status != FactorEntity.Status.AVAILABLE) {
return
}
// 如果當前因子組存在選中因子,則將該因子變?yōu)榭捎脿顟B(tài)
val factorGroup = product.factorGroupList.find { it.list.contains(factor) }!!
factorGroup.list.find { it.status == FactorEntity.Status.SELECTED }?.let {
removeSelectedFactor(it)
}
addSelectedFactor(factor)
updateAllFactorStatus(factor)
}
取消選中因子
只處理處理選中狀態(tài)的因子,將因子狀態(tài)置為可用狀態(tài),已選中因子列表移除該因子,然后更新所有因子的狀態(tài)。
fun unselectedFactor(factor: FactorEntity) {
if (factor.status != FactorEntity.Status.SELECTED) {
return
}
removeSelectedFactor(factor)
updateAllFactorStatus(factor)
}
更新因子狀態(tài)
這里是算法的核心部分,這里拿判斷因子 A 的狀態(tài)為例。
使用已選中因子列表的因子 id 加上因子 A 的 id 構(gòu)成一個集合 B,然后去遍歷因子 A 的 specificationList 中的規(guī)格,看是否能找到一個規(guī)格滿足集合 B 是其 factorIdList 的子集。
在這里,判斷一個集合是另一個集合的子集采用的是 containsAll 方法,你如果需要考慮優(yōu)化的話,可以參考淘寶團隊的sku組合查詢算法探索。
val find = factor.specificationList.find { specification ->
specification.factorIdList.containsAll(factorIdList)
}
if (find == null) {
factor.status = FactorEntity.Status.DISABLED
} else {
factor.status = FactorEntity.Status.AVAILABLE
}
獲得選中規(guī)格
如果選中因子列表為空,則返回 null。
拿出選中因子列表中的第一個因子,遍歷該因子的 specificationList 字段中的每一個規(guī)格,如果發(fā)現(xiàn)有規(guī)格滿足其字段 factorIdList 的數(shù)量等于 selectedFactorList 的數(shù)量 ,selectedFactorList 構(gòu)成的因子 id 集合是其factorIdList 的子集,則該規(guī)格為當前因子構(gòu)成的規(guī)格。
如果遍歷完后還找不到對應(yīng)的規(guī)格,則返回 null。
fun getSelectedSpecification(): SpecificationEntity? {
if (selectedFactorList.isEmpty()) {
return null
}
// 對包含選中因子列表中第一個的因子的規(guī)格進行遍歷,查看當前選中的因子列表是否能構(gòu)成一個規(guī)格
return selectedFactorList.first().specificationList.find { specification ->
specification.factorIdList.size == selectedFactorList.size &&
specification.factorIdList.containsAll(selectedFactorIdList)
}
}
默認選中
一開始的時候,覺得這里是一個難點,但是等把上面的算法都實現(xiàn)后,回過頭來考慮這個問題的時候,問題已經(jīng)變得很簡單了。
直接遍歷因子組列表,從因子組中找到第一個可用的因子,選中它,如果能構(gòu)成一個規(guī)格則結(jié)束,否則繼續(xù)選中下一個因子組中的第一個可用因子,直到構(gòu)成一個規(guī)格。
run breaking@{
product.factorGroupList.forEach forEach1@{ factorGroup ->
factorGroup.list.forEach forEach2@{ factor ->
if (factor.status == FactorEntity.Status.AVAILABLE) {
// 選中該因子
selectedFactor(factor)
if (getSelectedSpecification() != null) {
// 找到規(guī)格,結(jié)束尋找
return@breaking
}
return@forEach1
}
}
}
}
總結(jié)
這個算法和淘寶等商城App的選擇規(guī)格算法雖然有一定的差異性,但是把刪除的規(guī)格當作已經(jīng)賣完的規(guī)格,再加上規(guī)格數(shù)量,那么就和淘寶等商城App的邏輯差不多了。
項目源碼:choose-specification