fix(shopro): 修复拼团参团记录与后台订单展示时机

支付成功后正确识别 buy_type 并写入参团记录,我的拼团仅展示已支付订单。

后台与门店订单列表默认隐藏拼团进行中订单,成团成功后再进入备货/发货流程。

纳入版本库:Groupon、ActivityGroupon、OrderScope、Create 等 addons 拼团相关改动。

Co-authored-by: Cursor <cursoragent@cursor.com>
这个提交包含在:
可乐加冰
2026-09-23 23:03:45 +08:00
父节点 eb31a121f6
当前提交 d11c2bc0fa
共有 6 个文件被更改,包括 1528 次插入 和 0 次删除

查看文件

@@ -0,0 +1,429 @@
<?php
namespace addons\shopro\library\traits;
use addons\shopro\exception\Exception;
use addons\shopro\model\Activity;
use addons\shopro\model\ActivityGroupon;
use addons\shopro\model\ActivityGrouponLog;
use addons\shopro\model\Order;
use addons\shopro\model\OrderItem;
use addons\shopro\model\UserFake;
/**
* 拼团
*/
trait Groupon
{
/**
* *、redis 没有存团完整信息,只存了团当前人数,团成员(当前人数,团成员均没有存虚拟用户)
* *、redis userList 没有存这个人的购买状态
* *、团 解散,成团,虚拟成团,没有修改 redis 团信息(因为直接修改了数据库,参团判断,先判断的数据库后判断的 redis)
*/
/**
* 增加拼团预成员人数
*/
protected function grouponCacheForwardNum($activityGroupon, $activity, $user, $payed = 'nopay')
{
if (!$this->hasRedis()) {
return true;
}
$keys = $this->getKeys([
'groupon_id' => $activityGroupon['id'],
'goods_id' => $activityGroupon['goods_id'],
], [
'activity_id' => $activity['id'],
'activity_type' => $activity['type'],
]);
extract($keys);
$redis = $this->getRedis();
// 将拼团团信息存入 redis 没有用,还得维护团状态,先不存
// $redis->HSET($activityHashKey, $grouponKey, json_encode($activityGroupon));
// 当前团人数 grouponNumKey 如果不存在,自动创建
$current_num = $redis->HINCRBY($activityHashKey, $grouponNumKey, 1);
if ($current_num > $activityGroupon['num']) {
// 再把刚加上的减回来
$current_num = $redis->HINCRBY($activityHashKey, $grouponNumKey, -1);
new Exception('该团已满,请参与其它团或自己开团');
}
// 将用户加入拼团缓存,用来判断同一个人在一个团,多次下单,取消订单删除
$userList = $redis->HGET($activityHashKey, $grouponUserlistKey);
$userList = json_decode($userList, true);
$userList = $userList ? : [];
$userList[] = [
'user_id' => $user['id'],
// 'status' => $payed // 太复杂,先不做
];
$redis->HSET($activityHashKey, $grouponUserlistKey, json_encode($userList));
}
// 拼团团成员预成员退回
protected function grouponCacheBackNum($order)
{
$items = OrderItem::where('order_id', $order['id'])->select();
foreach ($items as $key => $item) {
// 不是拼团,或者 没有配置 redis
if (strpos($item['activity_type'], 'groupon') === false || !$this->hasRedis()) {
continue;
}
// 扩展字段
$order_ext = $order['ext_arr'];
// 团 id
$groupon_id = $order_ext['groupon_id'] ?? 0;
if (!$groupon_id) {
continue; // 商品独立购买,未参团,或者开新团
}
// 实例化 redis
$redis = $this->getRedis();
$keys = $this->getKeys([
'groupon_id' => $groupon_id,
'goods_id' => $item['goods_id'],
'goods_sku_price_id' => $item['goods_sku_price_id'],
], [
'activity_id' => $item['activity_id'],
'activity_type' => $item['activity_type'],
]);
extract($keys);
// 扣除预参团成员
if ($redis->EXISTS($activityHashKey) && $redis->HEXISTS($activityHashKey, $grouponNumKey)) {
$sale = $redis->HINCRBY($activityHashKey, $grouponNumKey, -1);
}
$userList = $redis->HGET($activityHashKey, $grouponUserlistKey);
$userList = json_decode($userList, true);
$userList = $userList ?: [];
foreach($userList as $key => $user) {
if ($user['user_id'] == $item['user_id']) {
unset($userList[$key]);
}
}
$redis->HSET($activityHashKey, $grouponUserlistKey, json_encode($userList));
}
}
/**
* 判断加入旧拼团
*/
protected function checkJoinGroupon($goods_info, $user, $groupon_id)
{
$goods = $goods_info['detail'];
$activity = $goods['activity'];
$rules = $activity['rules'];
// 获取团信息
$activityGroupon = ActivityGroupon::with('activity')->where('id', $groupon_id)->find();
if (!$activityGroupon) {
new Exception('要参与的团不存在');
}
// 判断团所属活动是否正常
if (!$activityGroupon->activity || $activityGroupon->activity['id'] != $activity['id']) { // 修复,后台手动将活动删除,然后又立即给这个商品创建新的拼团活动,导致参与新活动的旧团错乱问题
new Exception('要参与的活动已结束');
}
if ($activityGroupon['status'] != 'ing') {
new Exception('要参与的团已成团,请选择其它团或自己开团');
}
if ($activityGroupon['current_num'] >= $activityGroupon['num']) {
new Exception('该团已满,请参与其它团或自己开团');
}
if (!$this->hasRedis()) {
// 没有 redis 直接判断数据库团信息,因为 current_num 支付成功才会累加,故无法保证超员,
// 该团可加入
return $activityGroupon;
}
$keys = $this->getKeys([
'groupon_id' => $activityGroupon['id'],
'goods_id' => $activityGroupon['goods_id'],
], [
'activity_id' => $activity['id'],
'activity_type' => $activity['type'],
]);
extract($keys);
$redis = $this->getRedis();
$current_num = $redis->HGET($activityHashKey, $grouponNumKey);
if ($current_num >= $activityGroupon['num']) {
new Exception('该团已满,请参与其它团或自己开团');
}
// 将用户加入拼团缓存,用来判断同一个人在一个团,多次下单,订单失效删除
$userList = $redis->HGET($activityHashKey, $grouponUserlistKey);
$userList = json_decode($userList, true);
$userIds = array_column($userList, 'user_id');
if (in_array($user['id'], $userIds)) {
new Exception('您已参与该团,请不要重复参团');
}
return $activityGroupon;
}
/**
* 支付成功真实加入团
*/
protected function joinGroupon($order, $user) {
$item = $order->item;
$goods_item = $item[0]; // 拼团只能单独购买
// 扩展字段
$order_ext = $order['ext_arr'];
// 团 id
$groupon_id = (int)($order_ext['groupon_id'] ?? 0);
$buy_type = $order_ext['buy_type'] ?? 'alone';
// 不是拼团购买,比如拼团单独购买
if ($buy_type !== 'groupon') {
return true;
}
if ($groupon_id) {
// 加入旧团,查询团
$activityGroupon = ActivityGroupon::where('id', $groupon_id)->find();
if (!$activityGroupon) {
\think\Log::write('joinGroupon: 团不存在, order_id=' . $order['id'] . ', groupon_id=' . $groupon_id);
return false;
}
} else {
// 加入新团,创建团
$activityGroupon = $this->joinNewGroupon($order, $user);
}
if (!$activityGroupon) {
return false;
}
// 添加参团记录
$activityGrouponLog = $this->addGrouponLog($order, $user, $activityGroupon);
if (!$activityGrouponLog) {
return false;
}
return $this->checkGrouponStatus($activityGroupon);
}
/**
* 支付成功开启新拼团
*/
protected function joinNewGroupon($order, $user)
{
$item = $order->item;
$goodsItem = $item[0]; // 拼团只能单独购买
// 获取活动
$activity = Activity::where('id', $goodsItem['activity_id'])->find();
$rules = $activity['rules'];
// 小于 0 不限结束时间单位小时
$expiretime = 0;
if (isset($rules['valid_time']) && $rules['valid_time'] > 0) {
// 转为 秒
$expiretime = $rules['valid_time'] * 3600;
}
// 开团
$activityGroupon = new ActivityGroupon();
$activityGroupon->user_id = $user['id'];
$activityGroupon->goods_id = $goodsItem['goods_id'];
$activityGroupon->activity_id = $goodsItem['activity_id'];
$activityGroupon->num = $rules['team_num'] ?? 1; // 避免活动找不到
$activityGroupon->current_num = 0; // 真实团成员等支付完成之后再增加
$activityGroupon->status = 'ing';
$activityGroupon->expiretime = $expiretime > 0 ? (time() + $expiretime) : 0;
$activityGroupon->save();
// 记录团 id
$order->ext = json_encode($order->setExt($order, ['groupon_id' => $activityGroupon->id])); // 团 id
$order->save();
// 将团信息存入缓存,增加缓存中当前团人数
$this->grouponCacheForwardNum($activityGroupon, $activity, $user, 'payed');
if ($expiretime > 0) {
// 增加自动关闭拼团队列(如果有虚拟成团,会判断虚拟成团)
\think\Queue::later($expiretime, '\addons\shopro\job\ActivityGrouponAutoOper@expire', [
'activity' => $activity,
'activity_groupon_id' => $activityGroupon->id
], 'shopro');
}
return $activityGroupon;
}
/**
* 增加团成员记录
*/
protected function addGrouponLog($order, $user, $activityGroupon) {
if (!$activityGroupon) {
\think\Log::write('groupon-notfund: order_id: ' . $order['id']);
return null;
}
$item = $order->item;
$goodsItem = $item[0]; // 拼团只能单独购买
// 增加团成员数量
$activityGroupon->setInc('current_num', 1);
// 增加参团记录
$activityGrouponLog = new ActivityGrouponLog();
$activityGrouponLog->user_id = $user['id'];
$activityGrouponLog->user_nickname = $user['nickname'];
$activityGrouponLog->user_avatar = $user['avatar'];
$activityGrouponLog->groupon_id = $activityGroupon['id'] ?? 0;
$activityGrouponLog->goods_id = $goodsItem['goods_id'];
$activityGrouponLog->goods_sku_price_id = $goodsItem['goods_sku_price_id'];
$activityGrouponLog->activity_id = $goodsItem['activity_id'];
$activityGrouponLog->is_leader = ($activityGroupon['user_id'] == $user['id']) ? 1 : 0;
$activityGrouponLog->is_fictitious = 0;
$activityGrouponLog->order_id = $order['id'];
$activityGrouponLog->save();
return $activityGrouponLog;
}
// 虚拟成团,增加虚拟成员,并判断是否完成,然后将团状态改为,虚拟成团成功
protected function finishFictitiousGroupon($activityGroupon, $num = 0, $users = []) {
// 拼团剩余人数
$surplus_num = $activityGroupon['num'] - $activityGroupon['current_num'];
// 团已经满员
if ($surplus_num <= 0) {
if ($activityGroupon['status'] == 'ing') {
// 已满员但还是进行中状态,检测并完成团,起到纠正作用
return $this->checkGrouponStatus($activityGroupon);
}
return true;
}
// 本次虚拟人数, 如果传入 num 则使用 num 和 surplus_num 中最小值, 如果没有传入,默认剩余人数全部虚拟
$fictitious_num = $num ? ($num > $surplus_num ? $surplus_num : $num) : $surplus_num;
// 查询虚拟用户
$userFakes = UserFake::orderRaw('rand()')->limit($fictitious_num)->select();
if (count($userFakes) < $fictitious_num && $num == 0) {
// 虚拟用户不足,并且是自动虚拟成团进程,自动解散团
return $this->invalidRefundGroupon($activityGroupon);
}
// 增加团人数
$activityGroupon->setInc('current_num', $fictitious_num);
for ($i = 0; $i < $fictitious_num; $i ++) {
// 先用传过来的
$avatar = isset($users[$i]['avatar']) ? $users[$i]['avatar'] : '';
$nickname = isset($users[$i]['nickname']) ? $users[$i]['nickname'] : '';
// 如果没有,用查的虚拟的
$avatar = $avatar ? : $userFakes[$i]['avatar'];
$nickname = $nickname ? : $userFakes[$i]['nickname'];
// 增加参团记录
$activityGrouponLog = new ActivityGrouponLog();
$activityGrouponLog->user_id = 0;
$activityGrouponLog->user_nickname = $nickname;
$activityGrouponLog->user_avatar = $avatar;
$activityGrouponLog->groupon_id = $activityGroupon['id'] ?? 0;
$activityGrouponLog->goods_id = $activityGroupon['goods_id'];
$activityGrouponLog->goods_sku_price_id = 0; // 没有订单,所以也就没有 goods_sku_price_id
$activityGrouponLog->activity_id = $activityGroupon['activity_id'];
$activityGrouponLog->is_leader = 0; // 不是团长
$activityGrouponLog->is_fictitious = 1; // 虚拟用户
$activityGrouponLog->order_id = 0; // 虚拟成员没有订单
$activityGrouponLog->save();
}
return $this->checkGrouponStatus($activityGroupon);
}
/**
* 团过期退款,或者后台手动解散退款
*/
protected function invalidRefundGroupon($activityGroupon, $user = null) {
$activityGroupon->status = 'invalid'; // 拼团失败
$activityGroupon->save();
// 查询参团真人
$logs = ActivityGrouponLog::with('order')->where('groupon_id', $activityGroupon['id'])->where('is_fictitious', 0)->select();
foreach ($logs as $key => $log) {
$order = $log['order'];
if ($order && $order['status'] > 0) {
// 退款,只能有一个 item
$item = $order['item'][0];
if ($item && in_array($item['refund_status'], [OrderItem::REFUND_STATUS_NOREFUND, OrderItem::REFUND_STATUS_ING])) {
// 未申请退款,或者退款中,直接全额退款
Order::startRefund($order, $order['item'][0], $order['pay_fee'], $user, '拼团失败退款');
}
}
// 修改 logs 为已退款
$log->is_refund = 1;
$log->save();
}
// 触发拼团失败行为
$data = ['groupon' => $activityGroupon];
\think\Hook::listen('activity_groupon_fail', $data);
return true;
}
/**
* 检查团状态
*/
protected function checkGrouponStatus($activityGroupon) {
if (!$activityGroupon) {
return true;
}
// 重新获取团信息
$activityGroupon = ActivityGroupon::where('id', $activityGroupon['id'])->find();
if ($activityGroupon['current_num'] >= $activityGroupon['num'] && !in_array($activityGroupon['status'], ['finish', 'finish-fictitious'])) {
// 查询是否有虚拟团成员
$fictitiousCount = ActivityGrouponLog::where('groupon_id', $activityGroupon['id'])->where('is_fictitious', 1)->count();
// 将团设置为已完成
$activityGroupon->status = $fictitiousCount ? 'finish-fictitious' : 'finish';
$activityGroupon->finishtime = time();
$activityGroupon->save();
// 触发成团行为
$data = ['groupon' => $activityGroupon];
\think\Hook::listen('activity_groupon_finish', $data);
}
return true;
}
}

查看文件

@@ -0,0 +1,300 @@
<?php
namespace addons\shopro\library\traits\model\order;
use addons\shopro\library\Wechat;
use addons\shopro\model\Order;
use addons\shopro\model\OrderItem;
use addons\shopro\model\Store;
use think\Cache;
trait OrderScope
{
// 已失效
public function scopeInvalid($query)
{
return $query->where('status', Order::STATUS_INVALID);
}
// 已取消
public function scopeCancel($query)
{
return $query->where('status', Order::STATUS_CANCEL);
}
// 未支付
public function scopeNopay($query)
{
return $query->where('status', Order::STATUS_NOPAY);
}
// 未发货
public function scopeNosend($query)
{
$where = [
'dispatch_status' => OrderItem::DISPATCH_STATUS_NOSEND,
'refund_status' => ['not in', [OrderItem::REFUND_STATUS_OK, OrderItem::REFUND_STATUS_FINISH]] // 没有退款完成
];
return $query->whereExists(function ($query) use ($where) {
$order_table_name = (new Order())->getQuery()->getTable();
$table_name = (new OrderItem())->getQuery()->getTable();
$query->table($table_name)->where('order_id=' . $order_table_name . '.id')->where($where);
})->whereNotExists(function ($query) use($where) {
$order_table_name = (new Order())->getQuery()->getTable();
$table_name = (new OrderItem())->getQuery()->getTable();
$query->table($table_name)->where('order_id=' . $order_table_name . '.id')->where(['aftersale_status' => OrderItem::AFTERSALE_STATUS_AFTERING])->where($where);
});
}
// 待收货
public function scopeNoget($query)
{
$where = [
'dispatch_status' => OrderItem::DISPATCH_STATUS_SENDED,
'refund_status' => ['not in', [OrderItem::REFUND_STATUS_OK, OrderItem::REFUND_STATUS_FINISH]] // 没有退款完成
];
return $query->whereExists(function ($query) use ($where) {
$order_table_name = (new Order())->getQuery()->getTable();
$table_name = (new OrderItem())->getQuery()->getTable();
$query->table($table_name)->where('order_id=' . $order_table_name . '.id')->where($where);
});
}
// 待评价
public function scopeNocomment($query)
{
$where = [
'dispatch_status' => OrderItem::DISPATCH_STATUS_GETED,
'refund_status' => ['not in', [OrderItem::REFUND_STATUS_OK, OrderItem::REFUND_STATUS_FINISH]], // 没有退款完成
'comment_status' => OrderItem::COMMENT_STATUS_NO
];
return $query->whereExists(function ($query) use ($where) {
$order_table_name = (new Order())->getQuery()->getTable();
$table_name = (new OrderItem())->getQuery()->getTable();
$query->table($table_name)->where('order_id=' . $order_table_name . '.id')->where($where);
});
}
// 售后 (后台要用,虽然有专门的售后单列表)
public function scopeAftersale($query)
{
return $query->whereExists(function ($query) {
$order_table_name = (new Order())->getQuery()->getTable();
$table_name = (new OrderItem())->getQuery()->getTable();
$query->table($table_name)->where('order_id=' . $order_table_name . '.id')
->where('aftersale_status', '<>', OrderItem::AFTERSALE_STATUS_NOAFTER);
// ->where(function($query) {
// // 只要申请过售后都算
// $query->where('aftersale_status', '<>', OrderItem::AFTERSALE_STATUS_NOAFTER)
// ->whereOr('refund_status', '<>', OrderItem::REFUND_STATUS_NOREFUND);
// });
});
}
// 退款 (即将废弃,有专门的售后单列表)
public function scopeRefundStatus($query)
{
$where = [
'refund_status' => ['<>', OrderItem::REFUND_STATUS_NOREFUND], // 只要申请过退款
];
return $query->whereExists(function ($query) use ($where) {
$order_table_name = (new Order())->getQuery()->getTable();
$table_name = (new OrderItem())->getQuery()->getTable();
$query->table($table_name)->where('order_id=' . $order_table_name . '.id')->where($where);
});
}
// 已支付
public function scopePayed($query)
{
return $query->where('status', 'in', [Order::STATUS_PAYED, Order::STATUS_FINISH]);
}
/**
* 排除拼团进行中的订单(成团成功后再进入待发货/备货流程)
*/
public function scopeExcludeGrouponIng($query)
{
$orderTable = (new Order())->getQuery()->getTable();
$grouponTable = (new \addons\shopro\model\ActivityGroupon())->getQuery()->getTable();
return $query->whereNotExists(function ($sub) use ($orderTable, $grouponTable) {
$sub->table($grouponTable)
->whereRaw($grouponTable . '.id = CAST(JSON_UNQUOTE(JSON_EXTRACT(' . $orderTable . '.ext, "$.groupon_id")) AS UNSIGNED)')
->where($grouponTable . '.status', 'ing');
});
}
// 已完成
public function scopeFinish($query)
{
return $query->where('status', Order::STATUS_FINISH);
}
public function scopeCanAftersale($query)
{
return $query->where('status', 'in', [Order::STATUS_PAYED, Order::STATUS_FINISH]);
}
public function scopeCanDelete($query)
{
return $query->where('status', 'in', [
Order::STATUS_CANCEL,
Order::STATUS_INVALID,
Order::STATUS_FINISH
]);
}
// 门店订单 scope
// 获取门店 id 条件
private function getStoreWhere($store_id) {
$where = [];
if ($store_id == 'current') {
// 默认手机商户端,取当前登陆的门店
$store = Store::info();
$where['store_id'] = $store['id'];
} else {
if ($store_id) {
$where['store_id'] = $store_id;
} else {
$where['store_id'] = ['<>', 0];
}
}
return $where;
}
/**
* 全部订单
*/
public function scopeStore($query, $store_id = 'current')
{
$where = $this->getStoreWhere($store_id);
return $query->whereExists(function ($query) use ($where) {
$order_table_name = (new Order())->getQuery()->getTable();
$table_name = (new OrderItem())->getQuery()->getTable();
$query->table($table_name)->where('order_id=' . $order_table_name . '.id')->where($where);
});
}
/**
* 未发货
*/
public function scopeStoreNosend($query, $store_id = 'current') {
$where = [
'dispatch_status' => OrderItem::DISPATCH_STATUS_NOSEND,
'refund_status' => ['not in', [OrderItem::REFUND_STATUS_OK, OrderItem::REFUND_STATUS_FINISH]] // 没有退款完成
];
$where = array_merge($where, $this->getStoreWhere($store_id));
return $query->whereExists(function ($query) use ($where) {
$order_table_name = (new Order())->getQuery()->getTable();
$table_name = (new OrderItem())->getQuery()->getTable();
$query->table($table_name)->where('order_id=' . $order_table_name . '.id')->where($where);
});
}
/**
* 未收货
*/
public function scopeStoreNoget($query, $store_id = 'current') {
$where = [
'dispatch_status' => OrderItem::DISPATCH_STATUS_SENDED,
'refund_status' => ['not in', [OrderItem::REFUND_STATUS_OK, OrderItem::REFUND_STATUS_FINISH]] // 没有退款完成
];
$where = array_merge($where, $this->getStoreWhere($store_id));
return $query->whereExists(function ($query) use ($where) {
$order_table_name = (new Order())->getQuery()->getTable();
$table_name = (new OrderItem())->getQuery()->getTable();
$query->table($table_name)->where('order_id=' . $order_table_name . '.id')->where($where);
});
}
// 待评价
public function scopeStoreNocomment($query, $store_id = 'current')
{
$where = [
'dispatch_status' => OrderItem::DISPATCH_STATUS_GETED,
'refund_status' => ['not in', [OrderItem::REFUND_STATUS_OK, OrderItem::REFUND_STATUS_FINISH]], // 没有退款完成
'comment_status' => OrderItem::COMMENT_STATUS_NO
];
$where = array_merge($where, $this->getStoreWhere($store_id));
return $query->whereExists(function ($query) use ($where) {
$order_table_name = (new Order())->getQuery()->getTable();
$table_name = (new OrderItem())->getQuery()->getTable();
$query->table($table_name)->where('order_id=' . $order_table_name . '.id')->where($where);
});
}
// 门店售后订单
public function scopeStoreAftersale($query, $store_id = 'current')
{
$where = [
'aftersale_status' => ['<>', OrderItem::AFTERSALE_STATUS_NOAFTER]
];
$where = array_merge($where, $this->getStoreWhere($store_id));
return $query->whereExists(function ($query) use ($where) {
$order_table_name = (new Order())->getQuery()->getTable();
$table_name = (new OrderItem())->getQuery()->getTable();
$query->table($table_name)->where('order_id=' . $order_table_name . '.id')->where($where);
});
}
// 退款
public function scopeStoreRefundStatus($query, $store_id = 'current')
{
$where = [
'refund_status' => ['<>', OrderItem::REFUND_STATUS_NOREFUND], // 只要申请过退款
];
$where = array_merge($where, $this->getStoreWhere($store_id));
return $query->whereExists(function ($query) use ($where) {
$order_table_name = (new Order())->getQuery()->getTable();
$table_name = (new OrderItem())->getQuery()->getTable();
$query->table($table_name)->where('order_id=' . $order_table_name . '.id')->where($where);
});
}
/**
* 已完成,这个只手机门店订单查询
*/
public function scopeStoreFinish($query) {
$store = Store::info();
// 收完货就算已完成
$where = [
'store_id' => $store['id'],
'dispatch_status' => OrderItem::DISPATCH_STATUS_GETED,
'refund_status' => ['not in', [OrderItem::REFUND_STATUS_OK, OrderItem::REFUND_STATUS_FINISH]] // 没有退款完成
];
return $query->whereExists(function ($query) use ($where) {
$order_table_name = (new Order())->getQuery()->getTable();
$table_name = (new OrderItem())->getQuery()->getTable();
$query->table($table_name)->where('order_id=' . $order_table_name . '.id')->where($where);
});
}
}

查看文件

@@ -0,0 +1,198 @@
<?php
namespace addons\shopro\listener\order;
use addons\shopro\exception\Exception;
use addons\shopro\model\Cart;
use addons\shopro\model\Config;
use addons\shopro\model\Order;
use addons\shopro\model\OrderItem;
use addons\shopro\model\User;
use addons\shopro\library\traits\StockSale;
use addons\shopro\library\traits\ActivityCache;
use addons\shopro\library\traits\Groupon;
use addons\shopro\model\ActivityGroupon;
/**
* 订单创建
*/
class Create
{
use StockSale, ActivityCache, Groupon;
// 订单创建前
public function orderCreateBefore(&$params)
{
$user = $params['user'];
$order_type = $params['order_type'];
$groupon_id = $params['groupon_id'];
$buy_type = $params['buy_type'];
$goods_original_amount = $params['goods_original_amount'];
$goods_amount = $params['goods_amount'];
$dispatch_amount = $params['dispatch_amount'];
$total_amount = $params['total_amount'];
$score_amount = $params['score_amount'];
$total_fee = $params['total_fee'];
$discount_fee = $params['discount_fee'];
$coupon_fee = $params['coupon_fee'];
$new_goods_list = $params['new_goods_list'];
$activity_type = $params['activity_type'];
$user_coupons = $params['user_coupons'];
$user_address = $params['user_address'];
$from = $params['from'];
// 重新获取 user
$user = User::where('id', $user->id)->find();
// 如果需要支付积分
if ($score_amount) {
// 判断个人积分是否充足
if ($user->score < $score_amount) {
// 积分不足
new Exception('积分不足');
}
}
// 限购
$this->limitBuy($new_goods_list, $user);
// 是拼团, 并且不是单独购买 ,单独购买直接正常走商品流程
if (strpos($activity_type, 'groupon') !== false && $buy_type != 'alone') {
// 参与现有团
if ($groupon_id) {
// 获取当前团,并判断当前团是否可参与
$activityGroupon = $this->checkJoinGroupon($new_goods_list[0], $user, $groupon_id);
} else {
// 开新团,放到 支付成功之后
// $activityGroupon = $this->newGroupon($new_goods_list[0], $user);
}
}
// 判断 并 增加 redis 销量
if ($order_type == 'goods') {
$this->cacheForwardSale($new_goods_list);
}
// (开新团不判断)参与旧团 增加预拼团人数,上面加入团的时候已经判断过一次了,所以这里 99.99% 会加入成功的
if (isset($activityGroupon) && $activityGroupon) {
// 将团信息缓存,用在后续下单流程
$key = 'grouponinfo-' . $user['id'];
cache($key, json_encode($activityGroupon), 60);
// 增加拼团预成员人数, 拼团只能单独购买
$detail = $new_goods_list[0]['detail'];
$this->grouponCacheForwardNum($activityGroupon, $detail['activity'], $user);
}
}
// 订单创建后
public function orderCreateAfter(&$params)
{
$user = $params['user'];
$order = $params['order'];
$from = $params['from'];
$groupon = $params['groupon'];
$buy_type = $params['buy_type'];
$new_goods_list = $params['new_goods_list'];
// 删除购物车
if ($from == 'cart') {
foreach ($new_goods_list as $delCart) {
Cart::where([
'user_id' => $user->id,
'goods_id' => $delCart['goods_id'],
'sku_price_id' => $delCart['sku_price_id'],
])->delete();
}
}
// 更新订单扩展字段
$order_ext = $order['ext_arr'];
$order_ext['buy_type'] = ($buy_type === 'groupon') ? 'groupon' : 'alone'; // 购买方式
$order_ext['groupon_id'] = (int)($groupon['id'] ?? 0); // 参团时写入团 id,开团支付后再更新
// 判断需要支付的金额是否大于 0
if ($order['total_fee'] <= 0) {
// 更新订单扩展字段
$order->ext = json_encode($order_ext);
$order->save();
$order = (new Order)->paymentProcess($order, [
'order_sn' => $order['order_sn'],
'transaction_id' => '',
'notify_time' => date('Y-m-d H:i:s'),
'buyer_email' => $user->id,
'payment_json' => json_encode([]),
'pay_fee' => $order->total_fee,
'pay_type' => $order['type'] == 'score' ? 'score' : 'wallet' // 支付方式 积分完全支付,或者不需要支付,使用 wallet
]);
} else {
// 默认取第一个商品
$goods_one = $new_goods_list[0]['detail'];
$activity_one = $goods_one['activity'];
// 获取第一个商品的活动规则,活动不存在,自动会使用全局自动关闭
$rules = $activity_one['rules'] ?? [];
// 优先使用活动的订单关闭时间
if (isset($rules['order_auto_close']) && $rules['order_auto_close'] > 0) {
$close_minue = $rules['order_auto_close'];
} else {
// 添加自动关闭队列
$config = Config::where('name', 'order')->cache(300)->find(); // 读取配置自动缓存 5 分钟
$config = isset($config) ? json_decode($config['value'], true) : [];
$close_minue = (isset($config['order_auto_close']) && $config['order_auto_close'] > 0)
? $config['order_auto_close'] : 15;
}
// 更新订单,将过期时间存入订单,前台展示支付倒计时
$order_ext['expired_time'] = time() + ($close_minue * 60);
\think\Queue::later(($close_minue * 60), '\addons\shopro\job\OrderAutoOper@autoClose', ['order' => $order], 'shopro');
// 更新订单扩展字段
$order->ext = json_encode($order_ext);
$order->save();
}
return $order;
}
/**
* 判断是否限制购买
*/
private function limitBuy($new_goods_list, $user)
{
foreach ($new_goods_list as $key => $goods) {
$detail = $goods['detail'];
$activity = $detail['activity'];
$rules = $activity['rules'] ?? [];
// 不存在,或者 0 不限制
if (!isset($rules['limit_buy']) || $rules['limit_buy'] <= 0) {
continue;
}
// 查询用户老订单,判断本次下单数量,判断是否超过购买限制, 未支付的或者已完成的都算
$buy_num = OrderItem::where('user_id', $user['id'])->where('goods_id', $detail['id'])->where('activity_id', $activity['id'])
->whereExists(function ($query) use ($detail, $activity) {
$order_table_name = (new Order())->getQuery()->getTable();
$table_name = (new OrderItem())->getQuery()->getTable();
$query->table($order_table_name)->where('order_id=' . $order_table_name . '.id')
->where('status', '>=', Order::STATUS_NOPAY); // 未支付,或者已支付的订单都算
})->sum('goods_num');
if (($buy_num + $goods['goods_num']) > $rules['limit_buy']) {
$msg = '该商品限购 ' . $rules['limit_buy'] . ' 件';
if ($buy_num < $rules['limit_buy']) {
$msg .= ',当前还可购买 ' . ($rules['limit_buy'] - $buy_num) . ' 件';
}
new Exception($msg);
}
}
}
}

查看文件

@@ -0,0 +1,186 @@
<?php
namespace addons\shopro\model;
use addons\shopro\exception\Exception;
use addons\shopro\model\Order;
use think\Model;
/**
* 活动-拼团
*/
class ActivityGroupon extends Model
{
// 表名,不含前缀
protected $name = 'shopro_activity_groupon';
// 自动写入时间戳字段
protected $autoWriteTimestamp = 'int';
// 定义时间戳字段名
protected $createTime = 'createtime';
protected $updateTime = 'updatetime';
protected $hidden = [];
// 追加属性
protected $append = [
];
// 获取正在进行中的团
public static function getActivityGroupon($params)
{
$goods_id = $params['goods_id'] ?? 0;
$activity_id = $params['activity_id'] ?? 0;
$activityGroupons = self::with('leader')
->where('goods_id', $goods_id)
->where('activity_id', $activity_id)
->where('status', 'ing')
->limit(10)
->select();
return $activityGroupons;
}
// 团详情
public static function getActivityGrouponDetail ($id) {
$activityGroupon = self::with(['my', 'grouponLog', 'activity' => function ($query) {
$query->removeOption('soft_delete')->with('activity_goods_sku_price'); // 关联团所属活动,并关联活动规格
}])->where('id', $id)->find();
if (!$activityGroupon) {
new Exception('团未找到');
}
$detail = Goods::getGoodsDetail($activityGroupon['goods_id']); // 正常团选规格时候要用
$activityGroupon['goods'] = $detail;
// 初始化拼团价为商品价,防止活动被物理删除
$activityGroupon['goods']['groupon_price'] = $activityGroupon['goods']['price'];
// 初始化活动状态为已结束
$activityGroupon['activity_status'] = 'ended';
// 处理当时的活动价格
if ($activityGroupon['activity']) {
$activity = $activityGroupon['activity'];
$activityGroupon['activity_status'] = $activity['status_code'];
$currentGoodsActivitySkuPrices = [];
foreach ($activity['activity_goods_sku_price'] as $k => $skuPrice) {
if ($skuPrice['status'] == 'up' && $skuPrice['goods_id'] == $activityGroupon['goods_id']) {
$currentGoodsActivitySkuPrices[] = $skuPrice;
}
}
if ($currentGoodsActivitySkuPrices) {
// 当时参加活动真实销量
$activityGroupon['goods']['sales'] = array_sum(array_column($currentGoodsActivitySkuPrices, 'sales'));
// 这个是活动最低价
$activityGroupon['goods']['groupon_price'] = $activityGroupon['goods']['price'] = min(array_column($currentGoodsActivitySkuPrices, 'price'));
}
}
return $activityGroupon;
}
/**
* 获取我的拼团列表
*
* @param [type] $type
* @return void
*/
public static function getMyGroupon($type) {
$user = User::info();
$logs = \addons\shopro\model\ActivityGrouponLog::with(['groupon', 'order.firstItem', 'goods' => function ($query) {
$query->removeOption('soft_delete'); // 商品查询包括被软删除的
}])->whereExists(function ($query) {
$logTable = (new \addons\shopro\model\ActivityGrouponLog())->getQuery()->getTable();
$orderTable = (new Order())->getQuery()->getTable();
$query->table($orderTable)->where($orderTable . '.id=' . $logTable . '.order_id')
->where('status', '>=', Order::STATUS_PAYED);
});
if ($type != 'all') {
$type = $type == 'finish' ? ['finish', 'finish-fictitious'] : [$type];
$logs = $logs->whereExists(function ($query) use ($type) {
$log_name = (new \addons\shopro\model\ActivityGrouponLog())->getQuery()->getTable();
$groupon_name = (new \addons\shopro\model\ActivityGroupon())->getQuery()->getTable();
$query->table($groupon_name)->where('id=' . $log_name . '.groupon_id')
->where('status', 'in', $type);
});
}
$logs = $logs->where('user_id', $user->id)
->order('id', 'desc')
->paginate(10)->toArray();
// 将列表的显示价格,都查当时购买的价格,和当时对应的活动的 真实销量
if ($grouponLogs = $logs['data']) {
// 拿到所有活动 ids
$activity_ids = array_column($grouponLogs, 'activity_id');
// 一次获取所有活动,包括被软删除的活动,并关联规格
$activities = \addons\shopro\model\Activity::withTrashed()
->with('activityGoodsSkuPrice')
->where('id', 'in', $activity_ids)->select();
$activities = array_column($activities, null, 'id');
foreach ($grouponLogs as $key => $grouponLog) {
if (isset($grouponLog['goods']) && $grouponLog['goods'] && isset($activities[$grouponLog['activity_id']])) {
$activity = $activities[$grouponLog['activity_id']];
// 拿到当前商品对应的活动规格
$currentGoodsActivitySkuPrices = [];
foreach ($activity['activityGoodsSkuPrice'] as $k => $skuPrice) {
if ($skuPrice['status'] == 'up' && $skuPrice['goods_id'] == $grouponLog['goods_id']) {
$currentGoodsActivitySkuPrices[] = $skuPrice;
}
}
// 当时参加活动真实销量
if ($currentGoodsActivitySkuPrices) {
$grouponLogs[$key]['goods']['sales'] = array_sum(array_column($currentGoodsActivitySkuPrices, 'sales'));
}
// 这个是购买时候的活动单价
if (isset($grouponLog['order']['first_item'])) {
$grouponLogs[$key]['goods']['price'] = $grouponLog['order']['first_item']['goods_price'];
}
}
}
$logs['data'] = $grouponLogs;
}
return $logs;
}
public function activity()
{
return $this->belongsTo(\addons\shopro\model\Activity::class, 'activity_id', 'id');
}
public function grouponLog()
{
return $this->hasMany(\addons\shopro\model\ActivityGrouponLog::class, 'groupon_id', 'id');
}
public function leader() {
return $this->hasOne(\addons\shopro\model\ActivityGrouponLog::class, 'groupon_id', 'id')->where('is_leader', 1);
}
public function my() {
$user = User::info();
return $this->hasOne(\addons\shopro\model\ActivityGrouponLog::class, 'groupon_id', 'id')->where('user_id', ($user ? $user->id : 0));
}
public function goods()
{
return $this->belongsTo(\addons\shopro\model\Goods::class, 'goods_id', 'id');
}
}

查看文件

@@ -0,0 +1,412 @@
<?php
namespace addons\shopro\model\store;
use addons\shopro\model\ActivityGroupon;
use think\Model;
use addons\shopro\exception\Exception;
use addons\shopro\library\traits\model\order\OrderScope;
use think\Db;
use think\Queue;
use traits\model\SoftDelete;
use addons\shopro\library\traits\model\order\OrderOper;
use addons\shopro\library\traits\model\order\OrderStatus;
use addons\shopro\model\Order as BaseOrder;
use addons\shopro\model\OrderItem;
use addons\shopro\model\Store;
use addons\shopro\model\User;
use addons\shopro\model\Verify;
/**
* 商户订单模型
*/
class Order extends BaseOrder
{
//
protected $append = [
'status_code',
'status_name',
'ext_arr'
];
/**
* 获取订单列表(已付款的),覆盖点 baseOrder 中 orderOper getList 方法
*/
public static function getList($params) {
$user = User::info();
$store = Store::info();
// 查询分页列表
$orders = self::buildQueryList($params)->paginate(10);
// 查询搜索条件下的总金额
$type = $params['type'] ?? 'all';
$orderIds = self::buildQueryList($params)->column('id');
$orderItems = OrderItem::where('order_id', 'in', $orderIds)
->where('store_id', $store['id']);
// if ($type != 'all') { // 订单中只有一个发货,待收货中也会把所有商品都查出来,所以这里不加条件了
// $where= []; // 一个订单中可能有多个商品在这个门店,并且有可能有的发货了,有的没发货
// switch ($type) {
// case 'nosend':
// $where = ['dispatch_status' => OrderItem::DISPATCH_STATUS_NOSEND];
// break;
// case 'noget':
// $where = ['dispatch_status' => OrderItem::DISPATCH_STATUS_SENDED];
// break;
// case 'finish':
// $where = ['dispatch_status' => OrderItem::DISPATCH_STATUS_GETED];
// break;
// }
// $orderItems = $orderItems->where($where)->where('refund_status', 'not in', [
// OrderItem::REFUND_STATUS_OK,
// OrderItem::REFUND_STATUS_FINISH
// ]);
// }
$total_money = $orderItems->sum('pay_price'); // pay_price 是不包含运费的金额(商家配送,和到店自提都是没有运费的,所以可以用这个来计算)
$result = [
'total_num' => $orders->total(),
'total_money' => $total_money,
'result' => $orders
];
return $result;
}
/**
* 要单独查询总金额
*/
private static function buildQueryList($params) {
$date_type = $params['date_type'] ?? 'today';
$date = $params['date'] ?? '';
$date = explode(',', $date);
$type = $params['type'] ?? 'all';
// 只查付款的订单
$order = (new self())->with('item')->payed();
switch ($type) {
case 'all':
$order = $order->store();
break;
case 'nosend':
$order = $order->storeNosend();
break;
case 'noget':
$order = $order->storeNoget();
break;
case 'finish':
$order = $order->storeFinish();
break;
}
switch ($date_type) {
case 'today':
$start = strtotime(date('Y-m-d'));
$end = strtotime(date('Y-m-d', (time() + 86400)));
break;
case 'yesterday':
$start = strtotime(date('Y-m-d', (time() - 86400)));
$end = strtotime(date('Y-m-d'));
break;
case 'week':
// $start = strtotime(date('Y-m-d', (time() - (86400 * 6)))); // 减 6 天
$start = strtotime(date('Y-m-d', strtotime("-1 week Monday"))); // 周一开始
$end = strtotime(date('Y-m-d', (time() + 86400))); // 到今天结束
break;
case 'month':
// $start = strtotime(date('Y-m-d', (time() - (86400 * 30)))); // 减 30 天
$start = strtotime(date('Y-m-d', mktime(0, 0, 0, date("m"), 1, date("Y")))); // 减 30 天
$end = strtotime(date('Y-m-d', (time() + 86400))); // 到今天结束
break;
case 'custom':
$date_start = $date[0] ?? date('Y-m-d'); // 默认查 today
$date_end = $date[1] ?? date('Y-m-d'); // 默认查 today
$start = strtotime($date_start);
$end = strtotime($date_end) + 86400;
break;
default:
$start = strtotime(date('Y-m-d'));
$end = strtotime(date('Y-m-d', (time() + 86400)));
break;
}
$order = $order->where('createtime', '>=', $start)
->where('createtime', '<', $end)
->excludeGrouponIng()
->order('id', 'desc');
return $order;
}
/**
* 订单详情 覆盖点 baseOrder 中 orderOper detail 方法
*/
public static function detail($params)
{
$user = User::info();
$store = Store::info();
extract($params);
$order = (new self())->with('item')->payed()->store();
if (isset($order_sn)) {
$order = $order->where('order_sn', $order_sn);
}
if (isset($id)) {
$order = $order->where('id', $id);
}
$order = $order->find();
if (!$order) {
new Exception('订单不存在');
}
return $order;
}
/**
* 订单发货
*/
public static function operSend($params) {
$user = User::info();
$store = Store::info();
extract($params);
Db::transaction(function () use ($id) {
$order = (new self())->store()->payed()->where('id', $id)->find();
if (!$order) {
new Exception('订单不存在');
}
// 加锁查询要发货的 item,防止多次请求造成产生多个核销码
$item = OrderItem::where('order_id', $order['id'])
->where('dispatch_status', \addons\shopro\model\OrderItem::DISPATCH_STATUS_NOSEND)
->where('refund_status', 'not in', [OrderItem::REFUND_STATUS_OK, OrderItem::REFUND_STATUS_FINISH])
->lock(true)->select();
if (!$item) {
new Exception('订单已发货');
}
// 订单里面这个门店相关的全部发货
$order['item'] = $item;
$order->storeOrderSend($order);
});
}
/**
* 商家核销订单, 覆盖点 baseOrder 中 orderOper operConfirm 方法
*/
public static function operConfirm($params)
{
$user = User::info();
$store = Store::info();
extract($params);
$id = $params['id'] ?? 0;
$codes = $params['codes'] ?? [];
Db::transaction(function () use ($store, $codes) {
// 查询核销码
$verifies = Verify::canUse()->with(['order', 'orderItem'])->where('type', 'verify')
->where('code', 'in', $codes)->lock(true)->select();
if (!$verifies) {
new Exception('核销码不可用');
}
$check_num = 0;
foreach ($verifies as $key => $verify) {
$order = $verify['order'];
$orderItem = $verify['orderItem'];
if (!$order || $order['status'] <= 0) {
new Exception('订单不存在');
}
if (!$orderItem || $orderItem['store_id'] != $store['id']) {
new Exception('您不能操作该核销码');
}
if ($orderItem['dispatch_status'] != OrderItem::DISPATCH_STATUS_SENDED) {
new Exception('当前商品已核销');
}
if (in_array($orderItem['refund_status'], [OrderItem::REFUND_STATUS_OK, OrderItem::REFUND_STATUS_FINISH])) {
new Exception('当前商品已退款');
}
$check_num ++;
$order->verifyGeted($order, $orderItem, $verify, ['oper_type' => 'store']);
}
if ($check_num <= 0) {
// 一个核销码也没核销
new Exception('核销失败,无效的核销码');
}
});
}
/* -------------------------- 访问器 ------------------------ */
/**
* 覆盖 BaseOrder 中 OrderStatus trait getStatus 方法
*/
protected function getStatus($data, $type)
{
$btns = [];
$status_name = '';
$status_desc = '';
switch ($this->status_code) {
case 'cancel':
$status_name = '已取消';
break;
case 'invalid':
$status_name = '交易关闭';
break;
case 'nopay':
$status_name = '待付款';
break;
// 收完货就算已完成
case 'nocomment':
case 'commented':
case 'finish':
$status_name = '已完成';
break;
case 'noget':
$dispatchType = [];
foreach ($this->item as $key => $item) {
// 获取 item status
$dispatchType[] = $item['dispatch_type'];
}
$dispatchType = array_unique(array_filter($dispatchType)); // 过滤重复,过滤空值
$status_name = '待收货';
if (in_array('selfetch', $dispatchType)) {
$status_name = '待提货/到店';
} else if (in_array('selfetch', $dispatchType)) {
$status_name = '配送中';
}
break;
case 'nosend':
$dispatchType = [];
foreach ($this->item as $key => $item) {
// 获取 item status
$dispatchType[] = $item['dispatch_type'];
}
$dispatchType = array_unique(array_filter($dispatchType)); // 过滤重复,过滤空值
$status_name = '待发货';
if (in_array('store', $dispatchType)) {
$status_name = '待配送';
} else if (in_array('selfetch', $dispatchType)) {
$status_name = '待备货';
}
break;
case 'refund_finish':
$status_name = '退款完成';
break;
case 'refund_ing':
$status_name = '退款处理中';
break;
case 'groupon_ing':
$status_name = '等待成团';
break;
case 'groupon_invalid':
$status_name = '拼团失败';
break;
}
return $type == 'status_name' ? $status_name : ($type == 'btns' ? $btns : $status_desc);
}
/**
* 获取支付成功之后的子状态, 门店订单,覆盖 baseOrder model orderStatus trait 中的 getPayedStatusCode 方法
*/
public function getPayedStatusCode($data)
{
$status_code = '';
// 循环判断 item 状态
$itemStatusCode = [];
foreach ($this->item as $key => $item) {
// 获取 item status
$itemStatusCode[] = (new OrderItem)->getBaseStatusCode($item->toArray(), 'order');
}
// 取出不重复不为空的 status_code
$statusCodes = array_unique(array_filter($itemStatusCode));
if (in_array('nosend', $statusCodes)) {
$status_code = 'nosend';
} else if (in_array('noget', $statusCodes)) {
$status_code = 'noget';
} else if (in_array('nocomment', $statusCodes)) {
// 存在待评价,就是待评价
$status_code = 'nocomment';
} else if (in_array('commented', $statusCodes)) {
// 存在已评价,就是已评价
$status_code = 'commented'; // 走这里,说明只评价了一部分
} else if (in_array('refund_finish', $statusCodes)) {
// 都在退款完成
$status_code = 'refund_finish';
} else if (in_array('refund_ing', $statusCodes)) {
// 都在退款中
$status_code = 'refund_ing';
} // 售后都不在总状态显示
$ext_arr = json_decode($data['ext'], true);
// 是拼团订单
if (
strpos($data['activity_type'], 'groupon') !== false &&
isset($ext_arr['groupon_id']) && $ext_arr['groupon_id']
) {
$groupon = ActivityGroupon::where('id', $ext_arr['groupon_id'])->find();
if ($groupon) {
if ($groupon['status'] == 'ing') {
// 尚未成团
$status_code = 'groupon_ing';
} else if ($groupon['status'] == 'invalid') {
$status_code = 'groupon_invalid';
}
}
}
return $status_code;
}
/* -------------------------- 模型关联 ------------------------ */
/**
* 关联订单 item,只查 store_id 是当前门店的
*/
public function item()
{
$store = Store::info();
return $this->hasMany(\addons\shopro\model\OrderItem::class, 'order_id', 'id')->where('store_id', ($store ? $store['id'] : 0));
}
/**
* 关联订单第一个 item,只查 store_id 是当前门店的
*/
public function firstItem()
{
$store = Store::info();
return $this->hasOne(\addons\shopro\model\OrderItem::class, 'order_id', 'id')->where('store_id', ($store ? $store['id'] : 0));
}
/* -------------------------- 模型关联 ------------------------ */
}

查看文件

@@ -1317,6 +1317,9 @@ class Order extends Base
}
}
// 拼团未成功订单不在后台订单列表展示,成团后再进入备货/发货流程
$orders = $orders->excludeGrouponIng();
return $orders;
}
}