阿拉斯攀登头像
商品域起步-从pms_category到pms_product封面图

商品域起步-从pms_category到pms_product

01-商品域起步-从pms_category到pms_product

系列:URM Ultra 商品/设备/订单/支付/视觉 业务实战
主角:urm-server 后端 · product 域

做无人售货柜,第一件事不是写支付、也不是上视觉识别,而是先把"卖什么"搞清楚。顾客扫码开门,柜子里摆的是可乐还是纸巾,价格多少,能不能上架售卖——这些都得先有"商品"这个东西。

本篇我们从最朴素的两个概念出发:类目(Category) 和 商品(Product)。它们对应数据库里的两张表 pms_category 和 pms_product,是整个 URM Ultra 后端里最安静、也最基础的存在。把这两张表读顺了,后面的设备货道、订单、扣库存才有挂载点。

一、为什么需要"类目"和"商品"两样东西?

新手常问:我直接建一张商品表,名字价格写上去不就完了?为什么还要多一张类目表?

答案是:类目解决"归类与筛选",商品解决"单品售卖"。

想象你面前是一台双门智能柜:

  • 顾客在前端想按"饮料 / 零食 / 日用品"分类浏览,这是类目的活;
  • 但真正被识别、被下单、被扣库存的是"500ml 可乐"这一瓶,商品才是交易的最小单元。

二者是一对多关系:一个类目下挂多个商品,一个商品只属于一个类目。把类目单独抽出来,商品表只要留一个 categoryId 指回去即可,既不冗余,又能在不改商品结构的前提下无限扩展分类。

黑漂碎碎念:很多同学一开始把"分类"揉进商品里用个字符串字段存,后面要做多级分类、要做分类维度的统计,就抓瞎了。分层建模,是后端老鸟的肌肉记忆。

二、类目实体 Category:pms_category

先看源码里的 Category 类(项目源码,节选自 com.urm.ultra.product.entity):

@Entity
@Table(name = "pms_category")
public class Category extends BaseEntity {

    /** 类目名称 */
    private String name;

    /** 父级类目 ID,0 表示顶级 */
    private Long parentId;

    /** 排序,越小越靠前 */
    private Integer sort;
}

别看只有三个业务字段,每一个都值得讲。

1. name:类目名称

就是给人看的名字,比如"饮料"“零食”“日用品”。注意它只是一个展示名,系统内部不会拿名字去关联商品,关联用的是 id。

2. parentId:父级类目 ID,0 表示顶级

这是类目表最巧妙的一笔。它用"自引用"的方式支持无限层级的分类树:

  • 顶级类目(如"饮料")的 parentId = 0,表示没有父级;
  • 如果将来要细分,比如"饮料"下面再分"碳酸饮料"“果汁”,只需新建一条 parentId = 1(指向饮料)的记录即可。

本项目 Demo 阶段只用到一级分类(饮料/零食/日用品都是 parentId = 0),但字段预留了二级扩展能力。这种"用 parentId 指回自己"的玩法,在部门树、地区树里也极常见,记住它,以后你写任何树形结构都顺手。

3. sort:排序,越小越靠前

类目在管理后台展示时谁上谁下,靠 sort 决定。sort 值越小排得越靠前。比如:

nameparentIdsort含义
饮料01第一组,最靠前
零食02第二组
日用品03第三组

小提示:排序字段用"整型 + 越小越靠前"是业界习惯,比用下标索引好维护——插入新类目时,只需给它一个合适的 sort 值,不必挪动其它记录。

4. 关于 BaseEntity

注意 Category extends BaseEntity。在 URM Ultra 里,每个实体都继承这个基类,自动获得:

字段说明
id主键,自增(JPA IDENTITY),也是父子关联的外键
createTime创建时间,@PrePersist 自动写入,不可更新
updateTime更新时间,每次保存自动刷新

所以我们写业务代码时从不用手动去管 createTime/updateTime,插入即落库,更新即刷新,省心得很。

三、商品实体 Product:pms_product

商品是标准商品库,是"卖什么"的核心载体。源码如下(项目源码):

@Entity
@Table(name = "pms_product")
public class Product extends BaseEntity {

    /** 类目 ID */
    private Long categoryId;

    /** 商品名称 */
    private String name;

    /** 商品编码(内部) */
    private String code;

    /** 条码(用于视觉识别映射) */
    private String barcode;

    /** 售价 */
    private BigDecimal price;

    /** 成本价 */
    private BigDecimal costPrice;

    /** 商品图片 URL */
    private String image;

    /** 规格,如 500ml/瓶 */
    private String spec;

    /** 商品描述 */
    @Column(length = 1000)
    private String description;

    /** 上下架状态 */
    @Enumerated(EnumType.STRING)
    private ProductStatus status;
}

逐字段拆开看:

1. categoryId:所属类目

这就是前面说的"一对多"的"多"这一端。它存的是 pms_category.id 的值。比如可乐的 categoryId = 1(饮料),纸巾的 categoryId = 3(日用品)。

关键点:这里只存 id,不存类目名。好处是类目改名了,商品不用动;坏处是查"某商品属于哪个类目名"时要做一次关联查询。在 URM Ultra 里这层关联放到了货道视图 DeviceProductView 里做(后面设备篇会讲),商品自身保持干净。

2. name 与 code:名称 vs 编码

  • name:给人看的,如"可乐"。
  • code:系统内部唯一编码,如 P001。编码是给程序、给视觉识别、给订单流水用的,不会因为改名而变。

这俩一定要分清:name 会改,code 一般不动。所以凡是系统间对账、识别映射,都用 code 或 barcode,绝不用 name。

3. barcode:条码

条码是商品的"身份证号",来源于商品包装上的条形码。在 URM Ultra 里,它被视觉识别服务用来把"柜子里识别出的一瓶东西"映射回具体商品。也就是说,视觉模型识别出条码 → 系统用 barcode 反查 Product → 拿到价格去下单扣款。这是后面"视觉 → 订单"链路的关键钩子。

4. price 与 costPrice:售价与成本

  • price:顾客扫码付的钱。
  • costPrice:进货成本。

两者都用 BigDecimal 而非 double。这是电商系统的铁律——浮点数 double 有精度误差,0.1 + 0.2 ≠ 0.3,涉及钱必须用 BigDecimal 或分(整数)存储。毛利率一类报表就是 (price - costPrice) / price,数据干净才能算得准。

5. image:商品图片

存的是图片地址(URL)。商品主图展示用。本项目 Demo 用 Mock 数据,生产环境通常接入对象存储,把地址写进来即可。

6. spec:规格

自由文本,描述商品的物理规格,比如"500ml/瓶"“1kg/袋”。它不参与计算,纯展示,但能大幅减少顾客误解。

7. description:描述

商品详情描述,@Column(length = 1000) 限制最长 1000 字符,足够放一段卖点文案。

8. status:上下架状态

ProductStatus 是一个枚举,只有两个值:ON_SALE(上架)、OFF_SALE(下架)。注意字段上标了 @Enumerated(EnumType.STRING),意思是存数据库时存的是字符串 “ON_SALE” 而不是数字 0/1。这个设计后面第 04 篇专门展开讲,先记住:用字符串存枚举,可读性远大于数字,出 bug 时一眼能看懂。

四、两张表的关系:一对多

把两张表放一起,关系就清楚了:

表实体关键字段角色
pms_categoryCategoryid, name, parentId, sort“一”:分类
pms_productProductid, categoryId, name, code, …“多”:商品

关系图(文字版):

类目表 pms_category
  id=1  饮料   parentId=0  sort=1
  id=2  零食   parentId=0  sort=2
  id=3  日用品 parentId=0  sort=3
        │            │            │
        ▼            ▼            ▼
商品表 pms_product
  id=1 可乐   categoryId=1  code=P001
  id=3 橙汁   categoryId=1  code=P003
  id=4 薯片   categoryId=2  code=P004
  id=7 纸巾   categoryId=3  code=P007

一个类目对应多条商品,这就是经典的一对多(One-to-Many)。在 JPA 的世界里,我们这里没有用 @OneToMany 注解把关联建在实体上,而是直接用 categoryId 这个普通字段去手动关联。为什么?因为商品量大、查询多走"按类目分页",用一个外键字段 + Repository 方法 findByCategoryId 更直接、更好控制 SQL,避免懒加载和 N+1 查询的坑。这是务实的选择,初学者先理解"字段关联"这层,比死磕注解关联更重要。

五、Demo 初始数据长什么样

项目启动时(DataInitializer)会灌入演示数据,方便你一上来就有东西可玩。商品域的初始化内容大致是:

  • 类目 3 条:饮料(1) / 零食(2) / 日用品(3),parentId 全为 0,sort 升序。
  • 商品 7 条:编码 P001~P007,全部 ON_SALE。例如可乐(售价 8.00 / 成本 5.00 / 500ml)、矿泉水、橙汁、薯片、巧克力、方便面、纸巾。

你可以启动项目后,访问 http://localhost:8080/swagger-ui.html,找到"商品管理"分组,直接点 GET /admin-api/product/page 看返回,会看到这 7 条商品带着类目一起回来。

六、本篇小结

  • 类目(Category) 管分类,parentId=0 表示顶级,sort 控制展示顺序,支持自引用树形扩展。
  • 商品(Product) 是标准商品库,承载 name/code/barcode/price/costPrice/image/spec/description/status 等字段。
  • 二者是一对多:商品用 categoryId 指回类目。
  • 钱相关字段(price/costPrice)用 BigDecimal,枚举状态用 @Enumerated(STRING) 存字符串,这都是踩坑后沉淀的好规矩。
  • code 与 barcode 是系统内部和视觉识别的"通行证",比 name 可靠。

下一篇我们正式打开 AdminProductController,把管理端的增删改查接口一个一个过一遍——你会看到,这些接口其实就是对这两张表的"标准 CRUD 操作"。

转载自 CSDN-专业IT技术社区

原文链接:https://blog.csdn.net/alspd_zhangpan/article/details/168543574

文章来源转载

评论

评论列表

微信小程序
QQ小程序

关于作者

点赞数:0
关注数:0
粉丝:0
文章:0
关注标签:0
加入于:--