pictastudio/venditio 问题修复 & 功能扩展

解决BUG、新增功能、兼容多环境部署,快速响应你的开发需求

邮箱:yvsm@zunyunkeji.com | QQ:316430983 | 微信:yvsm316

pictastudio/venditio

Composer 安装命令:

composer require pictastudio/venditio

包简介

ecommerce package

README 文档

README

Latest Version on Packagist GitHub Tests Action Status GitHub Code Style Action Status Total Downloads

Venditio is a headless ecommerce package for Laravel. It provides API-only ecommerce primitives while host applications own auth, frontend, and rendering. Products can be organized through brands, categories, tags, and flat product collections. Orders can also expose configurable return reasons and return requests with per-line derived return state.

Installation

composer require pictastudio/venditio

Install Venditio

php artisan venditio:install

Documentation

  • Architecture: docs/ARCHITECTURE.md
  • API reference: docs/API.md
  • Database schema (DBML): database.dbml

Product Variants Model

Venditio models variants using a parent/child product strategy:

  • A base product is a row in products with parent_id = null
  • Each purchasable variant is another row in products with parent_id set to the base product id
  • Variant axes live in product_variants (for example Color, Size)
  • Axis values live in product_variant_options (for example Red, M)
  • Assigned option values for each variant product are stored in product_configuration

This keeps a single product identity while still allowing independent SKU/inventory/pricing per concrete variant.

Configuration

All behavior is configured through config/venditio.php.

Key sections

  • routes.api: route enable/prefix/name/middleware/pagination and resource wrapping
  • models: model overrides (all package models are replaceable)
  • validations: validation contract to implementation bindings
  • authorize_using_policies: optional policy/gate authorization
  • price_lists: optional multi-price feature
  • discounts: discount calculator/bindings/rules configuration
  • shipping: shipping strategy, default volumetric divisor, and resolver bindings
  • product: product enums, sku generator and product list variant visibility defaults
  • product_variants: variant naming/copy behavior
  • invoices: optional persisted invoice generation and swappable PDF pipeline

User model and auth integration

Authentication is not enforced by default. If your host app uses Sanctum, add HasApiTokens to your user model and point the package user model config to it:

namespace App\Models;

use Laravel\Sanctum\HasApiTokens;
use PictaStudio\Venditio\Models\User as VenditioUser;

class User extends VenditioUser
{
    use HasApiTokens;
}
'models' => [
    // ...
    'user' => App\Models\User::class,
],

Optional policy integration

Register policies in the host app and keep venditio.authorize_using_policies enabled:

use App\Models\Product;
use App\Policies\ProductPolicy;
use Illuminate\Support\Facades\Gate;

public function boot(): void
{
    Gate::policy(Product::class, ProductPolicy::class);
}

Controllers call authorization only when enabled and when a policy/gate definition exists.

Validation customization

Validation rules are resolved from contracts in config('venditio.validations'). Override a resource by rebinding its contract to your implementation.

use App\Validations\AddressValidation;
use PictaStudio\Venditio\Validations\Contracts\AddressValidationRules;

public function boot(): void
{
    $this->app->singleton(AddressValidationRules::class, AddressValidation::class);
}

Identifier generator customization

use PictaStudio\Venditio\Contracts\CartIdentifierGeneratorInterface;
use PictaStudio\Venditio\Contracts\InvoiceNumberGeneratorInterface;
use PictaStudio\Venditio\Contracts\OrderIdentifierGeneratorInterface;

$this->app->singleton(CartIdentifierGeneratorInterface::class, App\Generators\CartIdentifierGenerator::class);
$this->app->singleton(InvoiceNumberGeneratorInterface::class, App\Generators\InvoiceNumberGenerator::class);
$this->app->singleton(OrderIdentifierGeneratorInterface::class, App\Generators\OrderIdentifierGenerator::class);

Invoices

Venditio can persist one immutable invoice document per order and render a PDF from the stored snapshot. The feature is disabled by default and stays API-only: host apps decide when to create an invoice and can replace the default number generator, payload factory, HTML template, or PDF renderer.

Enable it in config/venditio.php:

'invoices' => [
    'enabled' => true,
    'seller' => [
        'name' => 'Acme SRL',
        'address_line_1' => 'Via Roma 1',
        'city' => 'Verona',
        'postal_code' => '37100',
        'country' => 'Italy',
    ],
],

Default endpoints:

  • POST /orders/{order}/invoice
  • GET /orders/{order}/invoice
  • GET /orders/{order}/invoice/pdf

The generated invoice record stores seller data, billing/shipping addresses, lines, totals, payments, and rendered HTML so later order edits do not rewrite already issued documents.

Invoice customization

'invoices' => [
    'number_generator' => App\Invoices\CustomInvoiceNumberGenerator::class,
    'payload_factory' => App\Invoices\CustomInvoicePayloadFactory::class,
    'template' => App\Invoices\CustomInvoiceTemplate::class,
    'renderer' => App\Invoices\CustomInvoicePdfRenderer::class,
],

Relevant contracts:

  • PictaStudio\Venditio\Contracts\InvoiceNumberGeneratorInterface
  • PictaStudio\Venditio\Contracts\InvoicePayloadFactoryInterface
  • PictaStudio\Venditio\Contracts\InvoiceTemplateInterface
  • PictaStudio\Venditio\Contracts\InvoicePdfRendererInterface

Shipping

Venditio ships with an API-first shipping domain built around three resources:

  • shipping_methods: couriers or delivery methods, with flat_fee and volumetric_divisor
  • shipping_zones: geographic scopes, linked to countries, regions, and provinces
  • shipping_method_zones: the priced pivot between a method and a zone, with rate_tiers and over_weight_price_per_kg

Shipping behavior is controlled by venditio.shipping.strategy:

  • disabled: shipping fee is always 0, but weights are still calculated
  • flat: the cart uses shipping_methods.flat_fee
  • zones: the cart resolves the best matching zone for the selected method and calculates the fee from the pivot row

The default volumetric divisor is 5000, configurable through venditio.shipping.default_volumetric_divisor. Each shipping method can override it with its own volumetric_divisor, so different couriers can use different volumetric rules.

How shipping is calculated

On cart create and update, Venditio calculates shipping after line totals and before cart-level discounts.

  1. The cart resolves the selected shipping_method_id.
  2. It calculates the line weights from cart.lines[*].product_data.
  3. It resolves the destination from addresses.shipping.
  4. If the strategy is zones, it finds the best active zone linked to the selected shipping method.
  5. It calculates the shipping fee.
  6. Discounts run after that, so free_shipping can still zero the final shipping_fee.
  7. When an order is created from a cart, Venditio snapshots shipping_method_id, shipping_zone_id, weights, fee, shipping_method_data, and shipping_zone_data on the order.

Weight calculation uses these formulas:

  • specific_weight = sum(product_data.weight * qty)
  • volumetric_weight = sum((length * width * height / divisor) * qty)
  • chargeable_weight = max(specific_weight, volumetric_weight)

Expected units in the default implementation:

  • weight in kg
  • length, width, height in cm

Destination matching works by specificity:

  • province match wins over region match
  • region match wins over country match
  • if two zones have the same specificity, the highest shipping_zones.priority wins
  • if priority is also equal, the lowest id wins

The destination is resolved in this order:

  • use addresses.shipping.province_id when present
  • otherwise try addresses.shipping.state as a province code
  • use addresses.shipping.country_id as the country-level fallback

Practical examples

1. Flat shipping

If the host app sets:

'shipping' => [
    'strategy' => 'flat',
],

and creates a method like:

{
  "code": "express",
  "name": "Express Courier",
  "active": true,
  "flat_fee": 9.90,
  "volumetric_divisor": 5000
}

then a cart created with that method:

{
  "shipping_method_id": 1,
  "addresses": {
    "billing": { "country_id": 1 },
    "shipping": { "country_id": 1 }
  },
  "lines": [
    { "product_id": 10, "qty": 2 }
  ]
}

will use shipping_fee = 9.90 regardless of zone matching. Weights are still calculated and exposed on the cart response.

2. Province overrides region and country

Suppose one courier is linked to three active zones:

  • Italy zone with country_ids containing the Italy country id, priced at 7.00 up to 5kg
  • Lazio zone with region_ids containing the Lazio region id, priced at 9.00 up to 5kg
  • Rome zone with province_ids containing the Rome province id, priced at 12.00 up to 5kg

For a shipping address in Rome province, Venditio picks the province zone and charges 12.00. For a shipping address in Viterbo province, Venditio falls back to the Lazio region zone and charges 9.00. For a shipping address in Milan province, Venditio falls back to the Italy country zone and charges 7.00.

This is true even if all three zones are linked to the same method: the most specific destination always wins.

3. Different couriers can produce different volumetric fees

Take the same parcel with:

  • actual weight 4kg
  • dimensions 50 x 40 x 30 cm

Courier A has volumetric_divisor = 5000. Courier B has volumetric_divisor = 4000.

That produces:

  • Courier A volumetric weight: (50 * 40 * 30) / 5000 = 12kg
  • Courier B volumetric weight: (50 * 40 * 30) / 4000 = 15kg

So the chargeable weight becomes:

  • Courier A: max(4, 12) = 12kg
  • Courier B: max(4, 15) = 15kg

If both couriers are linked to the same zone but with different pricing in shipping_method_zones, the final fee can differ twice:

  • because the chargeable weight is different
  • because each method-zone pivot can have different rate_tiers or over_weight_price_per_kg

4. Incomplete destination does not block the cart

If the cart has lines but is missing shipping_method_id, or the shipping address is still incomplete, Venditio does not fail the request.

Returns

Venditio ships with an API-first returns domain that stays aligned with the package's headless approach:

  • return_reasons: configurable database-backed reasons exposed through CRUD APIs
  • return_requests: order-linked return headers that snapshot orders.addresses.billing at creation time
  • partial quantities per order_line, so a line with qty > 1 can be returned incrementally
  • derived fields on order_lines for frontend/admin use: requested_return_qty, returned_qty, has_return_requests, is_returned, is_fully_returned

return_requests do not expose return_request_lines as a standalone CRUD resource in v1. The nested lines payload is validated against the selected order, and quantities cannot exceed the remaining returnable amount for each order line. It keeps:

  • shipping_fee = 0
  • shipping_zone_id = null

This is useful for checkout flows where the customer adds products before choosing a courier or completing the address.

5. Complete destination with no valid shipping rate returns 422

In zones mode, if the cart has:

  • a valid shipping_method_id
  • a complete enough destination to resolve a province or country

but the selected method is not linked to any matching active zone, Venditio returns a validation error. The same happens if a matching zone exists but its pivot has no applicable rate_tiers and no over_weight_price_per_kg.

This makes the failure machine-readable for the host app while still allowing incomplete carts to remain valid during checkout.

Commands

Release stock for abandoned carts

Enabled by default and configurable from:

  • venditio.commands.release_stock_for_abandoned_carts.enabled
  • venditio.commands.release_stock_for_abandoned_carts.inactive_for_minutes
  • venditio.commands.release_stock_for_abandoned_carts.schedule_every_minutes

Publish Bruno collection

php artisan vendor:publish --tag=venditio-bruno

High-level structure

src/
|--- Actions
|--- Contracts
|--- Discounts
|--- Dto
|--- Enums
|--- Http
|--- Models
|--- Pipelines
|--- Pricing
|--- Validations

Testing

composer test

Changelog

Please see CHANGELOG for more information on what has changed recently.

Contributing

Please see CONTRIBUTING for details.

Security Vulnerabilities

Please review our security policy on how to report security vulnerabilities.

Credits

License

The MIT License (MIT). Please see License File for more information.

pictastudio/venditio 适用场景与选型建议

pictastudio/venditio 是一款 基于 PHP 开发的 Composer 扩展包,目前已累计 109 次下载、GitHub Stars 达 1, 最近一次更新时间为 2026 年 02 月 12 日, 在 PHP 生态内属于活跃度较高的组件。

它主要适用于以下技术方向: 「laravel」 「Picta Studio」 「venditio」 等业务场景。在实际项目中,围绕这些方向常见需要落地的问题包括:接口对接、性能调优、并发安全、与既有框架(Laravel / ThinkPHP / Yii / Webman 等)的兼容适配,以及生产环境的日志埋点与稳定性保障。

我们在过去多个企业项目中使用过 pictastudio/venditio 或与其功能相近的方案,如果你在选型或落地过程中遇到问题,例如 版本兼容、二次改造、私有化封装、与内部系统对接、生产 BUG 排查,欢迎联系我们协助评估。

围绕 pictastudio/venditio 我们能提供哪些服务?
定制开发 / 二次开发

基于 pictastudio/venditio 在你已有业务上做功能扩展、字段裁剪、UI 适配、与内部账号 / 权限 / 日志系统的深度对接。

BUG 修复 & 性能优化

线上偶发问题、内存泄漏、慢查询、并发异常等排查修复;针对高流量场景做缓存、队列、索引层面的调优。

项目外包 & 长期维护

承接完整的项目从需求 → 设计 → 开发 → 上线 → 长期运维;也可按月提供技术保姆服务。

yvsm@zunyunkeji.com QQ:316430983 微信:yvsm316 西安尊云信息科技 · 专注 PHP / Go / 分布式系统研发

统计信息

  • 总下载量: 109
  • 月度下载量: 0
  • 日度下载量: 0
  • 收藏数: 1
  • 点击次数: 19
  • 依赖项目数: 0
  • 推荐数: 0

GitHub 信息

  • Stars: 1
  • Watchers: 2
  • Forks: 0
  • 开发语言: PHP

其他信息

  • 授权协议: MIT
  • 更新时间: 2026-02-12