CRMEB Pro 积分订单售后避坑:发货、收货、退积分怎么闭环?
摘要
积分商城真正难的不是把商品放出来,也不是让用户点兑换,而是订单后半段:发货怎么走、确认收货怎么改状态、退款时积分怎么返、库存怎么回、账单怎么避免重复。
CRMEB Pro 当前积分兑换订单已经并入统一订单体系,后台积分订单管理通过 type = 4 过滤统一订单。售后退积分和回库存也不是单独改用户积分,而是走统一退款回退链路。
这篇拆第三段:积分订单从后台列表、订单详情、发货、收货,到售后退积分和回库存怎么闭环。
1. 后台积分订单管理其实查的是统一订单
后台积分订单控制器在:
text
app/controller/admin/v1/marketing/integral/StoreIntegralOrder.php
注意它注入的是统一订单服务:
php
use app\services\order\StoreOrderServices;
class StoreIntegralOrder extends AuthController
{
#[Inject]
protected StoreOrderServices $services;
}
列表和统计都强制加 type = 4:
php
public function chart()
{
$where = $this->request->getMore([
['status', ''],
['real_name', ''],
['data', '', '', 'time'],
['type', ''],
['plat_type', 0],
['pay_type', ''],
['field_key', ''],
['store_id', 0],
['supplier_id', 0]
]);
if (!in_array($where['status'], [-1, -2, -3])) {
$where['pid'] = [0, -1];
}
$where['type'] = 4;
$data = $this->services->orderCount($where);
return $this->success($data);
}
订单列表同样如此:
php
public function lst()
{
$where = $this->request->getMore([
['status', ''],
['real_name', ''],
['data', '', '', 'time'],
['type', ''],
['plat_type', 0],
['pay_type', ''],
['field_key', ''],
['store_id', 0],
['supplier_id', 0]
]);
if (!in_array($where['status'], [-1, -2, -3])) {
$where['pid'] = [0, -1];
}
$where['type'] = 4;
return $this->success($this->services->getOrderList($where, ['*']));
}
所以后台"积分订单"不是查旧的独立积分订单表,而是查统一订单表里 type = 4 的订单。
2. 后台 API 和页面入口
后台前端积分订单接口在:
text
crmeb_pro_admin/src/api/marketing.js
核心接口:
js
export function integralOrderList(data) {
return request({
url: 'marketing/integral/order/list',
method: 'get',
params: data
});
}
export function integralGetOrdes(data) {
return request({
url: 'marketing/integral/order/chart',
method: 'get',
params: data
});
}
export function getIntegralOrderDataInfo(id) {
return request({
url: `marketing/integral/order/info/${id}`,
method: 'get'
});
}
发货接口:
js
export function integralOrderPutDelivery(data) {
return request({
url: `marketing/integral/order/delivery/${data.id}`,
method: 'put',
data: data.datas
});
}
物流和配送信息:
js
export function getIntegralOrderDistribution(id) {
return request({
url: `marketing/integral/order/distribution/${id}`,
method: 'get'
});
}
export function getExpress(id) {
return request({
url: `marketing/integral/order/express/${id}`,
method: 'get'
});
}
这意味着二开后台积分订单时,优先复用统一订单的发货、物流、备注、状态记录能力,不要单独做一套"积分订单发货"。
3. 订单详情会带用户信息和订单格式化
积分订单详情:
php
public function order_info($id)
{
if (!$id || !($orderInfo = $this->services->get($id, ['*'], ['virtual']))) {
return $this->fail('订单不存在');
}
$services = app()->make(UserServices::class);
$userInfo = $services->getUserWithTrashedInfo((int)$orderInfo['uid']);
if ($userInfo) {
$userInfo = $userInfo->hidden([
'pwd',
'add_ip',
'last_ip',
'login_type'
]);
$userInfo = $userInfo->toArray();
} else {
$userInfo = [];
}
$orderInfo = $this->services->tidyOrder($orderInfo->toArray());
return $this->success(compact('orderInfo', 'userInfo'));
}
这里有两个二开点:
text
用户信息会隐藏敏感字段
订单展示由 tidyOrder() 统一格式化
如果新增积分订单字段,比如"兑换来源""积分活动批次""人工审核备注",建议在统一订单扩展字段或订单附加信息里处理,再由 tidyOrder() 输出,不要在 Controller 拼一堆临时字段。
4. 积分订单发货复用统一 delivery()
发货接口:
php
public function update_delivery($id)
{
$data = $this->request->postMore([
['type', 1],
['delivery_name', ''],
['delivery_id', ''],
['delivery_code', ''],
['express_record_type', 2],
['express_temp_id', ""],
['to_name', ''],
['to_tel', ''],
['to_addr', ''],
['sh_delivery_name', ''],
['sh_delivery_id', ''],
['sh_delivery_uid', ''],
['fictitious_content', '']
]);
return $this->success('SUCCESS', $this->services->delivery((int)$id, $data));
}
这段没有自己改订单状态,而是调用统一订单服务的 delivery()。好处是:
text
物流发货
虚拟发货
配送员送货
电子面单
订单状态记录
发货后通知
这些能力都能复用,不会因为积分商城单独实现而漏掉。
5. 确认收货只允许从待收货进入已完成
确认收货接口:
php
public function take_delivery($id)
{
if (!$id) return $this->fail('缺少参数');
$order = $this->services->get($id);
if (!$order) {
return $this->fail('Data does not exist!');
}
if ($order['status'] == 3) {
return $this->fail('不能重复收货!');
}
if ($order['status'] == 2) {
$data['status'] = 3;
} else {
return $this->fail('请先发货或者送货!');
}
if (!$this->services->update($id, $data)) {
return $this->fail('收货失败,请稍候再试!');
}
}
收货成功后会写状态记录:
php
$statusService = app()->make(StoreIntegralOrderStatusServices::class);
$statusService->save([
'oid' => $order['id'],
'change_type' => 'take_delivery',
'change_message' => '已收货',
'change_time' => time()
]);
虽然这里类名是 StoreIntegralOrderStatusServices,但从业务上看,它记录的是积分订单状态变更。二开时如果加"平台自动收货""用户确认收货""后台强制收货",建议也写入状态记录,方便售后追踪。
6. 移动端兑换记录也按 type=4 查
移动端积分订单接口:
php
public function lst(Request $request)
{
$where['uid'] = $request->uid();
$where['paid'] = 1;
$where['is_del'] = 0;
$where['is_system_del'] = 0;
$where['type'] = 4;
$list = $this->services->getOrderApiList($where);
return app('json')->successful($list);
}
订单详情要求已支付:
php
public function detail(Request $request, $uni)
{
if (!strlen(trim($uni))) {
return app('json')->fail('参数错误');
}
$order = $this->services->getOne([
'order_id' => $uni,
'is_del' => 0
]);
if (!$order) {
return app('json')->fail('订单不存在');
}
if (!$order['paid']) {
return app('json')->fail('订单未支付,无法查看');
}
$orderData = $this->services->tidyOrder($order->toArray());
return app('json')->successful('ok', $orderData);
}
这能避免未支付订单被当成有效兑换记录展示。
7. 售后退积分不是只改用户余额
统一售后服务在:
text
app/services/order/StoreOrderRefundServices.php
退积分关键方法是 regressionIntegral()。它会处理三类积分:
text
订单赠送积分追回
积分兑换支付积分返还
普通下单积分抵扣返还
积分兑换支付积分返还在这里:
php
$pay_integral = $order['pay_integral'];
if ($pay_integral > 0) {
$integral = bcadd((string)$integral, (string)$pay_integral);
$res2 = $userBillServices->income(
'order_integral_refund',
$order['uid'],
(int)$pay_integral,
(int)$integral,
$order['id']
);
}
普通积分抵扣返还在这里:
php
$use_integral = $order['use_integral'];
if ($use_integral > 0) {
$integral = bcadd((string)$integral, (string)$use_integral);
$res2 = $userBillServices->income(
'pay_product_integral_back',
$order['uid'],
(int)$use_integral,
(int)$integral,
$order['id']
);
}
最后统一更新用户积分:
php
$res3 = $userServices->update($order['uid'], ['integral' => $integral]);
if (!($res1 && $res2 && $res3 && $res4 && $res5)) {
throw new ValidateException('回退积分增加失败');
}
二开时千万不要写:
php
$user->integral += $order->pay_integral;
$user->save();
这种写法没有账单、没有防重、没有状态记录,后面用户积分对账一定会出问题。
8. 退款回库存会走 case 4
退款时还要回库存。统一售后里会根据订单类型回退不同活动库存:
php
switch ($order['type']) {
case 1://秒杀
$res5 = $res5 && $seckillServices
->incSeckillStock($cart_num, $activity_id, $unique, $store_id);
break;
case 2://砍价
$res5 = $res5 && $bargainServices
->incBargainStock($cart_num, $activity_id, $unique, $store_id);
break;
case 3://拼团
$res5 = $res5 && $pinkServices
->incCombinationStock($cart_num, $activity_id, $unique, $store_id);
break;
case 4://积分
$res5 = $res5 && $storeIntegralServices
->incIntegralStock($cart_num, $activity_id, $unique, $store_id);
break;
}
积分商城回库存方法:
php
public function incIntegralStock(
int $num,
int $integralId,
string $unique,
int $store_id = 0
) {
$product_id = $this->dao->value(['id' => $integralId], 'product_id');
if ($product_id) {
if ($unique) {
$skuValueServices = app()->make(StoreProductAttrValueServices::class);
//增加积分商品的sku库存,减去销量
$res = false !== $skuValueServices
->incProductAttrStock($integralId, $unique, $num, 4);
//积分商品sku
$suk = $skuValueServices->value([
'unique' => $unique,
'product_id' => $integralId,
'type' => 4
], 'suk');
//平台商品sku unique
$productUnique = $skuValueServices->value([
'suk' => $suk,
'product_id' => $product_id,
'type' => 0
], 'unique');
$services = app()->make(StoreProductServices::class);
//增加普通商品库存
$res = $res && $services
->incProductStock($num, $product_id, (string)$productUnique);
}
//增加积分库存
$res = $res && false !== $this->dao
->incStockDecSales(['id' => $integralId, 'type' => 4], $num);
}
return $res;
}
这说明积分订单退款要同时回:
text
积分商品 SKU 库存
普通商品 SKU 库存
积分商品主表库存
积分商品销量
只回用户积分,不回库存;或者只回普通库存,不回积分活动库存,都会造成后续兑换异常。
9. 单独退积分也有表单和校验
后台订单还有单独退积分能力,表单会校验:
php
if ($orderInfo->getData('back_integral') >= $orderInfo->getData('use_integral')) {
throw new ValidateException('积分已退或者积分为零无法再退');
}
if (!$orderInfo->paid) {
throw new ValidateException('未支付无法退积分');
}
$f[] = Form::number(
'use_integral',
'使用的积分',
(float)$orderInfo->getData('use_integral')
)->min(0)->disabled(1);
$f[] = Form::number(
'back_integral',
'可退积分',
(float)bcsub(
$orderInfo->getData('use_integral'),
$orderInfo->getData('back_integral')
)
)->min(0)->precision(0)->required('请输入可退积分');
这块更多针对普通订单的积分抵扣回退。积分商城的兑换积分主要看 pay_integral,售后退款时要区分:
text
pay_integral:积分商城兑换消耗
use_integral:普通订单积分抵扣
back_integral:已经退回的积分抵扣
二开后台"退积分"按钮时,必须分清这几个字段,不要混用。
10. 二开建议:把积分售后做成可审计链路
积分商城售后建议至少保留这些信息:
text
谁操作
操作时间
操作原因
退了多少 pay_integral
退了多少 use_integral
是否回库存
回了哪个 SKU
账单类型是什么
订单状态从什么变成什么
已有实现里,积分回退会写用户账单:
text
order_integral_refund 返还积分兑换支付积分
pay_product_integral_back 返还下单抵扣积分
integral_refund 追回订单赠送积分
订单状态会写状态记录:
php
$statusService->save([
'oid' => $order['id'],
'change_type' => 'take_delivery',
'change_message' => '已收货',
'change_time' => time()
]);
如果要扩展"售后审核""积分人工补偿""部分退积分",建议也按这套思路写账单和状态记录,避免只改余额。
注意事项
- 后台积分订单管理查的是统一订单,条件是
type = 4。 - 发货复用
StoreOrderServices::delivery(),不要单独改订单状态。 - 确认收货只能从
status = 2到status = 3,要防重复收货。 - 退积分要写用户账单,不能只更新用户积分余额。
- 积分商城兑换积分看
pay_integral,普通积分抵扣看use_integral。 - 退款回库存必须走
incIntegralStock(),同时回积分 SKU 和普通 SKU。 - 新增售后动作建议写订单状态记录,方便后续对账。
标签建议
CRMEB Pro、积分商城、订单售后、退积分、库存回退、商城二开、源码解析