recoverShopAndResolveHd(); $mainId = $ctx['mainId']; $goodsId = intval(Yii::$app->request->get('goodsId', 0)); if ($goodsId <= 0) { util::fail('请选择商品'); } $row = GroupBuyActivityClass::requireActiveGoodsRowByGoodsId($mainId, intval($this->shopId), $goodsId); $goods = GoodsClass::getById($goodsId, true); $cover = strval($row['cover'] ?? ''); $coverUrl = $cover; if ($cover !== '' && strpos($cover, 'http') !== 0) { $formatted = business::formatUploadImg($cover); $coverUrl = $formatted['url'] ?? $cover; } // 轮播图兜底:保留商品图集;详情正文以 picTextGoods 为准(与 GoodsController::actionDetail 一致) $detailImages = []; $picTextGoods = []; if (!empty($goods)) { $pics = $goods->detail ?? ($goods->pic ?? ''); if (is_string($pics) && $pics !== '') { $decoded = json_decode($pics, true); if (is_array($decoded)) { foreach ($decoded as $p) { if (is_string($p) && $p !== '') { if (strpos($p, 'http') === 0) { $detailImages[] = $p; } else { $fmt = business::formatUploadImg($p); $detailImages[] = $fmt['url'] ?? $p; } } } } } // 规格商品详情图文挂在主商品上,与商品详情接口取值规则一致 $picTextGoodsId = empty($goods->masterId) ? $goodsId : intval($goods->masterId); $picTextRow = PicTextGoodsClass::getByCondition([ 'mainId' => $mainId, 'goodsId' => $picTextGoodsId, ], true); if (!empty($picTextRow)) { $picTextGoods = $picTextRow; } } $openGroup = GroupBuyClass::getHottestOpenGroup( $mainId, $goodsId, intval($row['activityGoodsId'] ?? 0) ); util::success([ 'activityGoodsId' => intval($row['activityGoodsId']), 'activityId' => intval($row['activityId']), 'goodsId' => $goodsId, 'name' => strval($row['name']), 'cover' => $cover, 'coverUrl' => $coverUrl, 'price' => floatval($row['price']), 'originPrice' => floatval($row['originPrice']), 'groupSize' => intval($row['groupSize']), 'stock' => intval($row['stock']), 'limit' => intval($row['limit']), 'virtualGroup' => intval($row['virtualGroup']), 'virtualMinutes' => intval($row['virtualMinutes']), 'autoRefund' => 1, 'startTime' => intval($row['startTime']), 'endTime' => intval($row['endTime']), 'title' => strval($row['title']), 'subtitle' => strval($row['subtitle']), 'desc' => strval($row['desc']), 'showCountdown' => intval($row['showCountdown']), 'detailImages' => $detailImages, 'picTextGoods' => $picTextGoods, // 花束是否包运费:结算页跑腿模式(bouquetIncluded)与混合单同源判断 'freightType' => !empty($goods) ? intval($goods->freightType ?? 0) : 0, 'openGroup' => $openGroup, // 供 mallApp 写入本地 hdId,避免后续开团/参团仍带旧店 hdId 'hdId' => $ctx['hdId'], 'inviteBound' => $ctx['inviteBound'], ]); } /** * 拼团详情 * GET id * 游客可浏览;已登录则按 account 恢复门店并建客,回传 hdId 供前端缓存 */ public function actionDetail() { $ctx = $this->recoverShopAndResolveHd(); $mainId = $ctx['mainId']; $id = intval(Yii::$app->request->get('id', 0)); if ($id <= 0) { util::fail('请选择拼团'); } $detail = GroupBuyClass::getDetail($mainId, $id); // 附带活动商品落地信息,便于分享进入后完整展示 $landing = GroupBuyActivityClass::getActiveGoodsRowById($mainId, intval($this->shopId), intval($detail['activityGoodsId'])); if (empty($landing)) { $landing = GroupBuyActivityClass::getActiveGoodsRowByGoodsId($mainId, intval($this->shopId), intval($detail['goodsId'])); } if (!empty($landing) && !isset($landing['freightType'])) { $landingGoodsId = intval($landing['goodsId'] ?? ($detail['goodsId'] ?? 0)); $landingGoods = $landingGoodsId > 0 ? GoodsClass::getById($landingGoodsId, true) : null; // 分享进详情也要带包运费标记,否则结算页跑腿会误走平台报价 $landing['freightType'] = !empty($landingGoods) ? intval($landingGoods->freightType ?? 0) : 0; } $detail['activityGoods'] = $landing; $detail['hdId'] = $ctx['hdId']; $detail['inviteBound'] = $ctx['inviteBound']; util::success($detail); } /** * 我要开团:创建团 + 创建待支付订单 + 挂接团长成员 */ public function actionCreate() { $form = new CreateGroupBuyForm(); $form->loadAndValidate(); $ctx = $this->ensureCustomerContext(); $mainId = $ctx['mainId']; $custom = $this->custom; $customId = intval($custom->id); $activityGoodsId = intval($form->activityGoodsId); $goodsNum = max(1, intval($form->goodsNum)); $row = GroupBuyActivityClass::requireActiveGoodsRowById($mainId, intval($this->shopId), $activityGoodsId); GroupBuyClass::assertPurchaseLimit($mainId, $customId, $row, $goodsNum); $connection = Yii::$app->db; $transaction = $connection->beginTransaction(); try { $group = GroupBuyClass::createGroup($mainId, intval($this->shopId), $customId, $row); $groupBuyId = intval($group['id']); $order = $this->createGroupBuyOrder($form, $row, $groupBuyId, $goodsNum); GroupBuyMemberClass::bindOrderMember( $groupBuyId, $mainId, intval($this->shopId), $customId, intval($order->id), strval($order->orderSn), GroupBuyMemberClass::ROLE_LEADER ); $transaction->commit(); util::success([ 'groupBuyId' => $groupBuyId, 'orderSn' => $order->orderSn, 'totalPrice' => $order->actPrice, 'id' => $order->id, ]); } catch (\Exception $e) { $transaction->rollBack(); Yii::error('开团失败:' . $e->getMessage(), __METHOD__); $msg = $e->getMessage(); if ($msg !== '' && mb_strlen($msg) < 80 && strpos($msg, 'SQLSTATE') === false) { util::fail($msg); } util::fail('开团失败'); } } /** * 我要参团:加入已有团 + 创建待支付订单 */ public function actionJoin() { $form = new JoinGroupBuyForm(); $form->loadAndValidate(); $ctx = $this->ensureCustomerContext(); $mainId = $ctx['mainId']; $custom = $this->custom; $customId = intval($custom->id); $groupBuyId = intval($form->groupBuyId); $goodsNum = max(1, intval($form->goodsNum)); $connection = Yii::$app->db; $transaction = $connection->beginTransaction(); try { $group = GroupBuyClass::assertCanJoin($mainId, $groupBuyId, $customId); $row = GroupBuyActivityClass::getActiveGoodsRowById($mainId, intval($this->shopId), intval($group->activityGoodsId)); if (empty($row)) { // 活动结束后仍允许在截止前参团:用团上快照价 $row = [ 'activityId' => intval($group->activityId), 'activityGoodsId' => intval($group->activityGoodsId), 'goodsId' => intval($group->goodsId), 'price' => floatval($group->price), 'limit' => 1, 'endTime' => intval($group->deadline), 'groupSize' => intval($group->needNum), 'virtualGroup' => intval($group->virtualGroup), 'virtualMinutes' => 0, ]; } GroupBuyClass::assertPurchaseLimit($mainId, $customId, $row, $goodsNum); $order = $this->createGroupBuyOrder($form, $row, $groupBuyId, $goodsNum, $custom); GroupBuyMemberClass::bindOrderMember( $groupBuyId, $mainId, intval($this->shopId), $customId, intval($order->id), strval($order->orderSn), GroupBuyMemberClass::ROLE_MEMBER ); $transaction->commit(); util::success([ 'groupBuyId' => $groupBuyId, 'orderSn' => $order->orderSn, 'totalPrice' => $order->actPrice, 'id' => $order->id, ]); } catch (\Exception $e) { $transaction->rollBack(); Yii::error('参团失败:' . $e->getMessage(), __METHOD__); $msg = $e->getMessage(); if ($msg !== '' && mb_strlen($msg) < 80 && strpos($msg, 'SQLSTATE') === false) { util::fail($msg); } util::fail('参团失败'); } } /** * 我的拼团列表 * 换店后可能仍带旧 hdId,先恢复门店并绑定当前店顾客再查列表 */ public function actionMyList() { $ctx = $this->ensureCustomerContext(); util::success([ 'list' => GroupBuyClass::getMyList($ctx['mainId'], intval($this->custom->id)), ]); } /** * 组装并创建拼团订单(核价以后端活动商品为准) * 团购价已是活动优惠价:跳过会员折、门店满减、红包;运费/包装费与混合结算同源计算 * 跑腿(sendType=2)走与混合单相同的 calcMixRunnerFreight,不信任前端报价 * * @param object $form * @param array $row * @param int $groupBuyId * @param int $goodsNum * @return object */ private function createGroupBuyOrder($form, $row, $groupBuyId, $goodsNum, $currenCustom = []) { $shop = $this->shop; $openShop = ShopClass::getOpenShop($shop); if ($openShop == 0) { util::fail('已休店'); } $mainId = intval($this->mainId); $custom = $this->custom; if (empty($custom) && !empty($currenCustom)) { $custom = $currenCustom; } if (empty($custom)) { util::fail('请先登录'); } $hd = $this->hd; $hdId = !empty($hd) ? intval($hd->id ?? 0) : 0; $hdName = !empty($hd) ? strval($hd->name ?? '') : ''; $user = $this->user; $saleGoodsId = intval($row['goodsId']); $goodsInfo = GoodsClass::getById($saleGoodsId, true); if (empty($goodsInfo) || intval($goodsInfo->mainId) !== $mainId) { util::fail('没有找到商品'); } $stock = intval($goodsInfo->stock ?? 0); if ($stock < $goodsNum) { util::fail('库存不足'); } $unitPrice = floatval($row['price'] ?? 0); if ($unitPrice <= 0) { util::fail('团购价格异常'); } $goodsPrice = bcmul($unitPrice, $goodsNum, 2); $sendType = intval($form->sendType ?? dict::getDict('sendType', 'shopGet')); // 商家配送(0)/跑腿(2)必须有收花人与坐标,与混合结算一致 if (in_array($sendType, [dict::getDict('sendType', 'carGet'), dict::getDict('sendType', 'thirdSend')], true)) { if (empty($form->receiveUserName)) { util::fail('请填写收花人名字'); } if (empty($form->receiveMobile) || !stringUtil::isMobile($form->receiveMobile)) { util::fail('请填写正确的收花人手机号'); } if (empty($form->long) || empty($form->lat)) { util::fail('请先完善地址'); } } // 商家配送(0)/跑腿(2)必填配送时间;自取与物流/快递不强制(与混合结算一致) if (in_array($sendType, [0, 2], true) && empty($form->reachPeriod)) { util::fail('请选择配送时间'); } $customInfo = PsMethodClass::buildCustomInfo($hd, $custom); $additionalCosts = PsMethodClass::calcFeeForMallContext( $this->shopId, $mainId, $sendType, $goodsPrice, $goodsNum, intval($custom->id), $customInfo ); $unMeetSendCost = $additionalCosts['sendCost'] ?? 0; $packingFee = $additionalCosts['packCost'] ?? 0; $psMethodError = PsMethodClass::checkLimit( $this->shopId, $mainId, $sendType, $goodsPrice, $goodsNum, $packingFee, $unMeetSendCost, intval($custom->id), $customInfo ); if ($psMethodError !== '') { util::fail($psMethodError); } $freightType = intval($goodsInfo->freightType ?? 0); $resolvedProduct = [[ 'productId' => $saleGoodsId, 'unitType' => 0, 'property' => 0, 'freightType' => $freightType, 'unitPrice' => $unitPrice, 'num' => $goodsNum, ]]; $runnerSendCost = 0; $sendDistance = 0; $freightSource = ''; // 跑腿距离运费与混合单同源;未达门槛附加运费仍由 calcFee 单独给出 if ($sendType == dict::getDict('sendType', 'thirdSend')) { $mapSet = \biz\shop\classes\ShopClass::hasIntraCity($shop); $openIntraCity = $mapSet['openIntraCity'] ?? 0; $runnerPost = [ 'receiveUserName' => strval($form->receiveUserName ?? ''), 'receiveMobile' => strval($form->receiveMobile ?? ''), 'address' => strval($form->address ?? ''), 'floor' => strval($form->floor ?? ''), 'city' => strval($form->city ?? ''), 'dist' => strval($form->dist ?? ''), 'long' => $form->long ?? '', 'lat' => $form->lat ?? '', 'deliveryPlatform' => strval($form->deliveryPlatform ?? ''), 'deliveryBracketContent' => strval($form->deliveryBracketContent ?? ''), 'remark' => strval($form->remark ?? ''), ]; $runnerResult = PsMethodClass::calcMixRunnerFreight( $shop, $this->shopId, $mainId, $runnerPost, $resolvedProduct, 0, $goodsPrice, $openIntraCity, 0, $goodsNum, 'PT-GB-', '团购单' ); $runnerSendCost = $runnerResult['sendCost']; $sendDistance = $runnerResult['sendDistance']; $freightSource = $runnerResult['freightSource']; } $sendCost = bcadd($unMeetSendCost, $runnerSendCost, 2); $modifyPrice = bcadd($goodsPrice, $sendCost, 2); $modifyPrice = bcadd($modifyPrice, $packingFee, 2); $post = [ 'sendType' => $sendType, 'receiveUserName' => strval($form->receiveUserName ?? ''), 'receiveMobile' => strval($form->receiveMobile ?? ''), 'address' => strval($form->address ?? ''), 'floor' => strval($form->floor ?? ''), 'city' => strval($form->city ?? ''), 'dist' => strval($form->dist ?? ''), 'province' => strval($form->province ?? ''), 'long' => strval($form->long ?? ''), 'lat' => strval($form->lat ?? ''), 'showAddress' => strval($form->showAddress ?? ''), 'remark' => strval($form->remark ?? ''), 'reachDate' => !empty($form->reachDate) ? $form->reachDate : date('Y-m-d'), 'reachPeriod' => strval($form->reachPeriod ?? ''), 'sendCost' => $sendCost, 'packingFee' => $packingFee, 'sendDistance' => $sendDistance, 'freightSource' => $freightSource, 'runnerSendCost' => $runnerSendCost, 'needCard' => intval($form->needCard ?? 0), 'cardInfo' => intval($form->needCard ?? 0) === 1 ? strval($form->cardInfo ?? '') : '', 'anonymity' => intval($form->anonymity ?? 0), 'sjId' => intval($this->sjId ?: ($shop->sjId ?? 0)), 'shopId' => $this->shopId, 'mainId' => $mainId, 'userId' => $this->userId, 'customId' => intval($custom->id), 'customName' => strval($custom->name ?? ''), 'customNamePy' => stringUtil::py(strval($custom->name ?? '')), 'hdId' => $hdId, 'hdName' => $hdName, 'bookName' => strval($user->name ?? ''), 'bookMobile' => !empty($form->bookMobile) && stringUtil::isMobile($form->bookMobile) ? $form->bookMobile : strval($user->mobile ?? ''), 'payWay' => 0, 'store' => 0, 'modifyPrice' => $modifyPrice, 'deadline' => time() + 1800, 'orderSn' => orderSn::getOrderSn(), 'fromType' => dict::getDict('fromType', 'mall'), 'cash' => 0, 'needPrint' => dict::getDict('needPrint', 'need'), 'groupBuyId' => intval($groupBuyId), // 拼团价已是活动价:跳过会员折、门店满减、红包,避免再叠优惠 'skipMemberDiscount' => 1, 'skipShopMeetCut' => 1, 'hbId' => 0, 'hbAmount' => 0, 'product' => $resolvedProduct, ]; return OrderService::createHdOrder($post, $custom, 0); } }