可以单个因子构成一个SKU的实现

最近做了一版需求,和淘宝等商城App有些不一样,我们的商品单个因子就可以构成一个SKU,特地记录下来,给也有这种需求的App提供一个思路。

需求

名词解释:

  • 规格:即SKU。
  • 因子组:构成商品规格的一个维度,比如:"座位数"。
  • 因子:因子组中具体的一个因子,比如说:"5座及以下","6座","7座及以上"。

灵活的因子

每一个因子组最多只能选择一个因子,来构成一个规格。

举个例子,假设钣金喷漆栏目里面有一个商品叫做 "前保险杠",该商品有两个因子组:

  • 漆类
    • 普通漆
    • 金属漆
  • 钣金
    • 普通钣金
    • 复杂钣金

那么一共可以有8种规格:

  • 普通漆
  • 金属漆
  • 普通钣金
  • 复杂钣金
  • 普通漆-普通钣金
  • 普通漆-复杂钣金
  • 金属漆-普通钣金
  • 金属漆-复杂钣金

在实际情况中,在配置的时候可能会删除几种不需要的规格,比如说删除规格:"普通钣金" 、 "复杂钣金" 和 "金属漆-复杂钣金"。

对用户来说,只选一个 "漆类" 可以构成一个规格,也可以再搭配一个 "钣金" 构成另一种规格。只是选择 "金属漆" 的时候只能选择 "普通钣金",不能选择 "复杂钣金"。

默认选中

在显示选择规格弹窗的时候,默认勾选一组可以构成一个规格的因子,尽量让构成选中的因子位于因子组中靠前的位置。

举个例子,假设后一个商品 "后保险杠" ,它也有 "漆类" 和 "钣金" 两个因子组,删除规格:"普通漆" 和 "普通漆-普通钣金",可用规格如下:

  • 金属漆
  • 普通钣金
  • 复杂钣金
  • 普通漆-复杂钣金
  • 金属漆-普通钣金
  • 金属漆-复杂钣金

那么默认选中的就是 "普通漆-复杂钣金"。

后端返回数据

后台返回的一个商品的 JSON 数据结构如下:

{
  "id": 1,
  "name": "前保险杠",
  "factorGroupList": [
    {
      "id": 10,
      "name": "漆类",
      "list": [
        {
          "id": 11,
          "name": "普通漆"
        }
      ]
    }
  ],
  "specificationList": [
    {
      "id": 101,
      "originalPrice": 1000,
      "discountPrice": 1000,
      "factorIdList": [
        11
      ]
    }
  ]
}
  • factorGroupList 是一个数组,里面的每一项都代表该商品的一个因子组,比如 "漆类"。在因子组中,list 字段中是具体的因子,比如:"普通漆"、"金属漆"。
  • specificationList 是一个数组,里面的每一项都代表该商品的一种可用规格。
    • originalPrice 是原价,discountPrice 是折扣价,如果折扣价等于原价,则没有折扣价,这里的单位为分。
    • factorIdList 是一个数组,里面的每一项代表着该规格所包含的因子id。

算法思路

因子一共有三种状态,所以在 FactorEntity 里创建了一个枚举类 Status,并新增一个 Status 类型的 status 字段来标识当前因子的状态。

/**
 * 因子状态
 */
enum class Status {
    /**
     * 不可用
     */
    DISABLED,
    /**
     * 可用
     */
    AVAILABLE,
    /**
     * 选中
     */
    SELECTED
}

考虑到选择规格的逻辑其实和UI逻辑相对独立,所以专门新建了一个类 ChooseSpecificationCalculator 来处理选择规格的计算。

初始化

创建一个列表来装当前选中的因子,在选中和取消选中因子的时候对该列表进行增加或者删除因子的操作;

// 选中因子列表
private val selectedFactorList = ArrayList<FactorEntity>()

FactorEntity 中新增一个字段 specificationList 放所有包含该因子的规格,这样后续处理该因子的时候不必每次都遍历整个规格列表,只需要在初始化的时候对每一个因子遍历一次整个规格列表就好。

product.factorGroupList.forEach { factorGroup ->
    factorGroup.list.forEach { factor ->
        factor.specificationList.addAll(product.specificationList.filter {
            it.factorIdList.contains(factor.id)
        })
    }
}

对每一个因子组遍历一次,删除 specificationList 为空的因子,然后再遍历因子组列表,删除因子数为空的因子组,给剩余的每一个因子 status 赋值为 AVAILABLE

// 移除没有规格的因子
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>

// 默认所有因子可用(经过上一步的筛选,剩下的因子至少包含一个规格,在没有选中因子的时候,所有的因子都是可用的)
product.factorGroupList.forEach { factorGroup ->
    factorGroup.list.forEach { factor ->
        factor.status = FactorEntity.Status.AVAILABLE
    }
}

注意:这里之所以这么处理是因为需求要求删除无用的因子,如果你们的需求需要保留不可用因子的话,可以判断 specificationList 是否为空,为空的 status 赋值为 DISABLED,不为空的 status 赋值为 AVAILABLE

选中因子

只处理处理可用状态的因子,如果当前因子组存在选中因子,则将该因子变为可用状态。然后将本次操作的因子状态置为选中状态,已选中因子列表加入该因子,然后更新所有因子的状态。

fun selectedFactor(factor: FactorEntity) {
    if (factor.status != FactorEntity.Status.AVAILABLE) {
        return
    }
    
    // 如果当前因子组存在选中因子,则将该因子变为可用状态
    val factorGroup = product.factorGroupList.find { it.list.contains(factor) }!!
    factorGroup.list.find { it.status == FactorEntity.Status.SELECTED }?.let {
        removeSelectedFactor(it)
    }
    
    addSelectedFactor(factor)
    updateAllFactorStatus(factor)
}

取消选中因子

只处理处理选中状态的因子,将因子状态置为可用状态,已选中因子列表移除该因子,然后更新所有因子的状态。

fun unselectedFactor(factor: FactorEntity) {
    if (factor.status != FactorEntity.Status.SELECTED) {
        return
    }
    
    removeSelectedFactor(factor)
    updateAllFactorStatus(factor)
}

更新因子状态

这里是算法的核心部分,这里拿判断因子 A 的状态为例。

使用已选中因子列表的因子 id 加上因子 Aid 构成一个集合 B,然后去遍历因子 AspecificationList 中的规格,看是否能找到一个规格满足集合 B 是其 factorIdList 的子集。

在这里,判断一个集合是另一个集合的子集采用的是 containsAll 方法,你如果需要考虑优化的话,可以参考淘宝团队的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
}

获得选中规格

如果选中因子列表为空,则返回 null

拿出选中因子列表中的第一个因子,遍历该因子的 specificationList 字段中的每一个规格,如果发现有规格满足其字段 factorIdList 的数量等于 selectedFactorList 的数量 ,selectedFactorList 构成的因子 id 集合是其factorIdList 的子集,则该规格为当前因子构成的规格。

如果遍历完后还找不到对应的规格,则返回 null

fun getSelectedSpecification(): SpecificationEntity? {
    if (selectedFactorList.isEmpty()) {
        return null
    }
    
    // 对包含选中因子列表中第一个的因子的规格进行遍历,查看当前选中的因子列表是否能构成一个规格
    return selectedFactorList.first().specificationList.find { specification ->
        specification.factorIdList.size == selectedFactorList.size &&
                specification.factorIdList.containsAll(selectedFactorIdList)
    }
}

默认选中

一开始的时候,觉得这里是一个难点,但是等把上面的算法都实现后,回过头来考虑这个问题的时候,问题已经变得很简单了。

直接遍历因子组列表,从因子组中找到第一个可用的因子,选中它,如果能构成一个规格则结束,否则继续选中下一个因子组中的第一个可用因子,直到构成一个规格。

run breaking@{
    product.factorGroupList.forEach forEach1@{ factorGroup ->
        factorGroup.list.forEach forEach2@{ factor ->
            if (factor.status == FactorEntity.Status.AVAILABLE) {
                // 选中该因子
                selectedFactor(factor)
                if (getSelectedSpecification() != null) {
                    // 找到规格,结束寻找
                    return@breaking
                }
                return@forEach1
            }
        }
    }
}

总结

这个算法和淘宝等商城App的选择规格算法虽然有一定的差异性,但是把删除的规格当作已经卖完的规格,再加上规格数量,那么就和淘宝等商城App的逻辑差不多了。

项目源码:choose-specification

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容