bitrix-sale
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseOnline Store (sale
)
sale在线商店(sale
模块)
salesalecatalogiblockphp
\Bitrix\Main\Loader::includeModule('sale');
\Bitrix\Main\Loader::includeModule('catalog'); // products, prices, stock, reservationsalecatalogiblockphp
\Bitrix\Main\Loader::includeModule('sale');
\Bitrix\Main\Loader::includeModule('catalog'); // products, prices, stock, reservationChoosing the API
API选择
| Task | API |
|---|---|
| Create/change basket, order, payment, shipment | Object model |
| Lists, reports, aggregates | ORM |
| Settings/dictionaries via code | Profile ORM: |
| Pick a configured service | Managers: |
| Operations with no full D7 replacement | Legacy |
Never change an order via or create payments/shipments as raw ORM rows — collections, recalcs, events, and history desync. Never in a loop for a list — use / . Don't mix legacy writes with a loaded object in memory.
OrderTable::update()Order::load()OrderTable::getList()Order::getList()CSale*Order| 任务 | API |
|---|---|
| 创建/修改购物车、订单、支付、发货 | 对象模型 |
| 列表查询、报表、聚合统计 | ORM |
| 通过代码配置设置/字典 | 配置类ORM: |
| 选择已配置的服务 | 管理器类: |
| 无完整D7替代方案的操作 | 遗留类 |
切勿通过 修改订单,也不要以原始ORM行的方式创建支付/发货记录——这会导致集合、重新计算、事件和历史记录不同步。切勿在循环中使用 批量获取订单列表——请使用 / 。不要在内存中同时混用遗留类 的写入操作和已加载的 对象。
OrderTable::update()Order::load()OrderTable::getList()Order::getList()CSale*OrderFUSER (Cart Owner)
FUSER(购物车所有者)
Anonymous and authorized carts are keyed by FUSER (), not .
Bitrix\Sale\FuserUSER_IDphp
$fuserId = Fuser::getId(); // creates if missing
$fuserId = Fuser::getId(true); // skip create → null if none
$fuserId = Fuser::getIdByUserId($userId); // false if cannot resolve/createUSER_IDFUSER_ID匿名和已授权用户的购物车均以 FUSER()作为标识,而非 。
Bitrix\Sale\FuserUSER_IDphp
$fuserId = Fuser::getId(); // 若不存在则创建
$fuserId = Fuser::getId(true); // 跳过创建 → 若不存在则返回null
$fuserId = Fuser::getIdByUserId($userId); // 若无法解析/创建则返回falseUSER_IDFUSER_IDBasket
购物车(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 /
PRICEfor catalog products — the provider does. Own pricing:CURRENCY+CUSTOM_PRICE => 'Y'+PRICE.CURRENCY - With SKUs put the offer ID in , never the parent. Verify the element is a product (
PRODUCT_ID) before adding.Bitrix\Catalog\ProductTable - Basket of a saved order: get via , save via
$order->getBasket()— neverOrder::save()/loadItemsForFUser()for it.$basket->save() - Before order creation: (
$basket->refresh()is deprecated), thenrefreshData()— separate basket with only purchasable, non-delayed items.$basket->getOrderableItems() - Item properties: /
$item->getPropertyCollection()->createItem(). Prices:redefine(),getPrice(),getBasePrice(),getPriceWithVat().getDiscountPrice() - Pre-order discounts preview: →
Discount::buildFromBasket($basket, new Discount\Context\Fuser($basket->getFUserId()))→calculate(). Never for an order-bound basket.$basket->applyDiscount($data['BASKET_ITEMS'])
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填入 ,不要使用父产品ID。添加前请验证该元素为产品(
PRODUCT_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 → → sync payment SUM → re-check restrictions → .
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); // 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 ( is not unique-constrained). Load later: , , ; lock while editing with .
XML_IDOrder::load($id)Order::loadByAccountNumber($number)Order::loadByFilter([...])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_IDOrder::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:
| Change | calculateDelivery | doFinalAction(true) | sync unpaid payments SUM |
|---|---|---|---|
| Status, cancel, mark, comment, tracking, allow-delivery | – | – | – |
| Location/address in restrictions | yes | yes | if price changed |
| Basket items/quantity; delivery service/cost; shipment removal | yes | yes | if price changed |
| Coupon/discount/tax data | if delivery affected | yes | if price changed |
- Quantity down: reduce first, then
ShipmentItem::setQuantity(); up: basket first, then shipment. ThenBasketItem::setField('QUANTITY')the item, recalc, save.refresh - Cancel via (+
setField('CANCELED', 'Y')); blocked while a paid payment or shipped shipment exists.REASON_CANCELEDis a service-only hard delete — never use for customer refusal.Order::delete() - change is a migration (property values are not remapped).
PERSON_TYPE_ID/CURRENCYare not changeable viaUSER_ID. Don't writesetField()/SUM_PAIDdirectly.PAYED
操作单个已加载对象,仅保存一次。修改后需决定重新执行哪些操作:
| 修改内容 | calculateDelivery | doFinalAction(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 (, bound to a person type; ORDER/SHIPMENT) vs value in an order (). Create settings via + in migrations, never during checkout.
OrderPropsTableENTITY_TYPEPropertyValueCollectionOrderPropsGroupTable::add()OrderPropsTable::add()- Find values: (first match),
getItemByOrderPropertyCode(), by role:getItemByOrderPropertyId(), groups viagetDeliveryLocation().getGroups() - takes the internal location code, not a name.
LOCATIONtakes variantENUM(options viaVALUE);$propertyValue->getPropertyObject()->getOptions()takes an array. Files/forms:MULTIPLE=Y+PropertyValueCollection::setValuesFromPost($_POST, $_FILES).verify() - Required check before save: iterate collection, +
isRequired().checkRequiredValue() - Values save with only; never write
Order::save()directly.OrderPropsValueTable
属性设置(,关联人员类型; 为 ORDER/SHIPMENT)与订单中的属性值()是不同概念。请在迁移脚本中通过 + 创建属性设置,切勿在结账流程中创建。
OrderPropsTableENTITY_TYPEPropertyValueCollectionOrderPropsGroupTable::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: , initial
STATUS_ID, finalN, classF. Shipment: ownBitrix\Sale\OrderStatus,STATUS_ID→DN, classDF. DictionaryDeliveryStatus(StatusTable/TYPE_ORDER) +TYPE_SHIPMENTnames.StatusLangTable - Allowed transitions for a user: ; operations per status:
OrderStatus::getAllowedUserStatuses($userId, $currentStatusId),getStatusesUserCanDoOperations()(operations:canGroupDoOperations(),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()(passCanUserUpdateOrder()for create),0, $groups, $siteId,CanUserCancelOrder(),CanUserChangeOrderStatus(),CanUserChangeOrderFlag($id, 'PERM_PAYMENT'|'PERM_DELIVERY'|'PERM_DEDUCTION', $groups). Check view rights beforeCanUserDeleteOrder().Order::load() - Module levels: denied,
Dcompany binding,Porder processing (still needs site + status-task grants),Ufull.W - History: written by on save; read via legacy
OrderHistory(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 in . Key ones: (may modify/deny), (, ; result ignored), deferred after save: (/), , , , , , ; per-entity and ; basket: , ; final calc: .
EventManagerinit.phpOnSaleOrderBeforeSavedOnSaleOrderSavedIS_NEWIS_CHANGEDOnSaleStatusOrderChangeVALUEOLD_VALUEOnSaleOrderPaidOnSaleOrderCanceledOnSaleStatusShipmentChangeOnShipmentDeductedOnShipmentAllowDeliveryOnShipmentTrackingNumberChangeOn[Before]Sale{BasketItem,Payment,Shipment,ShipmentItem,PropertyValue}SetFieldOnSale*EntitySavedOnSaleBasketItemBeforeSaved/SavedOnSaleBasketItemRefreshDataOn{Before,After}SaleOrderFinalActionNever call from — recursion. Mutate in instead, or queue a job that reloads the order. handlers returning surface as / errors.
$order->save()OnSaleOrderSavedOnSaleOrderBeforeSavedOnBefore*EventResult::ERRORsetField()save()在 中通过 注册事件。关键事件:(可修改/拒绝订单)、(包含、;忽略返回结果)、保存后延迟触发事件:(包含/)、、、、、、;各实体对应的 和 ;购物车相关:、;最终计算相关:。
init.phpEventManagerOnSaleOrderBeforeSavedOnSaleOrderSavedIS_NEWIS_CHANGEDOnSaleStatusOrderChangeVALUEOLD_VALUEOnSaleOrderPaidOnSaleOrderCanceledOnSaleStatusShipmentChangeOnShipmentDeductedOnShipmentAllowDeliveryOnShipmentTrackingNumberChangeOn[Before]Sale{BasketItem,Payment,Shipment,ShipmentItem,PropertyValue}SetFieldOnSale*EntitySavedOnSaleBasketItemBeforeSaved/SavedOnSaleBasketItemRefreshDataOn{Before,After}SaleOrderFinalAction切勿在 中调用 ——会导致递归。请在 中修改订单,或队列任务重新加载订单后修改。 处理器返回 会作为 / 的错误返回。
OnSaleOrderSaved$order->save()OnSaleOrderBeforeSavedOnBefore*EventResult::ERRORsetField()save()Payments
支付
- Create via ; several payments per order = split/partial pay. Available:
getPaymentCollection()->createItem($service)(orPaySystem\Manager::getListWithRestrictions($payment, MODE_CLIENT|MODE_MANAGER)pre-payment).getListWithRestrictionsByOrder() - Run: →
$payment->getPaySystem()->initiatePay($payment, $request, BaseServiceHandler::STRING)(ServiceResult,getTemplate(), QR). Manual confirm:getPaymentUrl(); refund:$payment->setPaid('Y'), partial via$payment->setReturn(Payment::RETURN_PS|RETURN_INNER|RETURN_NONE)(handler must implementService::refund($payment, $sum)). Recurring:IRefund,IRecurring.isRecurring()/repeatRecurrent() - Internal account pay system: ,
PaySystem\Manager::getInnerPaySystemId(). Balance itself: legacyPayment::isInner()/CSaleUserAccount::GetByUserID()— pass the delta, not the new total; journal read viaUpdateAccount($userId, $delta, ...). Buyer aggregates:Internals\UserTransactTable(per user+site+currency).Bitrix\Sale\BuyerStatistic - Custom handlers: (
/local/php_interface/include/sale_payment/<code>/extendinghandler.php,PaySystem\ServiceHandler,.description.php). Legacytemplate/unsupported since sale 22.200.0. Callback entry:/bitrix/modules/sale/payment/(verify signature/sum/currency; handle repeated notifications idempotently). Custom restrictions: extend/bitrix/tools/sale_ps_result.php, register onServices\Base\Restriction.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))。 recurring支付:IRefund、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/自sale 22.200.0 版本起不再支持。回调入口:/bitrix/modules/sale/payment/(需校验签名/金额/货币;幂等处理重复通知)。自定义限制:继承/bitrix/tools/sale_ps_result.php,在Services\Base\Restriction事件中注册。onSalePaySystemRestrictionsClassNamesBuildList
Delivery and Shipments
配送与发货
- Available services for a shipment: or
Delivery\Services\Manager::getRestrictedObjectsList($shipment). Single service object:getRestrictedList($shipment, Restrictions\Manager::MODE_CLIENT)— never trust a raw request ID without the restricted list.getObjectById() - Cost: (all non-system shipments; skips
ShipmentCollection::calculateDelivery()) orCUSTOM_PRICE_DELIVERY='Y'→Manager::calculateDeliveryPrice($shipment, $deliveryId, $extraServices)(price, period).CalculationResult - The collection always holds a system shipment () with undistributed quantity — never assign it a service or edit it. Partial/split shipments: distribute quantities; guard with
isSystem().getBasketItemDistributedQuantity() - State: /
allowDelivery(), deduct viadisallowDelivery(),setField('DEDUCTED', 'Y'),TRACKING_NUMBERfor pickup. Custom handler: extendsetStoreId()(Delivery\Services\Base,calculateConcrete()), register ongetConfigStructure(), add viaonSaleDeliveryHandlersClassNamesBuildList; restrictions onManager::add(); extra services inonSaleDeliveryRestrictionsClassNamesBuildList+Delivery\ExtraServices\*.Shipment::setExtraServices()
- 发货记录可用服务:或
Delivery\Services\Manager::getRestrictedObjectsList($shipment)。单个服务对象:getRestrictedList($shipment, Restrictions\Manager::MODE_CLIENT)——切勿直接信任请求中的原始ID,需通过限制列表校验。getObjectById() - 费用计算:(所有非系统发货记录;跳过
ShipmentCollection::calculateDelivery()的记录)或CUSTOM_PRICE_DELIVERY='Y'→ 返回Manager::calculateDeliveryPrice($shipment, $deliveryId, $extraServices)(包含价格、周期)。CalculationResult - 发货记录集合中始终包含一个系统发货记录(返回true),用于存放未分配的商品数量——切勿为其分配服务或编辑。部分发货/拆分发货:分配商品数量;通过
isSystem()校验。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()/CONDITIONStrees,ACTIONS+PRIORITY,SORT) — no full D7 replacement; delete viaLAST_DISCOUNT. Never compute discounts by hand or write final prices.Internals\DiscountTable::delete() - Calculation: standalone basket → +
Discount::buildFromBasket()+calculate(); saved order →applyDiscount()(neverOrder::doFinalAction(true)on an order basket).buildFromBasket() - Coupons: →
DiscountCouponsManager::init(MODE_CLIENT|MODE_MANAGER|MODE_ORDER [, userId/orderId]).add($code)does not mean the discount applied — recalc, thenadd() === trueand checkget(true, ['COUPON' => $code], true, true). Coupon rows:STATUS === STATUS_APPLYED(Internals\DiscountCouponTable,TYPE_ONE_ORDER+TYPE_MULTI_ORDER).MAX_USE - Applied result: ; saved orders:
$order->getDiscount()->getApplyResult(), rows inOrderDiscount::loadResultFromDb($orderId).Internals\OrderRulesTable
- 购物车规则通过遗留类 创建(包含
CSaleDiscount::Add()/Update()/CONDITIONS树、ACTIONS+PRIORITY、SORT)——无完整D7替代方案;通过LAST_DISCOUNT删除。切勿手动计算折扣或写入最终价格。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(); full-reserve checktryUnreserve(). Per-item store rows:isReserved()(BasketItem::getReserveQuantityCollection()→create()thensetStoreId()). Always finish withsetQuantity()— never editOrder::save()table fields.RESERVED* - Deduct (write-off) = ; catalog provider updates stock (
Shipment::setField('DEDUCTED', 'Y')) on save. Set the store first when inventory management is on.StoreProductTable.AMOUNT/QUANTITY_RESERVED - Auto-reserve config: →
Sale\Configuration::getProductReservationCondition(); TTLReserveCondition::ON_CREATE|ON_PAY|ON_FULL_PAY|ON_ALLOW_DELIVERY|ON_SHIP; stale reserves cleaned bygetProductReserveClearPeriod(). Available qty:Helpers\ReservedProductCleaner.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 , batch related tables by
selectarray (no N+1); order-level flagsORDER_ID/PAYEDavoid loading collections. Mass updates: pick IDs in chunks, then load/change/save each order.DEDUCTED - Archived orders disappear from active tables — read them via ;
Bitrix\Sale\Archive\Manager::getList()/getById()returns a read-only object (don't save it as active). Combine active + archive explicitly in reports.returnArchivedOrder() - One and one
doFinalAction(true)per logical operation; checksave()andResult::isSuccess()(warnings can hide sub-object failures — reload and verify critical state).getWarningMessages()
- 列表/聚合统计:使用ORM并显式指定,通过
select数组批量关联查询相关表(避免N+1查询);订单级标志ORDER_ID/PAYED可避免加载集合。批量更新:分批获取ID,然后逐个加载/修改/保存订单。DEDUCTED - 已归档订单会从活跃表中移除——通过 读取;
Bitrix\Sale\Archive\Manager::getList()/getById()返回只读对象(不要作为活跃订单保存)。报表中需显式合并活跃订单与归档订单数据。returnArchivedOrder() - 每个逻辑操作仅调用一次和一次
doFinalAction(true);检查save()以及Result::isSuccess()(警告可能隐藏子对象的失败——需重新加载并验证关键状态)。getWarningMessages()
Module REST / Controllers
模块REST / 控制器
salecontrollers.restIntegrationbitrix-controllersbitrix:sale.order.ajaxsalecontrollers.restIntegrationbitrix-controllersbitrix:sale.order.ajaxChecklist
检查清单
- (+
sale) included; API level chosen per task (object model / ORM read / manager / legacy).catalog - Cart keyed by ; catalog lines via
Fuseror provider class +addProductToBasket.refresh - Order pipeline: person type → basket → props → shipment+delivery calc → payment → → SUM sync → restriction re-check →
doFinalAction(true).save() - Services chosen from restricted lists, never by raw ID from request.
- Rights checked (,
CSaleOrder::CanUser*) before user-driven load/changes.getAllowedUserStatuses - Cancel via , not
CANCELED='Y'; no direct ORM writes to order tables.Order::delete() - Coupon applied status verified (), not just
STATUS_APPLYED.add() - All s checked incl. warnings; no
Resultfromsave().OnSaleOrderSaved - Business logic in services; components/controllers stay thin.
- 已引入(+
sale)模块;根据任务选择正确的API层级(对象模型 / ORM读取 / 管理器 / 遗留类)。catalog - 购物车以作为标识;目录商品通过
Fuser或提供者类 +addProductToBasket添加。refresh - 订单流程遵循:人员类型 → 购物车 → 属性 → 发货+配送计算 → 支付 → → 金额同步 → 限制重校验 →
doFinalAction(true)。save() - 服务选择来自限制列表,切勿直接使用请求中的原始ID。
- 用户驱动的加载/修改操作前已校验权限(、
CSaleOrder::CanUser*)。getAllowedUserStatuses - 通过取消订单,而非
CANCELED='Y';未直接写入订单表的ORM记录。Order::delete() - 已验证优惠券生效状态(),而非仅检查
STATUS_APPLYED返回值。add() - 已检查所有对象(包括警告);未在
Result中调用OnSaleOrderSaved。save() - 业务逻辑封装在服务中;组件/控制器保持轻量。
Related skills
相关技能
bitrix-catalogbitrix-iblocksbitrix-result-and-errorsbitrix-eventsbitrix-controllersbitrix-service-locatorbitrix-catalogbitrix-iblocksbitrix-result-and-errorsbitrix-eventsbitrix-controllersbitrix-service-locator