preface

The creation and query of commodity SKU is one of the most classic development scenarios of e-commerce business, and also the most basic function of the entire e-commerce system. Because if it lacks, then perhaps even accurately describe the positioning of a commodity, so the most basic needs, will become difficult, commodity inventory management is nowhere to talk about.

An overview of

As shown in the figure, the overall process can be divided into two parts:

  1. Operating side. Responsible for creating and configuring SKUs, the operation generates a list of SKUs by operating the SKU option.
  2. Client. Responsible for SKU inventory status query.

In the product details page, according to the user selected specifications, go to the SKU list for matching search. If the user completes the selection and can hit a SKU data in stock, a SKU-ID is submitted to the ordering process. Conclusion: This is a front-end oriented implementation scheme, SKU creation and query are completed by the front end, the back end is only responsible for SKU storage. This is also the mainstream implementation of major e-commerce platforms. Of course, there are also back-end oriented solutions, SKU creation, query, storage are handed over to the back-end to complete. Each operation can be regarded as a form submission to the back end, and only the necessary operation information needs to be submitted, the back end processing is completed, and the end result required by the front end is returned. The advantage of this scheme is that the inventory data has good real-time performance. However, whether the operation end dynamically creates the SKU list or the user clicks options on the detail page to query the inventory, both of them are sufficiently high-frequency interactions. Each asynchronous query or loading will interrupt the current operation of the user, resulting in poor user experience. In addition, the front-end workload of this implementation is very small, so this article will not cover.

Demo Project Introduction:

  1. In order to explain the content more clearly, the author organized a SKU-demo, including (1) how to create skUS on the operation side; (2) how does the client query SKU on the commodity details page? Although the sparrow is small, it has all the five organs, and basically covers most of the processing scenes of the front-end SKU. Readers can download and run the project, and read it compared to others, so that they can understand it faster.
  2. UseContext +useReducer is used to simulate a small global Store as a shared data Store between the client and the operator to simulate a complete interaction from the background to the foreground.
  3. The operation configuration page (SRC /views/ createsku.tsx) is responsible for configuring two sets of data: One is attrList and the other is skuList. After configuration, it is output to the user product details page (SRC /views/ searchSKu_XXx. TSX), which are used to render option UI and query SKU information respectively, so they are stored in Store. It facilitates shared communication and simulates the entire process of SKU generation from the operator => sending from the server => reading from the client.

SKU creation

npm startAfter starting the project, a simulation is entered by defaultOperating sideProduct configuration page. The diagram below: tip:Click the entry button in the upper left corner of the page to switch between the operation terminal and the client terminal.

Overall operation: operation throughOperate various specifications on the left to generate a list of SKUs on the right.

Analysis of key requirements:

Operations operations on specification options can be broadly divided into two categories:

  1. Adding, deleting, or modifying attributes affects the number of attributes. You need to insert or delete attributes in the SKU list.
  2. rightattributeTo add or delete

    The number of data layers is affected, which means that all SKU option combinations are not the latest and need to be cleared and recombined. For example, the original property type is onlyColor, size, styleNow add a grouppackageProperty, then all previous SKU combinations need andpackageCombined again, and the inventory data previously configured are invalid.

In order to reduce the complexity of the design, the following schemes are adopted:Regardless of operation type, the logic generated by recombination is followed uniformly. (Of course, if performance is important, you can also optimize by differentiating operations, similar to the domDiff efficiency optimization in VUE for different DOM operation types).

Figure below: Put the left side of different types of specification values throughPermutation and combinationTo generate a list of SKUs on the right.

Concrete implementation:

Then the question is abstracted as:Find the full permutation and combination of the list of commodity specifications. This process is a typical tree structure, and involves walking through every leaf of the tree to collect the leaves.If you’re interested, check out LeetCodecombinationSo this is a very similar problem.

For the Tree structure data with unfixed number of layers, we can first think of usingrecursiveThe idea of solving:

A recursive version

SRC /views/ createsku.tsx

/** * @description recursive version of all SKU perarray * @param attrList attribute list * @param prevSkuList last SKU list data * @returns new SKU list data */ export function createSkuList(attrList:AttrList,prevSkuList:SkuList = []):SkuList{ const skuList:SkuList = []; // let id = 0; Const prevSkuMap = skuList2Map(prevSkuList); const prevSkuMap = skuList2Map(prevSkuList); const loop = (rowIndex:number,prevOptions:AttrSetItem[])=>{ const attrItem = attrList[rowIndex]; if(attrItem.options.length === 0){ loop(rowIndex +1,prevOptions) return } for(const option of attrItem.options){ const curOptions = prevOptions.concat({ label:attrItem.attrLabel, value:option.value }); If (rowIndex === attrlist.length-1){// If (rowIndex === attrlist.length-1){// If (rowIndex === attrlist.length-1){ const key = curOptions.map(({value})=>value).join('_'); If (prevSkuMap[key]){// If (prevSkuMap[key]){// If (prevSkuMap[key]){// If (prevSkuMap[key]){// If (prevSkuMap[key]){// If (prevSkuMap[key]); prevSkuMap[key], id:`${id}` }) }else{ skuList.push({ id:`${id}`, key, attrSet:curOptions, stock:0 }) } }else{ loop(rowIndex +1,curOptions) } } } loop(0,[]) return skuList } /** * @description Sku list data is transferred to map for convenient mapping search. */ function skuList2Map(skuList: skuList){return (skuList: skuList) skuList.reduce<{[skuKey:string]:SkuInfo}>((map,sku)=>{ map[sku.key] = sku; return map },{}) }Copy the code

Cycle version

In addition to recursion, you can do it in a loop. However, they are essentially the same, n^m time complexity. The loop only emulates the concept of a recursive stack by iterating the temporary variable tempList, so the difference between the recursive version and the native function call stack is using the stack in your own code.

/** * @description loop version of all SKU perarray * @param attrs attribute list * @param prevSkuList last SKU list data * @returns new SKU list data */ export function createSkuList1(attrList:AttrList,prevSkuList:SkuList = []):SkuList{ let skuList:SkuList = []; let id = 0; Const prevSkuMap = skuList2Map(prevSkuList) attrList. ForEach ((row)=>{//1. Class if(row.options.length === = 0){return} if(skulist.length === 0){// Initialize first skuList = row.options.map((option)=>{skuList = row.options.map((option)=>{ id ++ const attrSet = [{label:row.attrLabel,value:option.value}] return { id:`${id}`, key:attrSet.map(({value})=>value).join('_'), attrSet, stock:0 } }); }else{ const tempList:SkuList = []; id = 0; skuList.forEach((skuItem)=>{//2. ForEach ((option)=>{//3. Iterate over the current specification values and concatenate the values with all accumulated specifications ID ++; const attrSet = skuItem.attrSet.concat({label:row.attrLabel,value:option.value}); const key = attrSet.map(({value})=>value).join('_'); If (prevSkuMap[key]){// If the skU key is the same before and after the change, reuse the SKU data to avoid overwriting templist.push ({... prevSkuMap[key], id:`${id}` }) }else{ tempList.push({ id:`${id}`, key, attrSet, stock:0 }) } }) }); if(row.options.length > 0){ skuList = tempList } } }) return skuList; } /** * @description skU list data to map, convenient mapping search, */ function skuList2Map(skuList: skuList){return (skuList: skuList) skuList.reduce<{[skuKey:string]:SkuInfo}>((map,sku)=>{ map[sku.key] = sku; return map },{}) }Copy the code

I won’t go into the actual code. But there is one caveat: Although the data in the SKU list is generated again every time, it needs to be retained for the previously configured content data (such as the amount of inventory) before and after the generation, if the SKU combination does not change, otherwise all these data will be lost. So at creation time, each SKU data is defined with a key (a string containing concatenated option values). Eg: combination with option for [M, red, easing] sku, M_ red _ loose, as one of its key only marked on sku to create, will take a new generation of key in the old sku data to find the same, if you can find, reuse the original data, thus avoiding the regenerate after covering the loss caused by the original data. In this way, the generation of the operator SKU is completed. Next, the query of the client SKU is discussed.

Second, SKU query



Here is a screenshot of the SKU query function, where the product manager usually mentions an interaction requirement:It is hoped that the user can predict which SKU combinations are out of stock based on the current selected specifications and turn the corresponding buttons to grey in advance.

Like in the screenshot above,[Red,M, men's]Out of stock, in the selectionred,MAfter, you need to advanceMale moneyThe ash.



Do the opposite. Pick the next two rowsM,Male money, gray the first rowred.

First choose the two lines of red and men’s style, and place the M in the middle of the gray. This is still only one SKU out of stock. What if two SKUs are out of stock? For example, [red, M, men’s style], [red, M, women’s style] are out of stock, that is, the red M is out of stock.



In this case, choose only onered, you need to figure outMNot optional.

1-2 SKU out of stock already need to judge the situation of so many ashes above, so more SKU out of stock? Think about it, there are too many situations to judge, or I don’t know how to judge. However, by deducting the above scenario, we can also summarize some reasons for the complexity of the problem:

  1. The path selected by the user may not be complete, so it is necessary to make a judgment before the user selects all of them.
  2. The order in which the user selects is uncertain, and the user can skip or reverse the selection.
  3. Clicking any option may trigger other options in any position to be grayed or not

A common thread can then be found: No matter what you currently select, walk through all the options, looking at each option and deciding if you need to grash yourself.

Before implementing it, let’s know what we know:

  1. AttrList: A list of attributes used to display the option UI, with a custom disabled field for each option indicating that the button is grayed.
AttrList = [{" attrLabel ":" color ", "options" : [{" value ":" red ", "disabled" : true}, {" value ":" green ", "disabled" : True}, {" value ":" blue ", "disabled" : true}},...Copy the code
  1. SkuList: A list of SKUs generated by the operator. Stock = 0 is the data source to judge whether the stock is out of stock
SkuList = [{" id ":" 1 ", "key" : "red _M_ male money", "attrSet" : [{" label ":" color ", "value" : "red"}, {" label ":" size ", "value" : "M"}, {" label ":" style ", "value" : "male money"}], "stock" : 0},...Copy the code

According to the above arrangement of ideas, write the following code

Conventional circular matching method

/** * @param attrList * @param skuList sku list data */ function setAttrOptionStatus(attrList:AttrList, skuList:SkuList) { // 1. Const selectedSet = attrlist. reduce<{[props:string]:string}>((arr, item) => { item.currentValue && (arr[item.attrLabel] = item.currentValue); return arr }, ForEach ((attr) => {attr.options.forEach((option) => {if (option.ischecked) {return} // 3. Const nextSelectSet = {A,x} const nextSelectSet = {... selectedSet,[attr.attrLabel]:option.value} const keys = Object.keys(nextSelectSet); /* 4. Run the skU list to see if (1) sku matches the following two conditions: find the specification set C corresponding to the SKU and check whether B and C have inclusion relationship B <= C? (2) In stock: judge the inventory search result is no, then this button needs to be gray, and vice versa. */ option.disabled = skuList.findIndex((sku) => { return keys.every((attrKey)=> sku.stock > 0 && sku.attrSet.findIndex((option)=>{ return option.value === nextSelectSet[attrKey] }) > -1) }) === -1; }) }) return attrList }Copy the code
  1. Gets the selected specification set {A}.
  2. Iterate over all the specifications to be selected.
  3. Make A new set B = {A,x} for each option {x} and option {A}
  4. Check whether the skU (1) is in stock and whether it is greater than 0 (2) Specification options match: find the specification set C corresponding to each SKU and check whether B and C have inclusion relationship B <= C? If the search result is no, this button needs to be grayed, and vice versa.

Try analyzing complexity. Assuming that there are m specifications, each of which has n options, step 2 needs to be traversed n x m times, which is acceptable. However, in step 4, the SKU list (length n^m) needs to be completely traversed to determine whether the two sets of B\C have inclusion relationship. It is not a simple process, so it is the focus of optimization.

The Map exhaustive method

Space for time: since the path, order and number of users’ choices are not fixed, the set of options formed is difficult to predict, so can we give all the skU combinations that users can choose in advance? This makes matching queries much faster, because it essentially front-loads the work of traversing query matches. Specific idea: according to the idea of exhaustive method, a Map Map containing any combination of specifications (representing any path chosen by the user) is calculated in advance. The first step is to split each SKU, for example [red, M, male] into (2^n), or 8 suboptions. This can be abstracted as a power set operation.

/** * @function powerSet (arr:string[]) {const ps:string[][] = [[]; for (let i = 0; i < arr.length; i++) { for (let j = 0, len = ps.length; j < len; j++) { ps.push(ps[j].concat(arr[i])); } } return ps; }Copy the code

With this exponentiation method, we can calculate the Map of any size combination in advance

Interface AnyOptionSkuMap{[key:string]:SkuList} /** * @description computes a SKU map containing any combination of specifications * @param SkuList SKu list data * @returns */ function computedSkuResultMap(skuList:SkuList) { const map:AnyOptionSkuMap = {} skuList.forEach((sku) => { If (sku.stock <= 0) {// If (sku.stock <= 0) Return} const ids = subr.attrset. map((item) => item.value) const keysArr = powerSet (ids); keysArr.forEach((keyArr) => { const key = keyArr.join('_') const v = map[key]; map[key] = v ? [...v, sku] : [sku] }) }) return map }Copy the code

The calculation results are as follows:



Next, the main query function SRC /views/ searchSKu_map.tsx is implemented

/** * @description based on the currently selected attribute, Update property button disable status => Map version * @param saleAttrs * @param skuList * @returns */ function setAttrOptionStatus(attrList: attrList, skuMap:AnyOptionSkuMap) { // 1. Const selectedSet = attrlist. reduce<{[props:string]:string}>((arr, item) => { item.currentValue && (arr[item.attrLabel] = item.currentValue); return arr }, ForEach ((attr) => {attr.options.forEach((option) => {if (option.ischecked) {return} // 3. Const nextSelectSet = {A,x} const nextSelectSet = {... selectedSet,[attr.attrLabel]:option.value} /* 4. Form a string key for the values of the elements in set B to look up in the pre-computed skuMap dictionary. If there is no result, this button needs to be grayed, and vice versa. */ const valueArr = attrList.map(({ attrLabel }) => nextSelectSet[attrLabel]).filter(Boolean) const sku = skuMap[valueArr.join('_')] option.disabled = sku === undefined; })})}Copy the code

Steps 1, 2 and 3 are the same as the first cycle of violence method above, but the difference is step 4. It is no longer the complex traversal matching in skuList, but simply judging whether the key (a string composed of the option values of set B in a fixed order) exists in the result Map, that is, we can know whether the corresponding button needs to be out of stock and gray. Advantages: The time complexity of the query is reduced to O1 because the complex traversal matching process is reduced to a map lookup, which is very fast. Disadvantages: a power set split has 2^n subsets. The total SKUS has n^m times 2^ N key-value pairs, resulting in a very large number of key-value pairs as shown in the figure above. Initialization is slow, resulting in a large number of computed MAP key-value pairs, resulting in space waste.

Undirected graph (to be updated)

To be continued, update notice: Try to use the data structure of undirected graph to solve.