商城模块
商城模块
ZrAdminNet 内置了一套完整的商城模块(ZR.Mall),涵盖品牌管理、商品管理、订单管理、规格模板、销售统计和会员管理等功能,并包含 C 端会员身份体系(手机号验证码登录,独立 mall-token 令牌,与后台账号完全隔离)。
一、模块架构
┌──────────────────────────────────────────────────────────┐
│ ZR.Mall 商城模块 │
├──────────────────────────────────────────────────────────┤
│ Controllers (6) Services (7) │
│ ┌─────────────────┐ ┌──────────────────────┐ │
│ │ BrandController │─────>│ BrandService │ │
│ │ CategoryController│────>│ CategoryService │ │
│ │ ProductController│─────>│ ProductService │ │
│ │ OrderController │─────>│ OMSOrderService │ │
│ │ SkusController │─────>│ SkusService │ │
│ │ SpecTemplateCtrl │─────>│ SpecTemplateService │ │
│ └─────────────────┘ │ ProductSpecService │ │
│ └──────────────────────┘ │
│ │
│ Model (9 Entities + 8 DTOs) │
│ ┌────────────────────────────────────────────────┐ │
│ │ Product ──1:N── Skus (SKU 含乐观锁版本号) │ │
│ │ Product ──1:1── Category (树形自引用) │ │
│ │ Product ──1:1── Brand │ │
│ │ Product ──1:N── ProductSpec (规格值 JSON) │ │
│ │ OMSOrder ──1:N── OMSOrderItem (商品快照) │ │
│ │ MMSUserAddress (收货地址, 独立) │ │
│ │ SpecTemplate (规格模板, 独立可复用) │ │
│ └────────────────────────────────────────────────┘ │
│ │
│ 数据库: MallDb(独立库,SaaS 模式下每租户一份) │
│ 表前缀: mms_(商城管理) / oms_(订单管理) │
└──────────────────────────────────────────────────────────┘
技术栈
| 组件 | 技术 |
|---|---|
| 框架 | .NET 8 / 10 |
| ORM | SqlSugar (CodeFirst) |
| 依赖注入 | [AppService] 自动注册 |
| 对象映射 | Mapster (Adapt) |
| 数据校验 | FluentValidation |
| 日志 | NLog |
| 多租户 | [Tenant("MallDb")] SaaS 独立数据库 |
会员体系(平台级)
- 会员表
ums_member(实体UmsMember位于ZR.ServiceCore/Member),与后台sys_user完全隔离;自增起始值 100000,防止会员 ID 被外部遍历估算。 - 登录即注册:手机号 + 短信验证码;令牌
mall-token使用独立密钥签发验签,与后台 JWT 互不可验,未配置密钥时 fail-closed。 - 已取消游客下单:下单 / 支付 / 查单 / 取消全部需要登录,订单归属只认
MemberId(唯一实现OrderOwnership.IsOwnedBy);收货手机号仅作联系快照(oms_order.GuestPhone),不参与归属判定。 - 后台提供会员管理(列表 / 详情 / 启用禁用);禁用会员的存量令牌经鉴权状态复核最迟 60 秒失效。
二、数据库路由
商城实体全部标注 [Tenant("MallDb")]:
- 非 SaaS 模式:
MallDb配置项指定商城库 ConfigId(默认 "0" 与主库共享) - SaaS 模式:
[Tenant("MallDb")]经配置解析后路由到租户独立的商城数据库
// appsettings.json
{
"MallDb": "0", // 非 SaaS 模式下商城实体使用的 ConfigId
"dbConfigs": [
{ "ConfigId": "0", "Conn": "...", "DbType": 1 } // 主库(含商城)
]
}
三、数据模型
3.1 商品 (Product)
表名:mms_product,主键:ProductId (long)
主要字段
| 字段 | 类型 | 说明 |
|---|---|---|
| ProductId | long | 主键 |
| ProductName | string | 商品名称 |
| ProductCode | string | 商品编码 |
| CategoryId | int | 分类ID,关联 mms_category |
| BrandId | long | 品牌ID,关联 mms_brand |
| ProductType | int | 商品形态:1=实物, 2=虚拟(见 5.7) |
| FreightTemplateId | long? | 运费模板ID,关联 mms_freight_template |
| Price | decimal | 最低价(由 SKU 自动计算) |
| MaxPrice | decimal | 最高价(由 SKU 自动计算) |
| OriginalPrice | decimal | 原价 |
| MainImage | string | 主图 |
| ImageUrls | string | 图片列表(JSON) |
| VideoUrl | string | 视频链接 |
| Unit | string | 单位 |
| SpecType | int | 规格类型:1=单规格, 2=多规格 |
| SpecSummary | string | 规格摘要(JSON) |
| PurchaseLimit | string | 购买限制(JSON) |
| FormItems | JSON | 下单表单字段定义(见 5.8) |
| TotalSalesVolume | int | 累计销量(冗余列,由 ProductTotalsRefresher 维护) |
| TotalStock | int | 总库存(冗余列,"缺货沉底"排序依据) |
| SaleStatus | int | 1=在售, 0=下架 |
| SortId | int | 置顶排序值(仅在置顶时效窗口内生效) |
| TopBeginTime | datetime? | 置顶生效开始时间 |
| TopEndTime | datetime? | 置顶生效结束时间(过期自动失效) |
| HomeWeight | int | 首页 / 推荐场景综合排序权重 |
| CategoryWeight | int | 分类场景综合排序权重 |
| SearchWeight | int | 搜索场景综合排序权重 |
| Introduce | string | 商品介绍 |
| DetailsHtml | string | 商品详情(富文本) |
| IsDelete | int | 软删除标记 |
3.2 商品 SKU (Skus)
表名:mms_skus,主键:SkuId (long),关联 ProductId
主要字段
| 字段 | 类型 | 说明 |
|---|---|---|
| SkuId | long | 主键 |
| ProductId | long | 商品ID |
| Price | decimal | SKU 价格 |
| Stock | int | 库存 |
| SalesVolume | int | 销量 |
| ImageUrl | string | SKU 图片 |
| SpecCombination | string | 规格组合描述 |
| SpecIds | string | 规格ID组合 |
| Specs | string | 规格详情(JSON) |
| Weight | decimal | 重量 |
| Version | long | 乐观锁版本号,防并发超卖 |
| IsDelete | int | 软删除标记 |
Version字段用于乐观锁控制,防止并发更新库存时数据不一致。
3.3 商品规格 (ProductSpec)
表名:mms_product_spec,主键:Id (long),关联 ProductId
| 字段 | 类型 | 说明 |
|---|---|---|
| Id | long | 主键 |
| ProductId | long | 商品ID |
| Name | string | 规格名称(如"颜色"、"尺码") |
| SpecValues | string | 规格值列表(JSON List<string>) |
3.4 商品分类 (Category)
表名:mms_category,主键:CategoryId (int),树形结构(自引用 ParentId)
| 字段 | 类型 | 说明 |
|---|---|---|
| CategoryId | int | 主键 |
| Name | string | 分类名称 |
| ParentId | int | 上级分类ID |
| Icon | string | 图标 |
| Introduce | string | 分类介绍 |
| OrderNum | int | 排序 |
| ShowStatus | int | 显示状态 |
| IsDelete | int | 软删除标记 |
3.5 品牌 (Brand)
表名:mms_brand,主键:Id (long)
| 字段 | 类型 | 说明 |
|---|---|---|
| Id | long | 主键 |
| Name | string | 品牌名 |
| Logo | string | Logo |
| Description | string | 描述 |
3.6 订单 (OMSOrder)
表名:oms_order,主键:Id (long)
主要字段
| 字段 | 类型 | 说明 |
|---|---|---|
| Id | long | 主键 |
| OrderNo | string | 订单号(唯一索引,雪花 ID) |
| MemberId | long? | 下单会员ID(归属唯一凭据,新订单必有值) |
| GuestPhone | string | 下单手机号快照(= 收货手机号,非身份凭据) |
| PayType | int | 支付类型 0模拟 1微信H5(回调写入) |
| TransactionId | string | 第三方支付流水号(微信 transaction_id) |
| OrderType | int | 下单时的商品形态快照:1=实物, 2=虚拟 |
| TotalAmount | decimal | 商品总额(订单项 单价×数量 之和,不含运费) |
| FreightAmount | decimal | 运费(服务端按运费模板重算,见 5.9) |
| DiscountAmount | decimal | 优惠合计(当前恒为 0,优惠券 / 促销未上线) |
| IntegralAmount | decimal | 积分抵扣(当前恒为 0) |
| PayAmount | decimal | 实付金额 = 商品总额 + 运费 − 优惠 − 积分抵扣 |
| RefundAmount | decimal | 已退金额累计(护栏:不得超过实付金额) |
| OrderStatus | int | 订单状态(枚举) |
| DeliveryStatus | int | 发货状态(枚举) |
| RefundStatus | int | 退款状态(枚举) |
| AddressSnapshot | string | 收货地址快照(JSON) |
| DeliveryCompany | string | 快递公司 |
| DeliveryNo | string | 快递单号 |
| OrderNote | string | 用户备注 |
| MerchantNote | string | 商家备注 |
| PayTime | datetime | 支付时间 |
| ShipTime | datetime | 发货时间 |
| ConfirmTime | datetime | 确认收货时间 |
| CancelTime | datetime | 取消时间 |
订单状态枚举
| 枚举值 | 状态名 | 说明 |
|---|---|---|
| 0 | 待付款 | 已下单未支付 |
| 1 | 待发货 | 已支付 |
| 2 | 已发货 | 已发货 |
| 3 | 已完成 | 已确认收货 |
| 4 | 已取消 | 已取消 |
| 5 | 已关闭 | 交易关闭 |
发货状态枚举
| 枚举值 | 状态名 | 说明 |
|---|---|---|
| 0 | 未发货 | 待发货 |
| 1 | 已发货 | 已发货 |
| 2 | 已收货 | 已确认收货 |
退款状态枚举
| 枚举值 | 状态名 | 说明 |
|---|---|---|
| 0 | 无退款 (None) | 正常 |
| 1 | 退款中 (Refunding) | 用户发起退款 / 退货中 |
| 2 | 已退款 (Refunded) | 已退款完成(整单或部分退款) |
| 3 | 退货中 (Returning) | 买家已寄回 |
| 4 | 已退货 (Returned) | 商家确认收货并退款 |
3.7 订单项 (OMSOrderItem)
表名:oms_order_item,主键:ItemId (long),关联 OrderId
主要字段
| 字段 | 类型 | 说明 |
|---|---|---|
| ItemId | long | 主键 |
| OrderId | long | 订单ID |
| ProductId | long | 商品ID |
| ProductName | string | 商品名称快照(下单时记录) |
| ProductPic | string | 商品图片快照 |
| SkuId | long | SKU ID |
| SkuSpec | string | SKU 规格快照 |
| UnitPrice | decimal | 单价快照 |
| TotalPrice | decimal | 小计 |
| Quantity | int | 数量 |
| FreightTemplateId | long? | 运费模板 ID(下单时快照) |
| FreightTemplateName | string | 运费模板名称(下单时快照) |
| FormValues | JSON | 买家填写的下单表单值快照(见 5.8) |
订单项使用快照模式,记录下单时刻的商品名、图片、单价、规格,避免后续商品变更影响历史订单数据。
3.8 收货地址 (MMSUserAddress)
表名:mms_address,主键:Id (long)
| 字段 | 类型 | 说明 |
|---|---|---|
| Id | long | 主键 |
| UserId | long | 用户ID |
| UserName | string | 收件人 |
| Phone | string | 手机号 |
| Province | string | 省 |
| City | string | 市 |
| District | string | 区/县 |
| DetailAddress | string | 详细地址 |
| IsDefault | bool | 是否默认地址 |
3.9 规格模板 (SpecTemplate)
表名:mms_spec_template,主键:Id (long),可复用的规格模板系统
| 字段 | 类型 | 说明 |
|---|---|---|
| Id | long | 主键 |
| TemplateName | string | 模板名称 |
| SpecJson | string | 规格配置(JSON List<SpecDto>) |
3.10 运费模板 (FreightTemplate)
表名:mms_freight_template,主键:Id (long)
| 字段 | 类型 | 说明 |
|---|---|---|
| Id | long | 主键 |
| TemplateName | string | 模板名称 |
| ChargeType | int | 计费方式:1=按件(ByPiece), 2=按重量(ByWeight,kg) |
| FirstPiece | decimal | 首件数(按件模式,首件内一个价) |
| AddPiece | decimal | 续件单位(按件模式,超出部分按此进位加价) |
| FirstWeight | decimal | 首重 kg(按重量模式) |
| AddWeight | decimal | 续重单位 kg(按重量模式) |
| Freight | decimal | 首费金额 |
| FreeShippingThreshold | decimal | 包邮门槛(0 表示不包邮) |
| IsDefault | int | 是否默认模板(商品未绑定模板时回落此模板) |
3.11 运费模板不配送地区 (FreightTemplateExcludeRegion)
表名:mms_freight_template_region,主键:Id (long),关联 TemplateId
| 字段 | 类型 | 说明 |
|---|---|---|
| Id | long | 主键 |
| TemplateId | long | 运费模板 ID |
| AreaCode | string | 行政区划编码(省 / 市 / 区县任一级) |
| AreaName | string | 地区名称(展示用) |
| Level | int | 层级:省 / 市 / 区县 |
3.12 售后单 (OMSAfterSale)
表名:oms_after_sale,主键:Id (long)
主要字段
| 字段 | 类型 | 说明 |
|---|---|---|
| Id | long | 主键 |
| AfterSaleNo | string | 售后单号(唯一) |
| OrderId / OrderNo | long / string | 关联订单 |
| MemberId | long? | 申请会员 |
| Type | int | 1=仅退款, 2=退货退款 |
| Status | int | 售后状态(见下表) |
| ApplyAmount | decimal | 申请金额 |
| RefundAmount | decimal | 实际退款金额(登记退款时写入) |
| Reason | string | 售后原因 |
| Describe | string | 问题描述 |
| ApplyTime | datetime | 申请时间 |
| AuditBy / AuditTime / AuditRemark | - | 审核人与审核意见 |
| ReturnCompany / ReturnNo / ReturnTime | - | 买家退货物流 |
| ReceiveTime / ReceiveRemark | - | 商家收货确认 |
| RefundChannelNo / RefundTime | - | 渠道退款单号与退款时间(人工登记) |
| CancelTime | datetime? | 买家撤销时间 |
售后类型枚举
| 枚举值 | 类型名 | 说明 |
|---|---|---|
| 1 | 仅退款 | 不寄回商品直接退钱(未发货,或货值低于运费 / 买家拒收) |
| 2 | 退货退款 | 买家先寄回商品,商家确认收到后再退款 |
售后状态枚举
| 枚举值 | 状态名 | 说明 |
|---|---|---|
| 0 | 待审核 | 买家已提交,等待后台审核 |
| 1 | 已驳回 | 审核不通过,流程终止 |
| 2 | 待买家退货 | 仅退货退款:审核通过后进入 |
| 3 | 待商家收货 | 买家已填写退货物流单号 |
| 4 | 待退款 | 仅退款审核通过 / 退货已由商家确认收到 |
| 5 | 已完成 | 渠道退款已由运营登记 |
| 6 | 已撤销 | 买家主动撤销售后 |
3.13 行政区划 (Area,主库公共参考数据)
表名:sys_area,主键:Id (long),[Tenant("0")] + IMainDbEntity(主库公共表,不随租户分库)
| 字段 | 类型 | 说明 |
|---|---|---|
| Id | long | 主键 |
| AreaCode | string | 行政编码(GB/T 2260 统计用区划代码),唯一索引 idx_sys_area_code |
| Name | string | 名称,如「浙江省」「杭州市」「西湖区」 |
| ParentCode | string | 父级编码(省级为 null),业务侧的关联依据 |
| ParentId | long | 父级 Id(查询优化用冗余,按 idx_sys_area_parent 懒加载) |
| Level | int | 层级:1=省 / 2=市 / 3=区县(只能按 Level 判定,东莞等不设区地级市为 9 位街道码) |
| SortId | int | 排序号(灌种按 省→市→区 展开顺序生成) |
种子数据随库初始化自动灌入(
ZR.ServiceCore/SqlSeed/area-seed.json内嵌资源,InitTable.InitDb无条件执行、幂等,已有数据即跳过;失败只记 Warn 不阻断初始化)。层级结构由ParentId懒加载,业务侧跨库对齐用ParentCode。
四、API 接口
接口基础路径:
/shopping,Swagger 分组:shopping
4.1 品牌管理
| 方法 | 路由 | 权限 | 说明 |
|---|---|---|---|
| GET | /shopping/brand/list | shop:brand:list | 品牌列表 |
| GET | /shopping/brand/{Id} | shop:brand:query | 品牌详情 |
| POST | /shopping/brand | shop:brand:add | 添加品牌 |
| PUT | /shopping/brand | shop:brand:edit | 更新品牌 |
| POST | /shopping/brand/delete/{ids} | shop:brand:delete | 删除品牌 |
| GET | /shopping/brand/export | shop:brand:export | 导出 Excel |
4.2 分类管理
| 方法 | 路由 | 权限 | 说明 |
|---|---|---|---|
| GET | /shopping/category/list | shop:category:list | 分页查询(5分钟缓存) |
| GET | /shopping/category/treeList | 无 | 树形列表 |
| GET | /shopping/category/{CategoryId} | 匿名 | 查询单个分类 |
| POST | /shopping/category | shop:category:add | 添加分类 |
| PUT | /shopping/category | shop:category:edit | 更新分类 |
| DELETE | /shopping/category/{ids} | shop:category:delete | 删除分类 |
| GET | /shopping/category/ChangeSort | shop:category:edit | 修改排序 |
| GET | /shopping/category/export | shop:category:export | 导出 Excel |
4.3 商品管理
| 方法 | 路由 | 权限 | 说明 |
|---|---|---|---|
| GET | /shopping/product/list | shop:product:list | 分页查询(含 SKU + 分类 + 品牌联表) |
| GET | /shopping/product/{ProductId} | 匿名 | 商品详情(含 SKU + 规格聚合) |
| POST | /shopping/product | shop:product:add | 添加商品(事务:插入商品+SKU+规格) |
| PUT | /shopping/product | shop:product:edit | 完整更新(事务:差异化更新SKU) |
| PUT | /shopping/product/edit | shop:product:edit | 部分字段更新 |
| POST | /shopping/product/delete/{ids} | shop:product:delete | 软删除 |
| POST | /shopping/product/multi/{type}/{ids} | shop:product:edit | 批量上架/下架 |
| GET | /shopping/product/ChangeSort | shop:product:edit | 修改排序 |
| GET | /shopping/product/export | shop:product:export | 导出 Excel |
4.4 SKU 管理
| 方法 | 路由 | 权限 | 说明 |
|---|---|---|---|
| GET | /shopping/skus/list | shop:skus:list | SKU 列表 |
| GET | /shopping/skus/{SkuId} | shop:skus:query | SKU 详情 |
| POST | /shopping/skus | shop:skus:add | 添加 SKU |
| PUT | /shopping/skus | shop:skus:edit | 更新 SKU |
| POST | /shopping/skus/delete/{ids} | shop:skus:delete | 删除 SKU |
4.5 订单管理
| 方法 | 路由 | 权限 | 说明 |
|---|---|---|---|
| GET | /shopping/order/list | oms:order:list | 订单列表(联表 OrderItem) |
| GET | /shopping/order/{Id} | oms:order:query | 订单详情 |
| PUT | /shopping/order | - | 更新订单(多操作类型) |
| POST | /shopping/order/delete/{ids} | oms:order:delete | 软删除 |
| POST | /shopping/order/delivery | - | 发货 |
| GET | /shopping/order/export | oms:order:export | 导出 Excel |
| GET | /shopping/order/exportDelivery | oms:order:ship | 导出待发货订单 |
| POST | /shopping/order/importData | oms:order:ship | 批量导入发货 |
| GET | /shopping/order/getSales | oms:sale:query | 总销售额统计 |
| GET | /shopping/order/getSalesTrade | oms:sale:query | 销售趋势(按天) |
| GET | /shopping/order/getSaleTopProduct | oms:sale:query | TOP10 热销商品 |
订单更新支持多种操作类型(operType):
| operType | 操作方法 | 说明 |
|---|---|---|
| 1 | NotDelivereOrder | 未发货订单查询 |
| 2 | UpdateMerchantNote | 更新商家备注 |
| 3 | (地址更新) | 更新收货地址 |
| 4 | (退款) | 处理退款 |
4.6 规格模板管理
| 方法 | 路由 | 权限 | 说明 |
|---|---|---|---|
| GET | /shopping/specTemplate/list | spectpl:list | 模板列表 |
| GET | /shopping/specTemplate/tplList | - | 简易列表 |
| GET | /shopping/specTemplate/{Id} | - | 模板详情 |
| POST | /shopping/specTemplate | spectpl:add | 添加模板 |
| PUT | /shopping/specTemplate | spectpl:edit | 更新模板 |
| POST | /shopping/specTemplate/delete/{ids} | spectpl:delete | 删除模板 |
4.7 会员管理(后台)
| 方法 | 路由 | 权限 | 说明 |
|---|---|---|---|
| GET | /shopping/member/list | shop:member:list | 会员分页列表(手机号 / 昵称 / 状态 / 注册时间筛选) |
| GET | /shopping/member/{id} | shop:member:query | 会员详情 |
| PUT | /shopping/member/changeStatus | shop:member:edit | 启用/禁用(禁用后存量令牌最迟 60 秒失效) |
4.8 C 端会员与订单(公网宿主 ZR.Front.WebApi)
基础路径
/front/member,鉴权请求头mall-token;已取消游客下单,下单 / 支付 / 查单 / 取消全部需登录。
| 方法 | 路由 | 说明 |
|---|---|---|
| POST | /front/member/send-code | 发送登录验证码(开发模式 MockSms:Enabled=true 返回 devCode) |
| POST | /front/member/login | 手机号 + 验证码登录(未注册自动注册) |
| GET | /front/member/info | 当前会员信息 |
| PUT | /front/member/update | 更新昵称 / 头像 |
| POST | /front/member/order/create | 下单(绑定 MemberId,防刷 + 幂等) |
| POST | /front/member/order/pay | 发起支付(模拟 / 微信 H5 / 小程序 JSAPI / App) |
| GET | /front/member/order/list | 我的订单(分页 + 状态筛选) |
| GET | /front/member/order/{id} | 订单详情(非本人返回授权失败) |
| POST | /front/member/order/cancel/{id} | 取消订单(回补库存) |
| POST | /front/member/order/confirm-receipt/{id} | 确认收货(幂等) |
| POST | /front/member/afterSale/apply | 提交售后申请(仅退款 / 退货退款) |
| GET | /front/member/afterSale/list | 我的售后单(分页 + 状态筛选) |
| GET | /front/member/afterSale/{id} | 售后单详情(非本人返回授权失败) |
| POST | /front/member/afterSale/return | 填写退货物流(待买家退货状态) |
| POST | /front/member/afterSale/cancel/{id} | 撤销申请(可撤销状态下) |
⚠️ 微信支付回调
POST /shopping/front/order/wechat/notify已配置在微信商户平台,路由不可改。
4.9 运费模板管理
| 方法 | 路由 | 权限 | 说明 |
|---|---|---|---|
| GET | /shopping/freightTemplate/list | shop:freight:list | 分页查询 |
| GET | /shopping/freightTemplate/all | shop:freight:list / shop:product:list / shop:product:edit | 全部模板(下拉用,商品编辑页也可调用) |
| GET | /shopping/freightTemplate/{id} | shop:freight:query | 模板详情(含不配送地区) |
| POST | /shopping/freightTemplate | shop:freight:add | 新增模板 |
| PUT | /shopping/freightTemplate | shop:freight:edit | 更新模板 |
| POST | /shopping/freightTemplate/delete/{ids} | shop:freight:delete | 删除模板 |
4.10 行政区划
| 方法 | 路由 | 权限 | 说明 |
|---|---|---|---|
| GET | /shopping/area/tree | shop:freight:list | 按 parentId 懒加载行政区划树(省/市/区) |
4.11 售后管理(后台)
| 方法 | 路由 | 权限 | 说明 |
|---|---|---|---|
| GET | /shopping/afterSale/list | shop:aftersale:list | 售后单分页列表 |
| GET | /shopping/afterSale/{id} | shop:aftersale:query | 售后单详情 |
| PUT | /shopping/afterSale/audit | shop:aftersale:audit | 审核(通过 / 驳回) |
| PUT | /shopping/afterSale/receive/{id} | shop:aftersale:audit | 确认收到退货 |
| PUT | /shopping/afterSale/refund | shop:aftersale:refund | 登记退款(渠道退款由人工完成后回填) |
| PUT | /shopping/afterSale/cancel/{id} | shop:aftersale:audit | 关闭 / 撤销售后单 |
4.12 商城 AI 文案 Pro
| 方法 | 路由 | 权限 | 说明 |
|---|---|---|---|
| POST | /shopping/ai/productCopy | shop:product:ai | 按商品 / SKU / 分类 / 品牌生成营销文案草稿(不落库,前端可回填表单) |
提示词模板:ZR.Admin.WebApi/Prompts/mall/product-copy.md,AI 场景标识 AiSceneCatalog.ShopProductCopy。
4.13 商城库同步
| 方法 | 路由 | 说明 |
|---|---|---|
| GET | /shopping/dbSync/diff | 预览库结构差异(缺表 / 缺列) |
| POST | /shopping/dbSync/sync | 一键同步(建表 + 补列 + 热查询索引 + 商品名全文索引,syncSeed 控制是否补种子) |
五、关键设计要点
5.1 SKU 乐观锁防超卖
Skus 表内置 Version 字段(long 类型),通过 SqlSugar 的乐观锁机制防止并发更新库存时出现超卖:
// SkusService 中的更新逻辑(伪代码)
var sku = GetInfo(skuId);
sku.Stock -= quantity;
sku.Version++; // 版本号递增
Update(sku, it => new { it.Stock, it.Version });
5.2 订单快照机制
OMSOrderItem 在下单时记录商品名、图片、单价、规格的快照值(ProductName、ProductPic、UnitPrice、SkuSpec),后续即使商品信息变更,历史订单数据不受影响。
5.3 价格由 SKU 驱动
Product.Price 和 Product.MaxPrice 并非独立存储,而是根据关联的 Skus 列表自动计算最低价和最高价:
product.Price = Math.Round(skus.Min(s => s.Price), 2);
product.MaxPrice = Math.Round(skus.Max(s => s.Price), 2);
5.4 JSON 字段存储
多字段使用数据库 JSON 列存储结构化数据,减少额外关联表:
| 字段 | 所属表 | 格式 |
|---|---|---|
| PurchaseLimit | Product | JSON(购买限制规则) |
| AddressSnapshot | OMSOrder | JSON(收货地址快照) |
| SpecValues | ProductSpec | JSON List<string> |
| Specs | Skus | JSON(规格详情) |
| SpecJson | SpecTemplate | JSON(模板配置) |
5.5 软删除
Product、OMSOrder、Skus、Category、Brand 均使用 IsDelete 字段实现软删除,数据不会真正从数据库移除。
5.6 规格模板复用
SpecTemplate 独立于 Product,可定义通用的规格模板(如"颜色/尺码"),多个商品可复用同一模板。
5.7 商品形态:实物 / 虚拟
Product.ProductType 区分商品形态,订单另存 OMSOrder.OrderType 作为下单时快照(商品后续改形态不影响历史订单):
| 形态 | 行为 |
|---|---|
| 实物 (1) | 需收货地址、按运费模板计费、发货后确认收货、可申请售后 |
| 虚拟 (2) | 免收货地址、免运费、付款即完成、买家不可申请售后 |
分流在下单校验、支付、运费计算、售后申请处统一执行。后台新增 / 修改商品时校验形态合法;C 端接口对缺失值补默认实物,避免下发非法枚举。
5.8 下单表单(买家附加字段)
商品可在 Product.FormItems 定义买家下单时需要填写的附加字段(如"身份证号""刻字内容"):
- 字段定义(
ProductFormItem):Field(字段键)、Label、Type、Required、Options、Pattern、MaxLength、Placeholder、Sensitive、Sort; - 字段类型:文本 / 长文本 / 数字 / 手机号 / 邮箱 / 日期 / 下拉 / 单选 / 多选 / 文件等;
- 定义校验(
ProductFormValidator.ValidateDefinition):字段键必须匹配[A-Za-z][A-Za-z0-9_]*且同一商品内唯一、字段数有上限,同时校验候选项、正则、最大长度; - 取值校验(
ValidateValues):下单时服务端校验买家填写的值,未知字段直接丢弃; - 快照:买家填写的值以
OMSOrderItem.FormValues落库,连同Field / Label / Type / Sensitive / Value一并快照,不回查商品当前定义; - 脱敏:后台订单展示默认对敏感字段脱敏(服务端兜底再脱敏一次,见
OrderFormValueHelper)。
5.9 运费计算(FreightCalculator)
- 两种计费模式(
FreightChargeTypeEnum):- 按件(
ByPiece):FirstPiece首件数内一个价,超出部分按AddPiece续件单位进位加价; - 按重量(
ByWeight):FirstWeight首重(kg)内一个价,超出部分按AddWeight续重单位进位加价,重量取自 SKU 的Weight;
- 按件(
- 包邮门槛:
FreeShippingThreshold(0 表示不包邮); - 多模板合并:同一模板的商品先合并件数 / 重量再计算,不同模板的运费相加;
- 默认模板:商品未绑定模板时回落
IsDefault=1的模板;没有任何模板时运费为 0; - 不配送地区:命中
mms_freight_template_region(按AreaCode匹配省 / 市 / 区县)直接阻止下单,不会静默按 0 运费处理; - 服务端重算:下单时按收货行政区划、商品件数 / 重量重新计算,不信任前端试算结果;订单主表写入
FreightAmount,订单项快照模板 ID / 名称便于溯源。
5.10 售后单状态机
仅退款: 待审核(0) ──通过──> 待退款(4) ──登记退款──> 已完成(5)
退货退款: 待审核(0) ──通过──> 待买家退货(2) ──填物流──> 待商家收货(3) ──确认收货──> 待退款(4) ──登记退款──> 已完成(5)
任意可撤销态 ──买家撤销──> 已撤销(6) 待审核 ──驳回──> 已驳回(1)
- 退款登记会累计订单
RefundAmount,并有"累计退款不得超过实付金额"的护栏(整单退款同样受限,不能绕过); - 售后申请受时效约束:确认收货后超过
MallSettings:AfterSaleDays天(默认 15,0 表示不限)不可申请;虚拟商品不可申请; - 渠道退款由运营人工完成后回填单号登记(系统不做渠道自动退款);
- 发货后超过
MallSettings:AutoConfirmReceiptDays天(默认 7)由定时任务Job_AutoConfirmReceipt自动确认收货。
5.11 C 端商品排序与搜索
排序档位(sortMode,见 ProductSortModeResolver):
| 档位 | 规则 |
|---|---|
| 综合(默认) | 分场景权重 → 有效置顶 SortId → 时间衰减热度分 → 上架时间 |
| 销量 | TotalSalesVolume DESC |
| 价格升序/降序 | Price ASC / DESC |
| 上新 | AddTime DESC |
所有档位统一规则:
- 缺货沉底:
TotalStock > 0排前、全缺货排末尾(另有OnlyInStock过滤); - 分场景权重:带关键词→
SearchWeight、带分类→CategoryWeight、其余→HomeWeight; - 置顶时效:仅当当前时间落在
[TopBeginTime, TopEndTime]内SortId才参与排序,窗口外按 0 处理(过期自动失效); - 热度衰减:
(销量+1) / (上架天数+10),避免历史销量永久霸榜、新品无曝光; - 兜底排序:一律以
ProductId DESC收尾,保证同值时分页不跳页、不重复。
搜索(商品名):
- 已建全文索引:SQL Server 走
CONTAINS(ProductName, @kw),MySQL 走MATCH(ProductName) AGAINST (@kw)(WITH PARSER ngram,索引名ft_product_name); - 索引未就绪 / 其他数据库 / 关键词含全文检索语法字符(
" ( ) * ~):自动回落ProductName LIKE @kwLike(通配符已转义,按字面匹配); - 索引由库初始化与
/shopping/dbSync/sync自动创建(ProductFullTextIndexEnsurer,幂等、失败不阻断)。
TotalSalesVolume/TotalStock都是Product上的冗余统计列(由ProductTotalsRefresher维护),排序全程不出现相关子查询 —— SqlSugar 在Includes + 分页场景下无法绑定子查询里的外表别名。
六、权限体系
| 权限前缀 | 作用域 | 示例权限 |
|---|---|---|
shop:brand:* | 品牌管理 | shop:brand:list/add/edit/delete |
shop:category:* | 分类管理 | shop:category:list/add/edit/delete |
shop:product:* | 商品管理 | shop:product:list/add/edit/delete |
shop:skus:* | SKU 管理 | shop:skus:list/add/edit/delete |
oms:order:* | 订单管理 | oms:order:list/query/delete |
oms:order:ship | 发货管理 | oms:order:ship |
oms:sale:* | 销售统计 | oms:sale:query |
spectpl:* | 规格模板 | spectpl:list/add/edit/delete |
shop:freight:* | 运费模板 | shop:freight:list/query/add/edit/delete(行政区划树复用 list) |
shop:aftersale:* | 售后管理 | shop:aftersale:list/query/audit/refund |
shop:product:ai | 商品 AI 文案 | shop:product:ai(AI 能力需 Pro 模块) |
shop:member:* | 会员管理 | shop:member:list/query/edit |
七、模块初始化与 SaaS 多租户集成
商城(含会员)的初始化分两层,均幂等(重复执行只补缺):
7.1 模块初始化(建表 + 种子):IModuleInitProvider
由 ModuleInitRunner 按 appsettings.json 的 ModuleInit 开关统一调度,--initdb 时执行:
// ZR.Mall/Service/MallModuleInitProvider.cs —— 建表 + 内置定时任务 + 商城后台菜单种子
[AppService(ServiceType = typeof(IModuleInitProvider))]
public class MallModuleInitProvider : IModuleInitProvider
{
public string ModuleName => "Mall";
public bool IsEnabled(OptionsSetting options) => options.ModuleInit.Mall;
public void Seed() { /* 内置定时任务:待付款订单超时自动取消 */ }
public List<string> SeedMenu() { /* 商城后台菜单/按钮权限种子(含会员管理) */ }
}
// ZR.ServiceCore/Member/MemberModuleInitProvider.cs —— ums_member 建表 + 自增种子校正(起始值 100000)
"ModuleInit": {
"Saas": false, "Member": false, "Mall": false, "Workflow": false, "Pro": false
}
- 初始化商城需同时开启
Member(会员表)与Mall- 存量库升级后重跑
--initdb即可补齐新增菜单(如会员管理),已有数据不受影响
7.2 租户级建表(SaaS):ITenantModuleInitializer
SaaS 模式下开通新租户时,由 MallTenantInitializer / MemberTenantInitializer / WorkflowTenantInitializer 为租户库自动建表;[Tenant("MallDb")] 实体随后按租户自动路由。商城菜单由 EnsureTenantPlanMenuSeedData 纳入默认套餐,租户开箱可见。
建表范围:
- 商城 14 张表:
mms_product、mms_product_spec、mms_skus、mms_category、mms_brand、mms_spec_template、mms_address、mms_freight_template、mms_freight_template_region、oms_order、oms_order_item、oms_payment、oms_order_log、oms_after_sale - 会员 1 张表:
ums_member - 主库公共表:
sys_area(行政区划,随库初始化自动灌入种子,与租户无关)
建表后依次执行两步"补齐"(均幂等、失败不阻断):
MallIndexEnsurer.Ensure(db)—— 补建热查询索引(订单状态+时间、订单项按订单、SKU 按商品、C 端在售商品等);索引名集中定义,[SugarIndex]特性直接引用同一批常量,不会出现"特性里一个名、补索引时另一个名";ProductFullTextIndexEnsurer.Ensure(db)—— 补建商品名全文索引(未就绪时 C 端搜索自动回落LIKE)。
存量库需要手动补索引
[SugarIndex] 只在建表那一刻由 CodeFirst 生效 —— 存量库与存量租户库不会自动获得事后新增的索引。升级后请任选其一:
- 重跑
dotnet run --project ZR.Admin.WebApi -- --initdb; - 或在后台点一次「商城库同步」(
POST /shopping/dbSync/sync,可先用GET /shopping/dbSync/diff预览差异)。
两处入口共用同一套索引/全文索引补齐逻辑(只加不改不删,对存量库无破坏性)。大表建非聚集索引可能耗时,建议低峰执行。另外索引补齐刻意不放在 Web 启动链路,避免每次启动都触发 DDL。
内置定时任务(EnsureTasksSeedData,幂等;已存在时不覆盖运营在后台改过的 Cron):
| 任务 | 作用 | 相关配置 |
|---|---|---|
Job_ClosePendingOrder | 未支付订单超时自动关闭并回补库存(分批处理) | MallSettings:ClosePendingMinutes / ClosePendingBatchSize |
Job_AutoConfirmReceipt | 发货后超期自动确认收货 | MallSettings:AutoConfirmReceiptDays |
八、配置项(appsettings.json → MallSettings)
商城参数集中经 MallSettingsReader 读取,业务代码不再散落读取配置:
"MallSettings": {
"AutoConfirmReceiptDays": 7, // 发货后自动确认收货天数(Job_AutoConfirmReceipt);配 0 或非法值按 7
"AfterSaleDays": 15, // 确认收货后可申请售后天数(0 表示不限,非法值按 15)
"AllowMockPay": false, // 是否允许模拟支付(WechatPay:Enabled=false 且此开关为 true 时才可直接标记已付)
"ClosePendingMinutes": 30, // 未支付订单超时关闭分钟数(Job_ClosePendingOrder);非正数按 30
"ClosePendingBatchSize": 500, // 超时关单每轮最多处理订单数(上限 1000,受回补库存 IN 参数限制)
"WaitDeliveryExportLimit": 50000 // 待发货导出行数上限
}
模拟支付必须显式开启
AllowMockPay 默认 false。只有真实支付渠道未启用且该开关为 true 时才允许把订单直接标记为已支付 —— 生产环境必须保持 false,否则任何登录用户都能"0 元购"。
