bitrix-sale

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Online Store (
sale
)

在线商店(
sale
模块)

sale
owns cart (basket), orders, payments, shipments, discounts, statuses, and history. Product master data, prices, stock live in
catalog
+
iblock
. Baseline: main 23.0+.
php
\Bitrix\Main\Loader::includeModule('sale');
\Bitrix\Main\Loader::includeModule('catalog'); // products, prices, stock, reservation
sale
模块管理购物车(basket)、订单、支付、发货、折扣、状态及历史记录。产品主数据、价格、库存存储在
catalog
+
iblock
模块中。基础版本要求:主版本 23.0+
php
\Bitrix\Main\Loader::includeModule('sale');
\Bitrix\Main\Loader::includeModule('catalog'); // products, prices, stock, reservation

Choosing the API

API选择

TaskAPI
Create/change basket, order, payment, shipmentObject model
Bitrix\Sale\*
(validates, saves collections, fires events, writes history)
Lists, reports, aggregatesORM
Bitrix\Sale\Internals\*Table
(
OrderTable
,
BasketTable
,
PaymentTable
,
ShipmentTable
) — read-only for order data
Settings/dictionaries via codeProfile ORM:
PersonTypeTable
,
OrderPropsTable
,
StatusTable
(+
StatusLangTable
),
OrderPropsGroupTable
— writes allowed
Pick a configured serviceManagers:
PaySystem\Manager
,
Delivery\Services\Manager
,
Cashbox\Manager
/
CheckManager
,
Services\Company\Manager
,
DiscountCouponsManager
Operations with no full D7 replacementLegacy
CSale*
:
CSaleOrder::CanUser*()
(rights),
CSaleOrderChange
(history read),
CSaleDiscount::Add/Update
(cart rules),
CSaleOrderUserProps
(buyer profiles),
CSaleUserAccount
(account balance),
CSaleOrderTax
(tax rows)
Never change an order via
OrderTable::update()
or create payments/shipments as raw ORM rows — collections, recalcs, events, and history desync. Never
Order::load()
in a loop for a list — use
OrderTable::getList()
/
Order::getList()
. Don't mix legacy
CSale*
writes with a loaded
Order
object in memory.
任务API
创建/修改购物车、订单、支付、发货对象模型
Bitrix\Sale\*
(提供校验、集合保存、事件触发、历史记录写入)
列表查询、报表、聚合统计ORM
Bitrix\Sale\Internals\*Table
OrderTable
BasketTable
PaymentTable
ShipmentTable
)——订单数据仅支持读取
通过代码配置设置/字典配置类ORM:
PersonTypeTable
OrderPropsTable
StatusTable
(+
StatusLangTable
)、
OrderPropsGroupTable
——支持写入操作
选择已配置的服务管理器类:
PaySystem\Manager
Delivery\Services\Manager
Cashbox\Manager
/
CheckManager
Services\Company\Manager
DiscountCouponsManager
无完整D7替代方案的操作遗留类
CSale*
CSaleOrder::CanUser*()
(权限校验)、
CSaleOrderChange
(读取历史记录)、
CSaleDiscount::Add/Update
(购物车规则)、
CSaleOrderUserProps
(买家配置文件)、
CSaleUserAccount
(账户余额)、
CSaleOrderTax
(税费行)
切勿通过
OrderTable::update()
修改订单,也不要以原始ORM行的方式创建支付/发货记录——这会导致集合、重新计算、事件和历史记录不同步。切勿在循环中使用
Order::load()
批量获取订单列表——请使用
OrderTable::getList()
/
Order::getList()
。不要在内存中同时混用遗留类
CSale*
的写入操作和已加载的
Order
对象。

FUSER (Cart Owner)

FUSER(购物车所有者)

Anonymous and authorized carts are keyed by FUSER (
Bitrix\Sale\Fuser
), not
USER_ID
.
php
$fuserId = Fuser::getId();               // creates if missing
$fuserId = Fuser::getId(true);           // skip create → null if none
$fuserId = Fuser::getIdByUserId($userId); // false if cannot resolve/create
USER_ID
(site account, required on saved order) and
FUSER_ID
(basket owner) are different — don't substitute one for the other.
匿名和已授权用户的购物车均以 FUSER
Bitrix\Sale\Fuser
)作为标识,而非
USER_ID
php
$fuserId = Fuser::getId();               // 若不存在则创建
$fuserId = Fuser::getId(true);           // 跳过创建 → 若不存在则返回null
$fuserId = Fuser::getIdByUserId($userId); // 若无法解析/创建则返回false
USER_ID
(站点账户,已保存订单必填)与
FUSER_ID
(购物车所有者)是不同的标识——不要相互替代。

Basket

购物车(Basket)

php
<?php declare(strict_types=1);

use Bitrix\Catalog\Product\Basket as CatalogBasket;
use Bitrix\Sale\Basket;
use Bitrix\Sale\Basket\RefreshFactory;
use Bitrix\Sale\Fuser;

$basket = Basket::loadItemsForFUser(Fuser::getId(), $siteId); // only rows with ORDER_ID = null

// Preferred for catalog products: sets module, provider, and product data itself
$r = CatalogBasket::addProductToBasket($basket, ['PRODUCT_ID' => $productId, 'QUANTITY' => 1], ['SITE_ID' => $siteId]);
// merges into an existing row by default; pass ['USE_MERGE' => 'N'] as 4th arg for a separate row

// Manual alternative:
$item = $basket->createItem('catalog', $productId);
$item->setFields(['QUANTITY' => 1, 'PRODUCT_PROVIDER_CLASS' => CatalogBasket::getDefaultProviderName()]);
$basket->refresh(RefreshFactory::createSingle($item->getBasketCode())); // provider fills PRICE/CURRENCY/NAME/VAT/weight

$result = $basket->save();                 // only for a basket NOT bound to an order
  • Don't set
    PRICE
    /
    CURRENCY
    for catalog products — the provider does. Own pricing:
    CUSTOM_PRICE => 'Y'
    +
    PRICE
    +
    CURRENCY
    .
  • With SKUs put the offer ID in
    PRODUCT_ID
    , never the parent. Verify the element is a product (
    Bitrix\Catalog\ProductTable
    ) before adding.
  • Basket of a saved order: get via
    $order->getBasket()
    , save via
    Order::save()
    — never
    loadItemsForFUser()
    /
    $basket->save()
    for it.
  • Before order creation:
    $basket->refresh()
    (
    refreshData()
    is deprecated), then
    $basket->getOrderableItems()
    — separate basket with only purchasable, non-delayed items.
  • Item properties:
    $item->getPropertyCollection()->createItem()
    /
    redefine()
    . Prices:
    getPrice()
    ,
    getBasePrice()
    ,
    getPriceWithVat()
    ,
    getDiscountPrice()
    .
  • Pre-order discounts preview:
    Discount::buildFromBasket($basket, new Discount\Context\Fuser($basket->getFUserId()))
    calculate()
    $basket->applyDiscount($data['BASKET_ITEMS'])
    . Never for an order-bound basket.
php
<?php declare(strict_types=1);

use Bitrix\Catalog\Product\Basket as CatalogBasket;
use Bitrix\Sale\Basket;
use Bitrix\Sale\Basket\RefreshFactory;
use Bitrix\Sale\Fuser;

$basket = Basket::loadItemsForFUser(Fuser::getId(), $siteId); // 仅加载ORDER_ID = null的行

// 推荐用于目录产品:自动设置模块、提供者及产品数据
$r = CatalogBasket::addProductToBasket($basket, ['PRODUCT_ID' => $productId, 'QUANTITY' => 1], ['SITE_ID' => $siteId]);
// 默认合并到现有行;若需添加独立行,可在第4个参数中传入 ['USE_MERGE' => 'N']

// 手动添加替代方案:
$item = $basket->createItem('catalog', $productId);
$item->setFields(['QUANTITY' => 1, 'PRODUCT_PROVIDER_CLASS' => CatalogBasket::getDefaultProviderName()]);
$basket->refresh(RefreshFactory::createSingle($item->getBasketCode())); // 提供者自动填充PRICE/CURRENCY/NAME/VAT/weight字段

$result = $basket->save();                 // 仅适用于未绑定订单的购物车
  • 不要为目录产品手动设置
    PRICE
    /
    CURRENCY
    ——由提供者自动处理。自定义定价需设置
    CUSTOM_PRICE => 'Y'
    +
    PRICE
    +
    CURRENCY
  • 对于SKU产品,请将变体ID填入
    PRODUCT_ID
    ,不要使用父产品ID。添加前请验证该元素为产品(
    Bitrix\Catalog\ProductTable
    )。
  • 已保存订单的购物车:通过
    $order->getBasket()
    获取,通过
    Order::save()
    保存——切勿对其使用
    loadItemsForFUser()
    /
    $basket->save()
  • 创建订单前:调用
    $basket->refresh()
    refreshData()
    已废弃),然后调用
    $basket->getOrderableItems()
    ——获取仅包含可购买、非延迟商品的独立购物车。
  • 商品属性:
    $item->getPropertyCollection()->createItem()
    /
    redefine()
    。价格相关:
    getPrice()
    getBasePrice()
    getPriceWithVat()
    getDiscountPrice()
  • 预订单折扣预览:
    Discount::buildFromBasket($basket, new Discount\Context\Fuser($basket->getFUserId()))
    calculate()
    $basket->applyDiscount($data['BASKET_ITEMS'])
    。切勿对绑定订单的购物车使用此方法。

Order Create (Pipeline)

订单创建流程

Order of operations matters: basket → order → person type → basket in → properties → shipment → delivery calc → payment →
doFinalAction(true)
→ sync payment SUM → re-check restrictions →
save()
.
php
<?php declare(strict_types=1);

use Bitrix\Sale\Delivery\Services\Manager as DeliveryManager;
use Bitrix\Sale\Order;
use Bitrix\Sale\PaySystem\Manager as PaySystemManager;
use Bitrix\Sale\Services\Base\RestrictionManager;

$order = Order::create($siteId, $userId); // currency: site's, else base
$order->setPersonTypeId($personTypeId);   // BEFORE getPropertyCollection(); not validated vs site
$order->setBasket($orderableBasket);      // new (unsaved) order only

// Properties (set depends on person type)
$prop = $order->getPropertyCollection()->getItemByOrderPropertyCode('PHONE');
$prop?->setValue($phone);                 // each setValue returns Result

// Shipment: create user shipment, bind basket items, pick allowed delivery
$shipment = $order->getShipmentCollection()->createItem(); // system shipment exists already — never assign it a service
foreach ($order->getBasket() as $basketItem) {
    $shipmentItem = $shipment->getShipmentItemCollection()->createItem($basketItem);
    $shipmentItem->setQuantity($basketItem->getQuantity());
}
$deliveries = DeliveryManager::getRestrictedObjectsList($shipment);
$shipment->setDeliveryService($deliveries[$deliveryId] ?? throw new \RuntimeException('delivery unavailable'));
$order->getShipmentCollection()->calculateDelivery();

// Payment: create, preliminary SUM, pick allowed pay system
$payment = $order->getPaymentCollection()->createItem();
$payment->setField('SUM', $order->getPrice());
$allowed = PaySystemManager::getListWithRestrictions($payment, RestrictionManager::MODE_CLIENT);
isset($allowed[$paySystemId]) or throw new \RuntimeException('pay system unavailable');
$payment->setPaySystemService(PaySystemManager::getObjectById($paySystemId));

$order->doFinalAction(true);              // discounts, taxes, totals — check Result
$payment->setField('SUM', $order->getPrice()); // sync after final calc
// re-check getRestrictedObjectsList / getListWithRestrictions here — totals may change availability

$saveResult = $order->save();             // check isSuccess() AND getWarningMessages()
$orderId = $saveResult->getId();
Payment and user shipment are optional at first save (digital goods, deferred flows) — skip those blocks; add later on the loaded order. Idempotency for integrations: store operation key yourself (
XML_ID
is not unique-constrained). Load later:
Order::load($id)
,
Order::loadByAccountNumber($number)
,
Order::loadByFilter([...])
; lock while editing with
Order::lock()/isLocked()/unlock()
.
操作顺序至关重要:购物车 → 订单 → 人员类型 → 购物车导入 → 属性 → 发货 → 配送计算 → 支付 →
doFinalAction(true)
→ 同步支付金额 → 重新校验限制 →
save()
php
<?php declare(strict_types=1);

use Bitrix\Sale\Delivery\Services\Manager as DeliveryManager;
use Bitrix\Sale\Order;
use Bitrix\Sale\PaySystem\Manager as PaySystemManager;
use Bitrix\Sale\Services\Base\RestrictionManager;

$order = Order::create($siteId, $userId); // 货币默认使用站点货币,否则使用基础货币
$order->setPersonTypeId($personTypeId);   // 调用getPropertyCollection()前设置;不校验站点关联
$order->setBasket($orderableBasket);      // 仅适用于新的未保存订单

// 属性设置(取决于人员类型)
$prop = $order->getPropertyCollection()->getItemByOrderPropertyCode('PHONE');
$prop?->setValue($phone);                 // 每个setValue调用返回Result对象

// 发货:创建用户发货记录,绑定购物车商品,选择可用配送方式
$shipment = $order->getShipmentCollection()->createItem(); // 系统发货记录已存在——切勿为其分配服务
foreach ($order->getBasket() as $basketItem) {
    $shipmentItem = $shipment->getShipmentItemCollection()->createItem($basketItem);
    $shipmentItem->setQuantity($basketItem->getQuantity());
}
$deliveries = DeliveryManager::getRestrictedObjectsList($shipment);
$shipment->setDeliveryService($deliveries[$deliveryId] ?? throw new \RuntimeException('配送不可用'));
$order->getShipmentCollection()->calculateDelivery();

// 支付:创建支付记录,初始化金额,选择可用支付系统
$payment = $order->getPaymentCollection()->createItem();
$payment->setField('SUM', $order->getPrice());
$allowed = PaySystemManager::getListWithRestrictions($payment, RestrictionManager::MODE_CLIENT);
isset($allowed[$paySystemId]) or throw new \RuntimeException('支付系统不可用');
$payment->setPaySystemService(PaySystemManager::getObjectById($paySystemId));

$order->doFinalAction(true);              // 计算折扣、税费、总计——请检查Result对象
$payment->setField('SUM', $order->getPrice()); // 最终计算后同步金额
// 此处重新校验getRestrictedObjectsList / getListWithRestrictions——金额变化可能影响服务可用性

$saveResult = $order->save();             // 检查isSuccess() 以及 getWarningMessages()
$orderId = $saveResult->getId();
首次保存订单时,支付和用户发货记录为可选(如数字商品、延迟流程)——可跳过相关代码块;后续可在已加载的订单中添加。集成幂等性:自行存储操作标识(
XML_ID
无唯一约束)。后续加载订单:
Order::load($id)
Order::loadByAccountNumber($number)
Order::loadByFilter([...])
;编辑时通过
Order::lock()/isLocked()/unlock()
加锁。

Order Update

订单更新

Work on one loaded object, save once. After a change decide what to rerun:
ChangecalculateDeliverydoFinalAction(true)sync unpaid payments SUM
Status, cancel, mark, comment, tracking, allow-delivery
Location/address in restrictionsyesyesif price changed
Basket items/quantity; delivery service/cost; shipment removalyesyesif price changed
Coupon/discount/tax dataif delivery affectedyesif price changed
  • Quantity down: reduce
    ShipmentItem::setQuantity()
    first, then
    BasketItem::setField('QUANTITY')
    ; up: basket first, then shipment. Then
    refresh
    the item, recalc, save.
  • Cancel via
    setField('CANCELED', 'Y')
    (+
    REASON_CANCELED
    ); blocked while a paid payment or shipped shipment exists.
    Order::delete()
    is a service-only hard delete — never use for customer refusal.
  • PERSON_TYPE_ID
    change is a migration (property values are not remapped).
    CURRENCY
    /
    USER_ID
    are not changeable via
    setField()
    . Don't write
    SUM_PAID
    /
    PAYED
    directly.
操作单个已加载对象,仅保存一次。修改后需决定重新执行哪些操作:
修改内容calculateDeliverydoFinalAction(true)同步未支付金额
状态、取消、标记、备注、追踪号、允许配送
限制条件中的位置/地址若价格变化则同步
购物车商品/数量;配送服务/费用;移除发货记录若价格变化则同步
优惠券/折扣/税费数据若影响配送则执行若价格变化则同步
  • 减少数量:先修改
    ShipmentItem::setQuantity()
    ,再修改
    BasketItem::setField('QUANTITY')
    ;增加数量:先修改购物车,再修改发货记录。然后刷新商品、重新计算、保存。
  • 取消订单:调用
    setField('CANCELED', 'Y')
    (+
    REASON_CANCELED
    );若存在已支付的支付记录或已发货的记录,则无法取消。
    Order::delete()
    仅为服务端硬删除——切勿用于处理用户取消请求。
  • 修改
    PERSON_TYPE_ID
    属于迁移操作(属性值不会自动映射)。
    CURRENCY
    /
    USER_ID
    无法通过
    setField()
    修改。不要直接写入
    SUM_PAID
    /
    PAYED
    字段。

Order Properties

订单属性

Setting (
OrderPropsTable
, bound to a person type;
ENTITY_TYPE
ORDER/SHIPMENT) vs value in an order (
PropertyValueCollection
). Create settings via
OrderPropsGroupTable::add()
+
OrderPropsTable::add()
in migrations, never during checkout.
  • Find values:
    getItemByOrderPropertyCode()
    (first match),
    getItemByOrderPropertyId()
    , by role:
    getDeliveryLocation()
    , groups via
    getGroups()
    .
  • LOCATION
    takes the internal location code, not a name.
    ENUM
    takes variant
    VALUE
    (options via
    $propertyValue->getPropertyObject()->getOptions()
    );
    MULTIPLE=Y
    takes an array. Files/forms:
    PropertyValueCollection::setValuesFromPost($_POST, $_FILES)
    +
    verify()
    .
  • Required check before save: iterate collection,
    isRequired()
    +
    checkRequiredValue()
    .
  • Values save with
    Order::save()
    only; never write
    OrderPropsValueTable
    directly.
属性设置(
OrderPropsTable
,关联人员类型;
ENTITY_TYPE
为 ORDER/SHIPMENT)与订单中的属性值(
PropertyValueCollection
)是不同概念。请在迁移脚本中通过
OrderPropsGroupTable::add()
+
OrderPropsTable::add()
创建属性设置,切勿在结账流程中创建。
  • 查询属性值:
    getItemByOrderPropertyCode()
    (首个匹配项)、
    getItemByOrderPropertyId()
    、按角色查询:
    getDeliveryLocation()
    、通过
    getGroups()
    查询分组。
  • LOCATION
    字段需填入内部位置编码,而非名称。
    ENUM
    字段需填入选项的
    VALUE
    (选项可通过
    $propertyValue->getPropertyObject()->getOptions()
    获取);
    MULTIPLE=Y
    时需传入数组。文件/表单:
    PropertyValueCollection::setValuesFromPost($_POST, $_FILES)
    +
    verify()
  • 保存前校验必填项:遍历集合,调用
    isRequired()
    +
    checkRequiredValue()
  • 属性值仅通过
    Order::save()
    保存;切勿直接写入
    OrderPropsValueTable

Statuses, Permissions

状态、权限

  • Order:
    STATUS_ID
    , initial
    N
    , final
    F
    , class
    Bitrix\Sale\OrderStatus
    . Shipment: own
    STATUS_ID
    ,
    DN
    DF
    , class
    DeliveryStatus
    . Dictionary
    StatusTable
    (
    TYPE_ORDER
    /
    TYPE_SHIPMENT
    ) +
    StatusLangTable
    names.
  • Allowed transitions for a user:
    OrderStatus::getAllowedUserStatuses($userId, $currentStatusId)
    ; operations per status:
    getStatusesUserCanDoOperations()
    ,
    canGroupDoOperations()
    (operations:
    view
    ,
    update
    ,
    delete
    ,
    cancel
    ,
    mark
    ,
    payment
    ,
    delivery
    ,
    deduction
    ,
    from
    ,
    to
    ).
  • Object API does not check rights. Before acting on a user request check the concrete order via legacy
    CSaleOrder
    :
    CanUserViewOrder()
    ,
    CanUserUpdateOrder()
    (pass
    0, $groups, $siteId
    for create),
    CanUserCancelOrder()
    ,
    CanUserChangeOrderStatus()
    ,
    CanUserChangeOrderFlag($id, 'PERM_PAYMENT'|'PERM_DELIVERY'|'PERM_DEDUCTION', $groups)
    ,
    CanUserDeleteOrder()
    . Check view rights before
    Order::load()
    .
  • Module levels:
    D
    denied,
    P
    company binding,
    U
    order processing (still needs site + status-task grants),
    W
    full.
  • History: written by
    OrderHistory
    on save; read via legacy
    CSaleOrderChange::GetList()
    (
    @TYPE => ['ORDER_STATUS_CHANGED', ...]
    ).
  • 订单:
    STATUS_ID
    ,初始状态为
    N
    ,最终状态为
    F
    ,对应类
    Bitrix\Sale\OrderStatus
    。发货记录:独立的
    STATUS_ID
    ,状态流转为
    DN
    DF
    ,对应类
    DeliveryStatus
    。状态字典存储在
    StatusTable
    TYPE_ORDER
    /
    TYPE_SHIPMENT
    ) +
    StatusLangTable
    (状态名称)中。
  • 用户允许的状态流转:
    OrderStatus::getAllowedUserStatuses($userId, $currentStatusId)
    ;各状态允许的操作:
    getStatusesUserCanDoOperations()
    canGroupDoOperations()
    (操作包括:
    view
    update
    delete
    cancel
    mark
    payment
    delivery
    deduction
    from
    to
    )。
  • 对象API不校验权限。响应用户请求前,请通过遗留类
    CSaleOrder
    校验具体订单权限:
    CanUserViewOrder()
    CanUserUpdateOrder()
    (创建时传入
    0, $groups, $siteId
    )、
    CanUserCancelOrder()
    CanUserChangeOrderStatus()
    CanUserChangeOrderFlag($id, 'PERM_PAYMENT'|'PERM_DELIVERY'|'PERM_DEDUCTION', $groups)
    CanUserDeleteOrder()
    加载订单前先校验查看权限
  • 模块权限级别:
    D
    拒绝、
    P
    公司绑定、
    U
    订单处理(仍需站点+状态任务授权)、
    W
    完全权限。
  • 历史记录:保存时由
    OrderHistory
    写入;通过遗留类
    CSaleOrderChange::GetList()
    读取(
    @TYPE => ['ORDER_STATUS_CHANGED', ...]
    )。

Events

事件

Register via
EventManager
in
init.php
. Key ones:
OnSaleOrderBeforeSaved
(may modify/deny),
OnSaleOrderSaved
(
IS_NEW
,
IS_CHANGED
; result ignored), deferred after save:
OnSaleStatusOrderChange
(
VALUE
/
OLD_VALUE
),
OnSaleOrderPaid
,
OnSaleOrderCanceled
,
OnSaleStatusShipmentChange
,
OnShipmentDeducted
,
OnShipmentAllowDelivery
,
OnShipmentTrackingNumberChange
; per-entity
On[Before]Sale{BasketItem,Payment,Shipment,ShipmentItem,PropertyValue}SetField
and
OnSale*EntitySaved
; basket:
OnSaleBasketItemBeforeSaved/Saved
,
OnSaleBasketItemRefreshData
; final calc:
On{Before,After}SaleOrderFinalAction
.
Never call
$order->save()
from
OnSaleOrderSaved
— recursion. Mutate in
OnSaleOrderBeforeSaved
instead, or queue a job that reloads the order.
OnBefore*
handlers returning
EventResult::ERROR
surface as
setField()
/
save()
errors.
init.php
中通过
EventManager
注册事件。关键事件:
OnSaleOrderBeforeSaved
(可修改/拒绝订单)、
OnSaleOrderSaved
(包含
IS_NEW
IS_CHANGED
;忽略返回结果)、保存后延迟触发事件:
OnSaleStatusOrderChange
(包含
VALUE
/
OLD_VALUE
)、
OnSaleOrderPaid
OnSaleOrderCanceled
OnSaleStatusShipmentChange
OnShipmentDeducted
OnShipmentAllowDelivery
OnShipmentTrackingNumberChange
;各实体对应的
On[Before]Sale{BasketItem,Payment,Shipment,ShipmentItem,PropertyValue}SetField
OnSale*EntitySaved
;购物车相关:
OnSaleBasketItemBeforeSaved/Saved
OnSaleBasketItemRefreshData
;最终计算相关:
On{Before,After}SaleOrderFinalAction
切勿在
OnSaleOrderSaved
中调用
$order->save()
——会导致递归。请在
OnSaleOrderBeforeSaved
中修改订单,或队列任务重新加载订单后修改。
OnBefore*
处理器返回
EventResult::ERROR
会作为
setField()
/
save()
的错误返回。

Payments

支付

  • Create via
    getPaymentCollection()->createItem($service)
    ; several payments per order = split/partial pay. Available:
    PaySystem\Manager::getListWithRestrictions($payment, MODE_CLIENT|MODE_MANAGER)
    (or
    getListWithRestrictionsByOrder()
    pre-payment).
  • Run:
    $payment->getPaySystem()->initiatePay($payment, $request, BaseServiceHandler::STRING)
    ServiceResult
    (
    getTemplate()
    ,
    getPaymentUrl()
    , QR). Manual confirm:
    $payment->setPaid('Y')
    ; refund:
    $payment->setReturn(Payment::RETURN_PS|RETURN_INNER|RETURN_NONE)
    , partial via
    Service::refund($payment, $sum)
    (handler must implement
    IRefund
    ). Recurring:
    IRecurring
    ,
    isRecurring()/repeatRecurrent()
    .
  • Internal account pay system:
    PaySystem\Manager::getInnerPaySystemId()
    ,
    Payment::isInner()
    . Balance itself: legacy
    CSaleUserAccount::GetByUserID()
    /
    UpdateAccount($userId, $delta, ...)
    pass the delta, not the new total; journal read via
    Internals\UserTransactTable
    . Buyer aggregates:
    Bitrix\Sale\BuyerStatistic
    (per user+site+currency).
  • Custom handlers:
    /local/php_interface/include/sale_payment/<code>/
    (
    handler.php
    extending
    PaySystem\ServiceHandler
    ,
    .description.php
    ,
    template/
    ). Legacy
    /bitrix/modules/sale/payment/
    unsupported since sale 22.200.0. Callback entry:
    /bitrix/tools/sale_ps_result.php
    (verify signature/sum/currency; handle repeated notifications idempotently). Custom restrictions: extend
    Services\Base\Restriction
    , register on
    onSalePaySystemRestrictionsClassNamesBuildList
    .
  • 通过
    getPaymentCollection()->createItem($service)
    创建支付记录;一个订单可对应多个支付记录(拆分/部分支付)。可用支付系统:
    PaySystem\Manager::getListWithRestrictions($payment, MODE_CLIENT|MODE_MANAGER)
    (或创建支付前调用
    getListWithRestrictionsByOrder()
    )。
  • 发起支付:
    $payment->getPaySystem()->initiatePay($payment, $request, BaseServiceHandler::STRING)
    → 返回
    ServiceResult
    (包含
    getTemplate()
    getPaymentUrl()
    、二维码)。手动确认支付:
    $payment->setPaid('Y')
    ;退款:
    $payment->setReturn(Payment::RETURN_PS|RETURN_INNER|RETURN_NONE)
    ,部分退款通过
    Service::refund($payment, $sum)
    (处理器需实现
    IRefund
    )。 recurring支付:
    IRecurring
    isRecurring()/repeatRecurrent()
  • 内部账户支付系统:
    PaySystem\Manager::getInnerPaySystemId()
    Payment::isInner()
    。账户余额:通过遗留类
    CSaleUserAccount::GetByUserID()
    /
    UpdateAccount($userId, $delta, ...)
    获取/更新——传入金额差值,而非新总额;账户流水通过
    Internals\UserTransactTable
    读取。买家统计:
    Bitrix\Sale\BuyerStatistic
    (按用户+站点+货币统计)。
  • 自定义处理器:放置在
    /local/php_interface/include/sale_payment/<code>/
    目录下(
    handler.php
    继承
    PaySystem\ServiceHandler
    .description.php
    template/
    模板目录)。遗留目录
    /bitrix/modules/sale/payment/
    自sale 22.200.0 版本起不再支持。回调入口:
    /bitrix/tools/sale_ps_result.php
    (需校验签名/金额/货币;幂等处理重复通知)。自定义限制:继承
    Services\Base\Restriction
    ,在
    onSalePaySystemRestrictionsClassNamesBuildList
    事件中注册。

Delivery and Shipments

配送与发货

  • Available services for a shipment:
    Delivery\Services\Manager::getRestrictedObjectsList($shipment)
    or
    getRestrictedList($shipment, Restrictions\Manager::MODE_CLIENT)
    . Single service object:
    getObjectById()
    — never trust a raw request ID without the restricted list.
  • Cost:
    ShipmentCollection::calculateDelivery()
    (all non-system shipments; skips
    CUSTOM_PRICE_DELIVERY='Y'
    ) or
    Manager::calculateDeliveryPrice($shipment, $deliveryId, $extraServices)
    CalculationResult
    (price, period).
  • The collection always holds a system shipment (
    isSystem()
    ) with undistributed quantity — never assign it a service or edit it. Partial/split shipments: distribute quantities; guard with
    getBasketItemDistributedQuantity()
    .
  • State:
    allowDelivery()
    /
    disallowDelivery()
    , deduct via
    setField('DEDUCTED', 'Y')
    ,
    TRACKING_NUMBER
    ,
    setStoreId()
    for pickup. Custom handler: extend
    Delivery\Services\Base
    (
    calculateConcrete()
    ,
    getConfigStructure()
    ), register on
    onSaleDeliveryHandlersClassNamesBuildList
    , add via
    Manager::add()
    ; restrictions on
    onSaleDeliveryRestrictionsClassNamesBuildList
    ; extra services in
    Delivery\ExtraServices\*
    +
    Shipment::setExtraServices()
    .
  • 发货记录可用服务:
    Delivery\Services\Manager::getRestrictedObjectsList($shipment)
    getRestrictedList($shipment, Restrictions\Manager::MODE_CLIENT)
    。单个服务对象:
    getObjectById()
    ——切勿直接信任请求中的原始ID,需通过限制列表校验。
  • 费用计算:
    ShipmentCollection::calculateDelivery()
    (所有非系统发货记录;跳过
    CUSTOM_PRICE_DELIVERY='Y'
    的记录)或
    Manager::calculateDeliveryPrice($shipment, $deliveryId, $extraServices)
    → 返回
    CalculationResult
    (包含价格、周期)。
  • 发货记录集合中始终包含一个系统发货记录
    isSystem()
    返回true),用于存放未分配的商品数量——切勿为其分配服务或编辑。部分发货/拆分发货:分配商品数量;通过
    getBasketItemDistributedQuantity()
    校验。
  • 状态管理:
    allowDelivery()
    /
    disallowDelivery()
    、通过
    setField('DEDUCTED', 'Y')
    扣减库存、设置
    TRACKING_NUMBER
    、自提商品设置
    setStoreId()
    。自定义处理器:继承
    Delivery\Services\Base
    (实现
    calculateConcrete()
    getConfigStructure()
    ),在
    onSaleDeliveryHandlersClassNamesBuildList
    事件中注册,通过
    Manager::add()
    添加;限制条件在
    onSaleDeliveryRestrictionsClassNamesBuildList
    事件中注册;额外服务通过
    Delivery\ExtraServices\*
    +
    Shipment::setExtraServices()
    设置。

Discounts and Coupons

折扣与优惠券

  • Cart rules are created via legacy
    CSaleDiscount::Add()/Update()
    (
    CONDITIONS
    /
    ACTIONS
    trees,
    PRIORITY
    +
    SORT
    ,
    LAST_DISCOUNT
    ) — no full D7 replacement; delete via
    Internals\DiscountTable::delete()
    . Never compute discounts by hand or write final prices.
  • Calculation: standalone basket →
    Discount::buildFromBasket()
    +
    calculate()
    +
    applyDiscount()
    ; saved order →
    Order::doFinalAction(true)
    (never
    buildFromBasket()
    on an order basket).
  • Coupons:
    DiscountCouponsManager::init(MODE_CLIENT|MODE_MANAGER|MODE_ORDER [, userId/orderId])
    add($code)
    .
    add() === true
    does not mean the discount applied
    — recalc, then
    get(true, ['COUPON' => $code], true, true)
    and check
    STATUS === STATUS_APPLYED
    . Coupon rows:
    Internals\DiscountCouponTable
    (
    TYPE_ONE_ORDER
    ,
    TYPE_MULTI_ORDER
    +
    MAX_USE
    ).
  • Applied result:
    $order->getDiscount()->getApplyResult()
    ; saved orders:
    OrderDiscount::loadResultFromDb($orderId)
    , rows in
    Internals\OrderRulesTable
    .
  • 购物车规则通过遗留类
    CSaleDiscount::Add()/Update()
    创建(包含
    CONDITIONS
    /
    ACTIONS
    树、
    PRIORITY
    +
    SORT
    LAST_DISCOUNT
    )——无完整D7替代方案;通过
    Internals\DiscountTable::delete()
    删除。切勿手动计算折扣或写入最终价格。
  • 计算逻辑:独立购物车 →
    Discount::buildFromBasket()
    +
    calculate()
    +
    applyDiscount()
    ;已保存订单 →
    Order::doFinalAction(true)
    (切勿对订单绑定的购物车使用
    buildFromBasket()
    )。
  • 优惠券:
    DiscountCouponsManager::init(MODE_CLIENT|MODE_MANAGER|MODE_ORDER [, userId/orderId])
    add($code)
    add() === true
    不代表折扣已生效
    ——需重新计算,然后调用
    get(true, ['COUPON' => $code], true, true)
    并检查
    STATUS === STATUS_APPLYED
    。优惠券记录存储在
    Internals\DiscountCouponTable
    (类型包括
    TYPE_ONE_ORDER
    TYPE_MULTI_ORDER
    +
    MAX_USE
    )。
  • 折扣生效结果:
    $order->getDiscount()->getApplyResult()
    ;已保存订单:
    OrderDiscount::loadResultFromDb($orderId)
    ,记录存储在
    Internals\OrderRulesTable

Reservation and Deduction

库存预留与扣减

  • Reserve a shipment:
    Shipment::tryReserve()
    /
    tryUnreserve()
    ; full-reserve check
    isReserved()
    . Per-item store rows:
    BasketItem::getReserveQuantityCollection()
    (
    create()
    setStoreId()
    then
    setQuantity()
    ). Always finish with
    Order::save()
    — never edit
    RESERVED*
    table fields.
  • Deduct (write-off) =
    Shipment::setField('DEDUCTED', 'Y')
    ; catalog provider updates stock (
    StoreProductTable.AMOUNT/QUANTITY_RESERVED
    ) on save. Set the store first when inventory management is on.
  • Auto-reserve config:
    Sale\Configuration::getProductReservationCondition()
    ReserveCondition::ON_CREATE|ON_PAY|ON_FULL_PAY|ON_ALLOW_DELIVERY|ON_SHIP
    ; TTL
    getProductReserveClearPeriod()
    ; stale reserves cleaned by
    Helpers\ReservedProductCleaner
    . Available qty:
    Reservation\BasketReservationService::getAvailableCountForBasketItem()/ForOrder()
    .
  • 预留发货库存:
    Shipment::tryReserve()
    /
    tryUnreserve()
    ;检查是否完全预留:
    isReserved()
    。按商品+仓库的预留记录:
    BasketItem::getReserveQuantityCollection()
    create()
    setStoreId()
    然后
    setQuantity()
    )。操作完成后务必调用
    Order::save()
    ——切勿直接编辑
    RESERVED*
    表字段。
  • 扣减库存(出库)=
    Shipment::setField('DEDUCTED', 'Y')
    ;目录提供者会在保存时更新库存(
    StoreProductTable.AMOUNT/QUANTITY_RESERVED
    )。启用库存管理时,需先设置仓库。
  • 自动预留配置:
    Sale\Configuration::getProductReservationCondition()
    ReserveCondition::ON_CREATE|ON_PAY|ON_FULL_PAY|ON_ALLOW_DELIVERY|ON_SHIP
    ;预留有效期
    getProductReserveClearPeriod()
    ;过期预留由
    Helpers\ReservedProductCleaner
    清理。可用库存数量:
    Reservation\BasketReservationService::getAvailableCountForBasketItem()/ForOrder()

Reports, Archive, Performance

报表、归档、性能

  • Lists/aggregates: ORM with explicit
    select
    , batch related tables by
    ORDER_ID
    array (no N+1); order-level flags
    PAYED
    /
    DEDUCTED
    avoid loading collections. Mass updates: pick IDs in chunks, then load/change/save each order.
  • Archived orders disappear from active tables — read them via
    Bitrix\Sale\Archive\Manager::getList()/getById()
    ;
    returnArchivedOrder()
    returns a read-only object (don't save it as active). Combine active + archive explicitly in reports.
  • One
    doFinalAction(true)
    and one
    save()
    per logical operation; check
    Result::isSuccess()
    and
    getWarningMessages()
    (warnings can hide sub-object failures — reload and verify critical state).
  • 列表/聚合统计:使用ORM并显式指定
    select
    ,通过
    ORDER_ID
    数组批量关联查询相关表(避免N+1查询);订单级标志
    PAYED
    /
    DEDUCTED
    可避免加载集合。批量更新:分批获取ID,然后逐个加载/修改/保存订单。
  • 已归档订单会从活跃表中移除——通过
    Bitrix\Sale\Archive\Manager::getList()/getById()
    读取;
    returnArchivedOrder()
    返回只读对象(不要作为活跃订单保存)。报表中需显式合并活跃订单与归档订单数据。
  • 每个逻辑操作仅调用一次
    doFinalAction(true)
    和一次
    save()
    ;检查
    Result::isSuccess()
    以及
    getWarningMessages()
    (警告可能隐藏子对象的失败——需重新加载并验证关键状态)。

Module REST / Controllers

模块REST / 控制器

sale
enables
controllers.restIntegration
. Prefer thin Engine controllers + services reusing sale entities (
bitrix-controllers
); standard public checkout is
bitrix:sale.order.ajax
— customize business logic via the object model, not by patching component internals.
sale
模块支持
controllers.restIntegration
。推荐使用轻量Engine控制器 + 复用sale实体的服务(
bitrix-controllers
);标准公共结账组件为
bitrix:sale.order.ajax
——请通过对象模型自定义业务逻辑,不要修改组件内部代码。

Checklist

检查清单

  • sale
    (+
    catalog
    ) included; API level chosen per task (object model / ORM read / manager / legacy).
  • Cart keyed by
    Fuser
    ; catalog lines via
    addProductToBasket
    or provider class +
    refresh
    .
  • Order pipeline: person type → basket → props → shipment+delivery calc → payment →
    doFinalAction(true)
    → SUM sync → restriction re-check →
    save()
    .
  • Services chosen from restricted lists, never by raw ID from request.
  • Rights checked (
    CSaleOrder::CanUser*
    ,
    getAllowedUserStatuses
    ) before user-driven load/changes.
  • Cancel via
    CANCELED='Y'
    , not
    Order::delete()
    ; no direct ORM writes to order tables.
  • Coupon applied status verified (
    STATUS_APPLYED
    ), not just
    add()
    .
  • All
    Result
    s checked incl. warnings; no
    save()
    from
    OnSaleOrderSaved
    .
  • Business logic in services; components/controllers stay thin.
  • 已引入
    sale
    (+
    catalog
    )模块;根据任务选择正确的API层级(对象模型 / ORM读取 / 管理器 / 遗留类)。
  • 购物车以
    Fuser
    作为标识;目录商品通过
    addProductToBasket
    或提供者类 +
    refresh
    添加。
  • 订单流程遵循:人员类型 → 购物车 → 属性 → 发货+配送计算 → 支付 →
    doFinalAction(true)
    → 金额同步 → 限制重校验 →
    save()
  • 服务选择来自限制列表,切勿直接使用请求中的原始ID。
  • 用户驱动的加载/修改操作前已校验权限(
    CSaleOrder::CanUser*
    getAllowedUserStatuses
    )。
  • 通过
    CANCELED='Y'
    取消订单,而非
    Order::delete()
    ;未直接写入订单表的ORM记录。
  • 已验证优惠券生效状态(
    STATUS_APPLYED
    ),而非仅检查
    add()
    返回值。
  • 已检查所有
    Result
    对象(包括警告);未在
    OnSaleOrderSaved
    中调用
    save()
  • 业务逻辑封装在服务中;组件/控制器保持轻量。

Related skills

相关技能

bitrix-catalog
,
bitrix-iblocks
,
bitrix-result-and-errors
,
bitrix-events
,
bitrix-controllers
,
bitrix-service-locator
.
bitrix-catalog
,
bitrix-iblocks
,
bitrix-result-and-errors
,
bitrix-events
,
bitrix-controllers
,
bitrix-service-locator
.