记一次iOS SKU功能开发

346 阅读6分钟

前言

作为一个客户端工程师,在开发过程中需要写算法的次数,基本是屈指可数的,

我工作6年多,也就在4年前写过一个16进制字符串对100求于的算法,主要用于本地实验分配

最近在开发SKU功能,是我第二次用到数据结构和算法的知识去开发一个功能

下面和大家分享一下整个开发过程

效果

ezgif.com-gif-maker.gif

功能介绍

Screen Shot 2022-11-02 at 14.56.18.png

如图所示,在商品设置有一个SKU选择的功能

每个sku_item会有五种状态:

  • 普通 (后端下发item_status=1)
  • 售罄(后端下发item_status=2)
  • 下架(后端下发item_status=3)
  • 活动红旗标(后端下发extension.activityFlag=true)
  • 置灰不可点击(端上生成)

运营可以配置红旗活动标,表示活动商品

如果后端下发的组合不存在,那么该组合置灰,不可点击

如图1,S款+石榴色这个组合后端没有下发,选中S款时,石榴色就不可点击

一个sku item命中多个组合时,取item_status值最小的那个,例如:

柑橘色+S款的状态值是1,柑橘色+M款的状态值是2,那么柑橘色在没有选中M款时,应该展示1

后端下发数据结构
{
    "goods_all_variant": {
        "content": [
            {
                "image": "",
                "name": "\u52a0\u7ed2,\u7ea2\u8272,33,1",
                "item_status": 1,
                "variants": [
                    {
                        "id": "616fb2a9afc7f60001228693",
                        "name": "\u5957\u88c5\u660e\u7ec61",
                        "value": "\u52a0\u7ed2"
                    },
                    {
                        "value": "\u7ea2\u8272",
                        "id": "60404e0de85df80001991224",
                        "name": "\u989c\u8272\u5206\u7c7b"
                    },
                    {
                        "name": "\u978b\u7801",
                        "value": "33",
                        "id": "62abfa0405f3c9000115fc39"
                    },
                    {
                        "value": "1",
                        "id": "24874142",
                        "name": "\u6253\u5305\u6570"
                    }
                ],
                "main_image": "图片地址",
                "id": "633a6b9cf696fd0001445b41",
                "price": "1",
                "extension": {
                    "activityFlag": "1"
                }
            }
        ],
        "sku_list": [
            {
                "id": "616fb2a9afc7f60001228693",
                "name": "套装明细1",
                "values": [
                    "\u52a0\u7ed2",
                    "\u5355\u91cc"
                ]
            },
            {
                "name": "颜色分类",
                "values": [
                    "\u7ea2\u8272",
                    "\u6d45\u7070\u8272",
                    "\u6df1\u7070\u8272",
                    "\u7070\u8272",
                    "\u9ed1\u8272",
                    "\u6854\u7ea2\u8272",
                    "\u73ab\u7ea2\u8272",
                    "\u85d5\u8272",
                    "\u6854\u8272"
                ],
                "id": "60404e0de85df80001991224"
            },
            {
                "name": "鞋码",
                "values": [
                    "33",
                    "36"
                ],
                "id": "62abfa0405f3c9000115fc39"
            },
            {
                "id": "24874142",
                "name": "打包数",
                "values": [
                    "1",
                    "2"
                ]
            }
        ],
        "spu_id": "633a6b9c0893a80001ffc28a"
    }
}

  • sku_list是所有规格选项,也就是每个sku_item上的值,见上图
  • content这个数组表示通过不同sku组合筛选出来的唯一商品,其中item_status表示该组合下,商品的状态(1正常,2售罄,3下架),extension.activityFlag表示是否是活动商品,用于展示红旗标

以上介绍完整个产品逻辑,那我们来一起思考该功能如何实现

功能实现

例如这样一个规格

image.png

其中可能存在的组合是:m * n (高中学过的排列组合)

拿选项 A选项举例,他会存在(A,1)、(A,2)、(A,3)这三种选择组合

那么意味着A选项可能对应着:

  • item_status=1(正常),
  • item_status=2(售罄),
  • item_status=3(下架)

这三种状态,按照产品逻辑,我们需要展示item_status=1的状态

初始化UI逻辑

那么我们可以遍历后端返回的content数据,取出每个sku_item所对应的状态(item_status),代码如下

static func initMap(_ allVariant: GoodsAllVariant) -> ([Variant: VariantState], [Set<Variant>: SkuGoods]) {
        // 找出所有匹配规格中,状态rawValue最小的,用于规格的默认status
        // 找出是活动的规格
        var dict: [Variant: VariantState] = [:]
        var skuMap: [Set<Variant>: SkuGoods] = [:]
        for goods in allVariant.goodsList {
            for variant in goods.variants {
                if let state = dict[variant] {
                    state.goodsMap[goods.variantSet] = goods
                } else {
                    let state = VariantState(with: goods)
                    dict[variant] = state
                }
                skuMap[goods.variantSet] = goods
            }
        }
        return (dict, skuMap)
    }

这段代码有两个返回值:

第一个就是遍历出每个sku规格所对应的goods,采用字典,方便在对每个sku_item赋值,goods包含了我们UI上所需要展示的所有数据,其中也包括item_status,这个goods其实是后端处理好sku组合后所对应的唯一商品

第二个是sku组合对应的goods,在我们选中sku_item后,需要展示对应组合的goods信息,采用字典存储,key使用Set方便读取,因为后续我们会把选中的sku_item放在一个Set

VariantState
class VariantState {
    var goodsMap: [Set<Variant>: SkuGoods]
    init (with sku: SkuGoods = SkuGoods()) {
        goodsMap = [sku.variantSet: sku]
    }
    
    private(set) lazy var optimalStatus: SKUItemStatus = {
        return Self.optimalStatus(with: Set(goodsMap.values.map { $0.status }))
    }()

    private(set) lazy var optimalActivity: Bool = {
        goodsMap.values.first(where: { $0.isActivity }) != nil
    }()

    static func optimalStatus(with set: Set<SkuGoods.Status>) -> SKUItemStatus {
        let status = set.min { $0.rawValue < $1.rawValue }
        guard let status = status else {
            return .unfound
        }

        switch status {
        case .normal:
            return .normal
        case .sellOut:
            return .sellOut
        case .notSale:
            return .notSale
        }
    }

    static func status(with status: SkuGoods.Status) -> SKUItemStatus {
        switch status {
        case .normal:
            return .normal
        case .sellOut:
            return .sellOut
        case .notSale:
            return .notSale
        }
    }
}

每个sku_item持有一个VariantState,里面存放了sku_item可以对应的goods,主要后续方便读取item_statusactivityFlag,方便刷新sku_item的状态

    func setDefaultStatus() {
        for item in allItems {
            item.setStatus(with: item.state.optimalStatus, isActivity: item.state.optimalActivity)
        }
    }

使用VariantState加工好的最合适状态设置sku_item,如图所示

drawing

选中逻辑

drawing

如图所示,我们在每次选中一个sku_item时,都需要刷新不同行sku_item的状态,并且每个sku_item的状态和不同行选中的sku_item有关,因为他们可以组合起来,确定相同的一个goods

说起来就是这么一个简单的逻辑,但是需要去设计这个算法的时候,我还是花了很多时间

    func setSelectedStatus(with changedItem: SKUItemCellModel) {
        if selectedItemSet.isEmpty {
            setDefaultStatus()
            return
        }

        let selectedItemSets = selectedItemSets()

        for item in allItems where changedItem.indexPath.section != item.indexPath.section {
            let result = selectedItemSets[item.indexPath.section]
            var tuple: (SKUItemStatus, Bool) = (.unfound, false)
            for (key, value) in item.state.goodsMap where result.0.isSubset(of: key) {
                let new = VariantState.status(with: value.status)
                tuple.0 = new.rawValue < tuple.0.rawValue ? new : tuple.0
                tuple.1 = item.state.optimalActivity && result.1
            }
            item.setStatus(with: tuple.0, isActivity: tuple.1)
        }
    }
                                                         
    func selectedItemSets() -> [(Set<Variant>, Bool)] {
        var result: [(Set<Variant>, Bool)] = Array(repeating: (Set<Variant>(), false), count: sections.count)
        for section in 0 ..< result.count {
            var tuple: (Set<Variant>, Bool) = ([], true)
            for item in selectedItemSet where item.indexPath.section != section {
                tuple.0.insert(item.variant)
                tuple.1 = tuple.1 && item.state.optimalActivity
            }
            result[section] = tuple
        }
        return result
    }

首先我构造了一个数组,主要算出不同行应该出现的组合,例如: drawing

先选中单里,那么我需要刷新颜色分类鞋码打包数

我的每个sku_itemstate中已经有了他可以出现的组合,那么我构造好这样一个数组,遍历所有符合条件的组合,并设置item_status值最小的状态即可

image.png

再选中红色,这时数组会变成

image.png

细心的同学会发现,为什么activity的默认值是true,并且只需要与出一个结果就行

首先活动,他是针对一个商品,也就是唯一的一个sku组合,那么当有一个sku_item不是活动的时候,那么对应的组合肯定就不是活动商品,所以activity的默认值需要为true,且与出一个结果就行

整个SKU功能的核心部分到这里就介绍完了,虽然整个算法过程不是很精妙,但是总体时间复杂度还是可用的,并且整个功能流程还是很清晰的,下面放了Demo链接,有需要的同学可以自取

Demo