前言
作为一个客户端工程师,在开发过程中需要写算法的次数,基本是屈指可数的,
我工作6年多,也就在4年前写过一个16进制字符串对100求于的算法,主要用于本地实验分配
最近在开发SKU功能,是我第二次用到数据结构和算法的知识去开发一个功能
下面和大家分享一下整个开发过程
效果
功能介绍
如图所示,在商品设置有一个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表示是否是活动商品,用于展示红旗标
以上介绍完整个产品逻辑,那我们来一起思考该功能如何实现
功能实现
例如这样一个规格
其中可能存在的组合是: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_status和activityFlag,方便刷新sku_item的状态
func setDefaultStatus() {
for item in allItems {
item.setStatus(with: item.state.optimalStatus, isActivity: item.state.optimalActivity)
}
}
使用VariantState加工好的最合适状态设置sku_item,如图所示
选中逻辑
如图所示,我们在每次选中一个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
}
首先我构造了一个数组,主要算出不同行应该出现的组合,例如:
先选中单里,那么我需要刷新颜色分类,鞋码,打包数
我的每个sku_item的state中已经有了他可以出现的组合,那么我构造好这样一个数组,遍历所有符合条件的组合,并设置item_status值最小的状态即可
再选中红色,这时数组会变成
细心的同学会发现,为什么activity的默认值是true,并且只需要与出一个结果就行
首先活动,他是针对一个商品,也就是唯一的一个sku组合,那么当有一个sku_item不是活动的时候,那么对应的组合肯定就不是活动商品,所以activity的默认值需要为true,且与出一个结果就行
整个SKU功能的核心部分到这里就介绍完了,虽然整个算法过程不是很精妙,但是总体时间复杂度还是可用的,并且整个功能流程还是很清晰的,下面放了Demo链接,有需要的同学可以自取