跳至主要內容

商品SKU

起凡大约 5 分钟起凡商城商品管理商品SKU

商品SKU

sku创建
sku创建

建表

-- auto-generated definition
create table product_sku
(
    id           varchar(36)    not null
        primary key,
    created_time datetime(6)    not null,
    edited_time  datetime(6)    not null,
    creator_id   varchar(36)    not null,
    editor_id    varchar(36)    not null,
    `values`     varchar(255)   not null,
    name         varchar(255)   not null,
    product_id   varchar(255)   not null,
    cover        varchar(255)   null,
    price        decimal(38, 2) null,
    stock        int            null,
    description  varchar(255)   null
);

实体类

@GenEntity
@Entity
public interface ProductSku extends BaseEntity {

  @Column(name = "`values`")
  @Key
  @Serialized
  List<String> values();

  @GenField(value = "Sku名称", order = 0)
  String name();

  @ManyToOne
  @OnDissociate(DissociateAction.DELETE)
  @Key
  Product product();

  @IdView
  @GenField(value = "商品", order = 1)
  String productId();

  @GenField(value = "封面", type = ItemType.PICTURE, order = 2)
  String cover();

  @NotNull
  @GenField(value = "价格", type = ItemType.INPUT_NUMBER, order = 3)
  BigDecimal price();

  @GenField(value = "库存", order = 4)
  int stock();

  @GenField(value = "描述", order = 5)
  String description();
}

生成代码

参考开发流程

关联商品

修改Dto

ProductSku.dto

input ProductSkuInput {
    #allScalars(ProductSku)
    productId
    id? 
}

商品查询

product-store.ts

export const productQueryOption = async (keyword: string, id: string) => {
  return (
    await api.productController.query({
      body: { query: { name: keyword, id } }
    })
  ).content
}

商品选择

      <el-form-item label="商品" prop="product">
        <remote-select
          label-prop="name"
          :query-options="productQueryOption"
          :model-value="createForm.productId"
          @update:model-value="handleProductChange"
        ></remote-select>
      </el-form-item>

复用商品信息

商品sku的默认值可以用关联的商品信息填写. 选择完商品后得到商品的id,请求数据得到商品信息. 将商品信息复制给sku创建表单

const handleProductChange = (id: string) => {
  api.productController.findById({ id }).then((res) => {
    createForm.value = {
      ...createForm.value,
      ..._.pick(res, ['price', 'cover', 'name', 'description'])
    }
    createForm.value.productId = id
  })
}

sku创建

sku的生成是基于商品的属性进行排列组合生成的. 如手机sku, (黑色, 8gb+256gb),(白色, 8gb+256gb), (黑色, 12gb+512gb), (白色, 12gb+512gb).

存储商品属性

const attributes = ref<KeyValue[]>([])
const skuList = ref<ProductSkuInput[]>([])
const handleProductChange = (id: string) => {
  api.productController.findById({ id }).then((res) => {
    attributes.value = res.attributes
    // 忽略...
  })
}

生成sku

product-sku-create-table.vue

<script lang="ts" setup>
import { computed, watch } from 'vue'
import type { KeyValue, ProductSkuInput } from '@/apis/__generated/model/static'
import type { Scope } from '@/typings'
import _ from 'lodash'

type ProductSku = ProductSkuInput & {
  [index: string]: string | string[] | number
}
type ProductSkuScope = Scope<ProductSku>

const props = withDefaults(
  defineProps<{
    attributes: KeyValue[]
    skuList: ProductSkuInput[]
  }>(),
  {
    attributes: () => {
      return []
    },
    skuList: () => {
      return []
    }
  }
)
const emit = defineEmits<{ 'update:skuList': [skuList: ProductSku[]] }>()
const initSku: ProductSku = {
  productId: '',
  cover: '',
  description: '',
  name: '',
  price: 0,
  stock: 0,
  values: []
}
// 递归排列组合
const deep = (sku: ProductSku, depth: number, attrList: KeyValue[]) => {
  const list: ProductSku[] = []
  if (attrList.length === 0) {
    return []
  }
  if (depth === attrList.length) {
    return [_.cloneDeep(sku)]
  }
  const attribute = attrList[depth]
  if (attribute.values.length === 0) return []
  // 获取每个属性的值(选择的),将各个属性的值进行组合
  const attrValues = attribute.values
  attrValues.forEach((value) => {
    // sku对象新增字段,字段的名字是属性的名字。比如 {"糖份":"多糖"}
    sku[attribute.name] = value
    // 加入当前属性的参数
    sku.values.push(value)
    // 获取下一个属性的参数
    list.push(...deep(sku, depth + 1, attrList))
    // 弹出当前属性的参数,准备加入当前属性的下一个参数
    sku.values.pop()
  })
  return list
}

// 计算函数,将下面的sku格式转成前端的表格读取格式。
// [{"values": ["黑色", "8gb+256"]},{"values": ["白色", "8gb+256"]}]
// -> [{"颜色": "黑色", "型号": "8gb+256"},{"颜色": "白色", "型号": "8gb+256"}]
const skuArray = computed(() => {
  return props.skuList.map((sku) => {
    const row: ProductSku = { ...sku }
    // attributes=[{"name" :"颜色", "values": ["黑色", "白色"]}, {"name" : "型号", "values": ["8gb+256", "128gb"]}]
    // 可以看到sku中values各个值的索引在attributes中可以对应属性名称。如 黑色(0) 用0在attributes中找到对应属性名称即"颜色"。
    sku.values.forEach((attrValue, index) => {
      row[props.attributes[index].name] = attrValue
    })
    return row
  })
})
watch(
  () => props.attributes,
  (value, oldValue) => {
    // 第一次加载时使用默认的skuList不需要重新生成
    if (oldValue.length === 0 && props.skuList.length > 0) {
      return
    }
    const skuList = deep({ ...initSku }, 0, props.attributes)
    console.log(skuList)
    // 当属性修改时重新生成skuList
    emit('update:skuList', skuList)
  },
  { deep: true }
)
</script>
<template>
  <el-table ref="table" :data="skuArray" :border="true">
    <el-table-column
      v-for="attribute in attributes"
      :key="attribute.name"
      :prop="attribute.name"
      :label="attribute.name"
      minWidth="100"
      show-overflow-tooltip
    >
    </el-table-column>
    <el-table-column
      label="Sku名称"
      prop="name"
      sortable="custom"
      min-width="120"
      show-overflow-tooltip
    >
      <template v-slot:default="{ row }: ProductSkuScope">
        {{ row.name }}
      </template>
    </el-table-column>
    <el-table-column label="封面" prop="cover" sortable="custom" min-width="80">
      <template v-slot:default="{ row }: ProductSkuScope">
        <ElAvatar :src="row.cover"></ElAvatar>
      </template>
    </el-table-column>
    <el-table-column
      label="价格"
      prop="price"
      sortable="custom"
      min-width="80"
      show-overflow-tooltip
    >
      <template v-slot:default="{ row }: ProductSkuScope">
        {{ row.price }}
      </template>
    </el-table-column>
    <el-table-column
      label="库存"
      prop="stock"
      sortable="custom"
      min-width="80"
      show-overflow-tooltip
    >
      <template v-slot:default="{ row }: ProductSkuScope">
        {{ row.stock }}
      </template>
    </el-table-column>
    <el-table-column
      label="描述"
      prop="description"
      sortable="custom"
      min-width="120"
      show-overflow-tooltip
    >
      <template v-slot:default="{ row }: ProductSkuScope">
        {{ row.description }}
      </template>
    </el-table-column>
  </el-table>
</template>

<style lang="scss" scoped>
.create-form {
  margin-right: 30px;
}
</style>

初始化sku信息

如果用户编辑了sku表单,则把表单中的信息赋值给skuList

watch(
  () => createForm.value,
  () => {
    skuList.value = skuList.value.map((sku) => {
      return { ...sku, ...createForm.value, values: sku.values }
    })
  },
  {
    deep: true
  }
)

sku是在用户选择关联的商品之后才会出现的, attributes赋值完之后会生成skuList. nextTick就是等待页面渲染完成之后再执行,这样可以保证skuList已经生成了, createFrom复用product的基础信息, 上面的watch又会更新skuList.

const handleProductChange = (id: string) => {
  api.productController.findById({ id }).then((res) => {
    attributes.value = res.attributes
    nextTick(() => {
      createForm.value = {
        ...createForm.value,
        ..._.pick(res, ['price', 'cover', 'name', 'description'])
      }
      createForm.value.productId = id
    })
  })
}

批量创建sku

ProductSkuController

  @PostMapping("save-batch")
  public Boolean saveBatch(@RequestBody List<ProductSkuInput> productSkus) {
    productSkus.forEach(productSkuService::save);
    return true;
  }

调用接口批量新增sku
product-sku-create-form

const handleConfirm = () => {
  createFormRef.value?.validate(
    assertFormValidate(() =>
      api.productSkuController.saveBatch({ body: skuList.value }).then(async (res) => {
        assertSuccess(res).then(() => {
          closeDialog()
          reloadTableData()
        })
      })
    )
  )
}

组合值查询条件

修改dto

specification ProductSkuSpec {
    #allScalars
    like/i(name)
    like/i(description)
    like/i(cover)
    like/i(id)
    // 新增查询字段
    valueStr: String?
    ge(editedTime)
    le(editedTime)
    ge(createdTime)
    le(createdTime)
}

模糊查询

jimmer中提供的查询条件满不足不了需求, 下面可以自定sql动态拼接到where中. values字段在实体类中是List<String>, 在数据库是varchar. jimmer不支持对List<String>like查询, 但是在数据库层面是可以的, 因此这种情况我们直接拼接原生sql进行查询.

ProductSkuRepository

        .whereIf(StringUtils.hasText(query.getValueStr()),
            Predicate.sql("%e like %v", it -> {
              it.expression(productSkuTable.values())
                  .value("%" + query.getValueStr() + "%");
            }))

查询项

product-sku-query

      <el-form-item label="属性组合值">
        <el-input v-model="query.valueStr"></el-input>
      </el-form-item>