简介
本技术文档聚焦 DouPHP 的产品服务,围绕商品信息管理、分类管理、属性管理、库存管理等业务逻辑,系统阐述 ProductService 的职责边界、数据流、与模型层及价格服务的协作方式,并提供可操作的开发示例与优化建议。文档同时覆盖后台管理与前台展示两条链路,帮助开发者快速理解并扩展产品能力。
项目结构
DouPHP 将“产品”能力拆分为后台(Admin)与前台(Front)两套控制器与服务,并通过共享的定价服务完成促销价/会员价计算;数据访问由各自模块的 Model 封装,提供筛选、排序、预加载等能力。
graph TB
subgraph "后台 Admin"
AC["ProductController"]
ASvc["ProductService"]
AModel["Product(Model)"]
end
subgraph "前台 Front"
FC["ProductController"]
FSvc["ProductService"]
FModel["Product(Model)"]
end
PS["PricingService(定价服务)"]
AC --> ASvc
ASvc --> AModel
FC --> FSvc
FSvc --> FModel
ASvc -.-> PS
FSvc -.-> PS
核心组件
- 后台 ProductService:负责商品列表构建、新增/更新/删除、批量操作、缩略图重建、型号关联等后台业务流程。
- 前台 ProductService:负责商品分类列表页数据构建、详情页数据构建、API 属性选中后的价格计算、型号列表等。
- 模型层:
- 后台 Product:定义白名单、筛选 scope、默认排序、删除前清理附件与多语言记录、型号串生成与管理。
- 前台 Product:定义 casts/prefetchers、关联分类/品牌、上架过滤、归档/品牌筛选、默认排序等。
- 定价服务 PricingService:统一计算原价/促销价/会员价,支持等级价序列化与请求级缓存。
架构总览
下图展示了从控制器到服务再到模型与定价服务的调用链,以及关键的数据加工点(如价格计算、Markdown 渲染、附件处理)。
sequenceDiagram
participant C as "控制器"
participant S as "ProductService"
participant M as "Product(Model)"
participant P as "PricingService"
C->>S : 构建列表/详情数据
S->>M : 查询商品(含筛选/分页/预加载)
S->>P : 计算销售价/会员价
P-->>S : 返回价格结构
S-->>C : 组装视图数据(格式化/URL/相册)
Note over S,P : 列表/详情均复用定价服务
详细组件分析
后台 ProductService 分析
职责要点
- 列表数据:按分类/关键词筛选,分页并补齐模板字段(分类名、图片、会员价档位等)。
- 新增流程:校验草稿令牌与管理员ID,处理会员价序列化、内容XSS与远程图片本地化、主图上传、附件认领、写日志。
- 编辑流程:读取记录并转换为模板格式(图片URL、型号列表、自定义字段换行、Markdown 转 HTML)。
- 更新流程:校验存在性,处理主图/正文与会员价,持久化并写日志。
- 缩略图重建:获取待处理文件列表,逐条生成缩略图并输出进度脚本。
- 型号关联:为商品生成唯一 model 串,支持添加/清除关联条目,返回列表片段。
- 删除与批量操作:二次确认删除、批量改分类、批量删除,写审计日志。
关键实现要点
- 列表构建使用 AR with('category') 预加载分类,field 限定字段提升性能。
- 新增/更新通过 attachment() 存储主图与内容图片,结合 draft token 管理草稿。
- 会员价通过 PricingService::levelPrice 序列化为字符串存储。
- Markdown 内容通过 MarkdownRenderer 转为 HTML 供模板渲染。
- 删除前会触发模型层的 purgeRelatedOnDelete,清理图库与多语言记录。
flowchart TD
Start(["新增提交"]) --> Validate["校验草稿令牌/管理员ID"]
Validate --> LevelPrice{"是否启用用户功能且有level_price?"}
LevelPrice --> |是| Serialize["调用定价服务序列化等级价"]
LevelPrice --> |否| SkipLevel["跳过等级价处理"]
Serialize --> Content["XSS过滤/远程图片本地化"]
SkipLevel --> Content
Content --> Save["写入商品记录"]
Save --> UploadMain["上传主图并回写"]
UploadMain --> Claim["认领草稿附件"]
Claim --> Log["写后台操作日志"]
Log --> End(["返回新ID"])
前台 ProductService 分析
职责要点
- 分类列表:支持品牌筛选、归档区间、排序选项;批量预热附件与 URL;计算销售价;补充收藏态。
- 详情页:仅返回商品主体,格式化价格、计算销售价、加载相册、品牌信息、型号列表、Markdown 内容。
- API 属性选中后价格:根据属性选择累加价格变化,同步调整积分与售价。
- 型号列表:按 model 字段聚合同型号商品,返回标题、图片与链接。
关键实现要点
- 列表查询使用 with('category') 预加载分类,prefetchers 预热附件与首图。
- 价格计算统一走 PricingService::salePrice,支持促销价优先、会员等级价次之。
- 收藏状态通过 favorites 模块动态注入。
- API 场景下额外返回 stock/sales/sales_percentage 等统计字段。
sequenceDiagram
participant C as "前台控制器"
participant S as "前台ProductService"
participant M as "前台Product(Model)"
participant P as "PricingService"
C->>S : buildProductListData(...)
S->>M : with('category')->published()->filterByBrand()->paginate()
S->>P : salePrice('product', id, userId, itemData)
P-->>S : {value, format, type, name}
S-->>C : 返回 product_list/pager/sort_list/brand
模型层交互与数据验证
- 后台 Product:
- fillable 白名单限制可批量写入字段,避免越权赋值。
- scopeFilterByKeyword、scopeApplyDefaultOrder、scopePublished 等用于列表查询。
- ensureOrCreateModelNumber / setModelById / clearModelByModelString / clearModelById 管理型号串。
- 删除前清理图库与多语言记录,保证数据一致性。
- 前台 Product:
- casts 与 prefetchers 在 toArray 时自动格式化 image/defined/created_at,并预热 URL、多语言、附件与首图。
- scopeFilterByArchive、scopeFilterByBrand、scopeImageNotEmpty、scopeApplyDefaultOrder 等组合查询条件。
- related 方法用于推荐商品,内置 forUser 以注入会员视图字段。
价格计算与促销活动处理
- 定价优先级:促销价 > 会员等级价(精确匹配或折扣)> 原价。
- 等级价序列化:后台保存 level_price 数组为序列化字符串,前端/定价服务按需反序列化。
- 请求级缓存:PricingService 对 user_level 查询结果进行请求级缓存,减少重复查询。
- 属性联动:前台 API 场景下,属性选中带来的价格变动会同步累加至 price 与 sale_price,并折算积分。
与库存、订单的集成点
- 订单付款后扣减库存并增加销量:当开启库存功能时,订单状态机在付款后将对应商品的 stock 扣减、sales 累加。
- 商品列表/详情中展示 stock/sales 等字段,便于前端展示售罄率等指标。
依赖关系分析
- 控制器依赖服务:前后端控制器分别依赖各自的 ProductService,负责参数解析、路由与视图组装。
- 服务依赖模型:通过 AR 查询、scope 组合、with 预加载、prefetchers 预热,降低 N+1 查询。
- 服务依赖定价服务:统一价格计算,屏蔽会员等级与促销策略细节。
- 附件与内容:通过 attachment()、MarkdownRenderer 处理图片与富文本。
classDiagram
class Admin_ProductController
class Admin_ProductService
class Admin_Product_Model
class Front_ProductController
class Front_ProductService
class Front_Product_Model
class PricingService
Admin_ProductController --> Admin_ProductService : "调用"
Admin_ProductService --> Admin_Product_Model : "AR查询/Scope"
Front_ProductController --> Front_ProductService : "调用"
Front_ProductService --> Front_Product_Model : "AR查询/Scope"
Admin_ProductService --> PricingService : "等级价/原价"
Front_ProductService --> PricingService : "销售价"
性能考虑
- 预加载与缓存
- 列表使用 with('category') 预加载分类,避免 N+1。
- 前台模型 prefetchers 预热 URL、多语言、附件与首图,减少重复 IO。
- 定价服务对用户等级查询做请求级缓存,避免重复查库。
- 字段裁剪
- 后台列表 field 限定必要字段,减少传输体积。
- 流式处理
- 缩略图重建采用流式输出,边处理边刷新进度,避免长时间阻塞。
- 索引与查询
- 合理配置 category_id、brand_id、status、created_at 等常用筛选字段索引,提升查询效率。
- 资源释放
- 删除商品时清理图库与多语言记录,防止孤儿数据占用空间。
故障排查指南
常见问题与定位思路
- 新增/更新失败
- 检查表单校验规则(FormRequest)与必填字段;确认草稿令牌与管理员ID有效。
- 查看异常抛出位置(DomainException),通常来自非法参数或记录不存在。
- 价格显示异常
- 确认 pricingService 的 salePrice 返回值类型与优先级(促销价优先于会员价)。
- 检查 level_price 是否已正确序列化/反序列化。
- 列表无数据
- 检查 published scope 是否过滤了未上架商品。
- 确认分类/品牌筛选条件是否正确传入。
- 缩略图未生成
- 核对磁盘配置 thumb_directory、image_quality 与 site.thumb_width/height。
- 确认原图路径与缩略图命名规则一致。
- 删除后仍有附件残留
- 确认模型层 purgeRelatedOnDelete 是否执行;检查 file 表 module=item_id 关联。
结论
DouPHP 的产品服务通过清晰的职责划分与模块化设计,实现了商品信息的完整生命周期管理。后台 ProductService 专注管理流程与数据准备,前台 ProductService 专注展示与价格计算,两者共同依托模型层与定价服务,形成高内聚、低耦合的业务体系。借助预加载、缓存、流式处理等手段,系统在可读性与性能之间取得良好平衡。
附录:开发示例与最佳实践
-
添加新产品(后台)
- 步骤:构造表单数据 -> 调用 insert -> 处理主图与内容图片 -> 写日志 -> 返回新ID。
- 关键点:确保草稿令牌与管理员ID有效;如需会员价,传入 level_price 数组。
- 参考路径
- 新增流程:174-213
- 控制器提交入口:146-153
-
更新商品信息(后台)
- 步骤:校验存在性 -> 处理主图/内容/会员价 -> 持久化 -> 写日志。
- 关键点:update 仅允许 fillable 白名单字段;内容需 XSS 过滤。
- 参考路径
- 更新流程:260-294
- 控制器更新入口:211-217
-
处理商品状态变更(后台)
- 步骤:通过列表或编辑页修改 status -> 保存 -> 写日志。
- 关键点:前台列表默认只展示 published 商品;注意状态变更对前台可见性的影响。
- 参考路径
- 上架过滤:223-226
- 后台默认排序/筛选:125-130
-
计算商品销售价(前台)
- 步骤:调用 salePrice('product', id, userId, itemData) -> 返回价格结构 -> 格式化展示。
- 关键点:促销价优先;会员等级价次之;未登录或未启用用户功能时返回原价。
- 参考路径
- 销售价计算:75-167
- 详情页价格应用:234-235
-
属性选中后价格联动(前台 API)
- 步骤:获取基础商品 -> 计算 salePrice -> 累加属性价格变化 -> 同步积分 -> 返回 box。
- 关键点:属性价格变动需同时影响 price 与 sale_price,并按汇率折算积分。
- 参考路径
- 属性价格联动:311-344
-
型号关联与列表(后台/前台)
- 步骤:ensureOrCreateModelNumber -> add/del 操作 -> 返回 HTML 片段或列表。
- 关键点:同 model 的商品归为一组,便于前端展示与跳转。
- 参考路径
- 后台型号关联:373-417
- 前台型号列表:267-291
-
批量缩略图重建(后台)
- 步骤:buildThumbData 获取待处理文件 -> thumbFlush 逐条生成缩略图 -> 输出进度脚本。
- 关键点:注意磁盘配置与质量参数;流式输出避免超时。
- 参考路径
- 缩略图重建:302-343
-
最佳实践
- 始终通过 FormRequest 进行参数校验,Service 层只做业务逻辑。
- 列表查询尽量使用 with/prefetchers,避免 N+1。
- 价格计算统一走 PricingService,不要在各处重复实现。
- 删除操作务必清理关联附件与多语言记录,保持数据整洁。
- 对敏感字段(如 content)进行 XSS 过滤,对外部图片进行本地化处理。