diff --git a/addons/shopro/library/traits/Groupon.php b/addons/shopro/library/traits/Groupon.php new file mode 100644 index 0000000..785f498 --- /dev/null +++ b/addons/shopro/library/traits/Groupon.php @@ -0,0 +1,429 @@ +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; + } +} diff --git a/addons/shopro/library/traits/model/order/OrderScope.php b/addons/shopro/library/traits/model/order/OrderScope.php new file mode 100644 index 0000000..b9fc45a --- /dev/null +++ b/addons/shopro/library/traits/model/order/OrderScope.php @@ -0,0 +1,300 @@ +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); + }); + } +} diff --git a/addons/shopro/listener/order/Create.php b/addons/shopro/listener/order/Create.php new file mode 100644 index 0000000..1a4dd0f --- /dev/null +++ b/addons/shopro/listener/order/Create.php @@ -0,0 +1,198 @@ +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); + } + } + } +} diff --git a/addons/shopro/model/ActivityGroupon.php b/addons/shopro/model/ActivityGroupon.php new file mode 100644 index 0000000..bec19b1 --- /dev/null +++ b/addons/shopro/model/ActivityGroupon.php @@ -0,0 +1,186 @@ +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'); + } +} diff --git a/addons/shopro/model/store/Order.php b/addons/shopro/model/store/Order.php new file mode 100644 index 0000000..1a25f1c --- /dev/null +++ b/addons/shopro/model/store/Order.php @@ -0,0 +1,412 @@ +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)); + } + /* -------------------------- 模型关联 ------------------------ */ +} diff --git a/application/admin/controller/shopro/order/Order.php b/application/admin/controller/shopro/order/Order.php index 55425f9..2011b68 100644 --- a/application/admin/controller/shopro/order/Order.php +++ b/application/admin/controller/shopro/order/Order.php @@ -1317,6 +1317,9 @@ class Order extends Base } } + // 拼团未成功订单不在后台订单列表展示,成团后再进入备货/发货流程 + $orders = $orders->excludeGrouponIng(); + return $orders; } }