GroupBuyController.php 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420
  1. <?php
  2. namespace mall\controllers;
  3. use bizHd\custom\classes\CustomClass;
  4. use bizHd\goods\classes\GoodsClass;
  5. use bizHd\groupBuy\classes\GroupBuyActivityClass;
  6. use bizHd\groupBuy\classes\GroupBuyClass;
  7. use bizHd\groupBuy\classes\GroupBuyMemberClass;
  8. use bizHd\order\services\OrderService;
  9. use bizHd\shop\classes\ShopClass;
  10. use bizMall\hd\classes\HdClass;
  11. use bizMall\pictext\classes\PicTextGoodsClass;
  12. use common\components\business;
  13. use common\components\dict;
  14. use common\components\orderSn;
  15. use common\components\stringUtil;
  16. use common\components\util;
  17. use mall\models\groupBuy\CreateGroupBuyForm;
  18. use mall\models\groupBuy\JoinGroupBuyForm;
  19. use Yii;
  20. /**
  21. * 商城端拼团接口
  22. * 落地页 / 团详情 / 开团 / 参团 / 我的拼团
  23. * 开团/参团订单按团购价结算,不叠加会员折扣、门店满减等优惠
  24. */
  25. class GroupBuyController extends BaseController
  26. {
  27. public $guestAccess = [
  28. 'goods-landing',
  29. 'detail',
  30. ];
  31. /**
  32. * 团购商品落地页:活动商品信息 + 商品详情图文 + 当前最快成团开放团
  33. * GET goodsId
  34. * 返回字段与商品详情页对齐:picTextGoods(标题/content JSON)供前端 rich-media-viewer 渲染
  35. */
  36. public function actionGoodsLanding()
  37. {
  38. $mainId = intval($this->mainId);
  39. if ($mainId <= 0) {
  40. util::fail('无效门店');
  41. }
  42. $goodsId = intval(Yii::$app->request->get('goodsId', 0));
  43. if ($goodsId <= 0) {
  44. util::fail('请选择商品');
  45. }
  46. $row = GroupBuyActivityClass::requireActiveGoodsRowByGoodsId($mainId, intval($this->shopId), $goodsId);
  47. $goods = GoodsClass::getById($goodsId, true);
  48. $cover = strval($row['cover'] ?? '');
  49. $coverUrl = $cover;
  50. if ($cover !== '' && strpos($cover, 'http') !== 0) {
  51. $formatted = business::formatUploadImg($cover);
  52. $coverUrl = $formatted['url'] ?? $cover;
  53. }
  54. // 轮播图兜底:保留商品图集;详情正文以 picTextGoods 为准(与 GoodsController::actionDetail 一致)
  55. $detailImages = [];
  56. $picTextGoods = [];
  57. if (!empty($goods)) {
  58. $pics = $goods->detail ?? ($goods->pic ?? '');
  59. if (is_string($pics) && $pics !== '') {
  60. $decoded = json_decode($pics, true);
  61. if (is_array($decoded)) {
  62. foreach ($decoded as $p) {
  63. if (is_string($p) && $p !== '') {
  64. if (strpos($p, 'http') === 0) {
  65. $detailImages[] = $p;
  66. } else {
  67. $fmt = business::formatUploadImg($p);
  68. $detailImages[] = $fmt['url'] ?? $p;
  69. }
  70. }
  71. }
  72. }
  73. }
  74. // 规格商品详情图文挂在主商品上,与商品详情接口取值规则一致
  75. $picTextGoodsId = empty($goods->masterId) ? $goodsId : intval($goods->masterId);
  76. $picTextRow = PicTextGoodsClass::getByCondition([
  77. 'mainId' => $mainId,
  78. 'goodsId' => $picTextGoodsId,
  79. ], true);
  80. if (!empty($picTextRow)) {
  81. $picTextGoods = $picTextRow;
  82. }
  83. }
  84. $openGroup = GroupBuyClass::getHottestOpenGroup(
  85. $mainId,
  86. $goodsId,
  87. intval($row['activityGoodsId'] ?? 0)
  88. );
  89. util::success([
  90. 'activityGoodsId' => intval($row['activityGoodsId']),
  91. 'activityId' => intval($row['activityId']),
  92. 'goodsId' => $goodsId,
  93. 'name' => strval($row['name']),
  94. 'cover' => $cover,
  95. 'coverUrl' => $coverUrl,
  96. 'price' => floatval($row['price']),
  97. 'originPrice' => floatval($row['originPrice']),
  98. 'groupSize' => intval($row['groupSize']),
  99. 'stock' => intval($row['stock']),
  100. 'limit' => intval($row['limit']),
  101. 'virtualGroup' => intval($row['virtualGroup']),
  102. 'virtualMinutes' => intval($row['virtualMinutes']),
  103. 'autoRefund' => 1,
  104. 'startTime' => intval($row['startTime']),
  105. 'endTime' => intval($row['endTime']),
  106. 'title' => strval($row['title']),
  107. 'subtitle' => strval($row['subtitle']),
  108. 'desc' => strval($row['desc']),
  109. 'showCountdown' => intval($row['showCountdown']),
  110. 'detailImages' => $detailImages,
  111. 'picTextGoods' => $picTextGoods,
  112. 'openGroup' => $openGroup,
  113. ]);
  114. }
  115. /**
  116. * 拼团详情
  117. * GET id
  118. */
  119. public function actionDetail()
  120. {
  121. $mainId = intval($this->mainId);
  122. if ($mainId <= 0) {
  123. util::fail('无效门店');
  124. }
  125. $id = intval(Yii::$app->request->get('id', 0));
  126. if ($id <= 0) {
  127. util::fail('请选择拼团');
  128. }
  129. $detail = GroupBuyClass::getDetail($mainId, $id);
  130. // 附带活动商品落地信息,便于分享进入后完整展示
  131. $landing = GroupBuyActivityClass::getActiveGoodsRowById($mainId, intval($this->shopId), intval($detail['activityGoodsId']));
  132. if (empty($landing)) {
  133. $landing = GroupBuyActivityClass::getActiveGoodsRowByGoodsId($mainId, intval($this->shopId), intval($detail['goodsId']));
  134. }
  135. $detail['activityGoods'] = $landing;
  136. $currentShopId = intval(Yii::$app->request->get('account', 0));
  137. $hd = HdClass::getByCondition(['shopId' => $currentShopId, 'userId' => $this->userId], false, null, 'id, customId');
  138. if (empty($hd)) {
  139. // util::error(errCode::GROUP_BUY_HD_NOT_BOUND, '客户暂无关联此店');
  140. // 客户与当前门店无关系时,立即建立
  141. if (empty($this->user)) {
  142. util::fail('请先登录');
  143. }
  144. $shop = ShopClass::getById($currentShopId, true);
  145. if (empty($shop)) {
  146. util::fail('门店不存在');
  147. }
  148. $respond = CustomClass::buildRelation($shop, $this->user);
  149. if (empty($respond) || empty($respond['hd'])) {
  150. util::fail('关联门店失败');
  151. }
  152. $hdId = intval($respond['hd']->id);
  153. } else {
  154. $hdId = intval($hd['id']);
  155. }
  156. $detail['hdId'] = $hdId;
  157. util::success($detail);
  158. }
  159. /**
  160. * 我要开团:创建团 + 创建待支付订单 + 挂接团长成员
  161. */
  162. public function actionCreate()
  163. {
  164. $form = new CreateGroupBuyForm();
  165. $form->loadAndValidate();
  166. $mainId = intval($this->mainId);
  167. $custom = $this->custom;
  168. if (empty($custom)) {
  169. util::fail('请先登录');
  170. }
  171. $customId = intval($custom->id);
  172. $activityGoodsId = intval($form->activityGoodsId);
  173. $goodsNum = max(1, intval($form->goodsNum));
  174. $row = GroupBuyActivityClass::requireActiveGoodsRowById($mainId, intval($this->shopId), $activityGoodsId);
  175. GroupBuyClass::assertPurchaseLimit($mainId, $customId, $row, $goodsNum);
  176. $connection = Yii::$app->db;
  177. $transaction = $connection->beginTransaction();
  178. try {
  179. $group = GroupBuyClass::createGroup($mainId, intval($this->shopId), $customId, $row);
  180. $groupBuyId = intval($group['id']);
  181. $order = $this->createGroupBuyOrder($form, $row, $groupBuyId, $goodsNum);
  182. GroupBuyMemberClass::bindOrderMember(
  183. $groupBuyId,
  184. $mainId,
  185. intval($this->shopId),
  186. $customId,
  187. intval($order->id),
  188. strval($order->orderSn),
  189. GroupBuyMemberClass::ROLE_LEADER
  190. );
  191. $transaction->commit();
  192. util::success([
  193. 'groupBuyId' => $groupBuyId,
  194. 'orderSn' => $order->orderSn,
  195. 'totalPrice' => $order->actPrice,
  196. 'id' => $order->id,
  197. ]);
  198. } catch (\Exception $e) {
  199. $transaction->rollBack();
  200. Yii::error('开团失败:' . $e->getMessage(), __METHOD__);
  201. $msg = $e->getMessage();
  202. if ($msg !== '' && mb_strlen($msg) < 80 && strpos($msg, 'SQLSTATE') === false) {
  203. util::fail($msg);
  204. }
  205. util::fail('开团失败');
  206. }
  207. }
  208. /**
  209. * 我要参团:加入已有团 + 创建待支付订单
  210. */
  211. public function actionJoin()
  212. {
  213. $form = new JoinGroupBuyForm();
  214. $form->loadAndValidate();
  215. $mainId = intval($this->mainId);
  216. $custom = $this->custom;
  217. $currentShopId = intval(Yii::$app->request->get('account', 0));
  218. if (empty($custom)) {
  219. $hd = HdClass::getByCondition(['shopId' => $currentShopId, 'userId' => $this->userId], false, null, 'id, customId');
  220. if (empty($hd)) {
  221. // 客户与当前门店无关系时,立即建立
  222. if (empty($this->user) || empty($this->shop)) {
  223. util::fail('请先登录');
  224. }
  225. $respond = CustomClass::buildRelation($this->shop, $this->user);
  226. if (empty($respond) || empty($respond['custom'])) {
  227. util::fail('关联门店失败');
  228. }
  229. $custom = $respond['custom'];
  230. } else {
  231. $customId = $hd['customId'];
  232. $custom = CustomClass::getById($customId, true);
  233. }
  234. }
  235. $customId = intval($custom->id);
  236. $groupBuyId = intval($form->groupBuyId);
  237. $goodsNum = max(1, intval($form->goodsNum));
  238. $connection = Yii::$app->db;
  239. $transaction = $connection->beginTransaction();
  240. try {
  241. $group = GroupBuyClass::assertCanJoin($mainId, $groupBuyId, $customId);
  242. $row = GroupBuyActivityClass::getActiveGoodsRowById($mainId, intval($this->shopId), intval($group->activityGoodsId));
  243. if (empty($row)) {
  244. // 活动结束后仍允许在截止前参团:用团上快照价
  245. $row = [
  246. 'activityId' => intval($group->activityId),
  247. 'activityGoodsId' => intval($group->activityGoodsId),
  248. 'goodsId' => intval($group->goodsId),
  249. 'price' => floatval($group->price),
  250. 'limit' => 1,
  251. 'endTime' => intval($group->deadline),
  252. 'groupSize' => intval($group->needNum),
  253. 'virtualGroup' => intval($group->virtualGroup),
  254. 'virtualMinutes' => 0,
  255. ];
  256. }
  257. GroupBuyClass::assertPurchaseLimit($mainId, $customId, $row, $goodsNum);
  258. $order = $this->createGroupBuyOrder($form, $row, $groupBuyId, $goodsNum, $custom);
  259. GroupBuyMemberClass::bindOrderMember(
  260. $groupBuyId,
  261. $mainId,
  262. intval($this->shopId),
  263. $customId,
  264. intval($order->id),
  265. strval($order->orderSn),
  266. GroupBuyMemberClass::ROLE_MEMBER
  267. );
  268. $transaction->commit();
  269. util::success([
  270. 'groupBuyId' => $groupBuyId,
  271. 'orderSn' => $order->orderSn,
  272. 'totalPrice' => $order->actPrice,
  273. 'id' => $order->id,
  274. ]);
  275. } catch (\Exception $e) {
  276. $transaction->rollBack();
  277. Yii::error('参团失败:' . $e->getMessage(), __METHOD__);
  278. $msg = $e->getMessage();
  279. if ($msg !== '' && mb_strlen($msg) < 80 && strpos($msg, 'SQLSTATE') === false) {
  280. util::fail($msg);
  281. }
  282. util::fail('参团失败');
  283. }
  284. }
  285. /**
  286. * 我的拼团列表
  287. */
  288. public function actionMyList()
  289. {
  290. $mainId = intval($this->mainId);
  291. $custom = $this->custom;
  292. if (empty($custom)) {
  293. util::fail('请先登录');
  294. }
  295. util::success([
  296. 'list' => GroupBuyClass::getMyList($mainId, intval($custom->id)),
  297. ]);
  298. }
  299. /**
  300. * 组装并创建拼团订单(核价以后端活动商品为准)
  301. * 团购价已是活动优惠价:跳过会员折、门店满减,避免再叠折扣
  302. *
  303. * @param object $form
  304. * @param array $row
  305. * @param int $groupBuyId
  306. * @param int $goodsNum
  307. * @return object
  308. */
  309. private function createGroupBuyOrder($form, $row, $groupBuyId, $goodsNum, $currenCustom = [])
  310. {
  311. $shop = $this->shop;
  312. $openShop = ShopClass::getOpenShop($shop);
  313. if ($openShop == 0) {
  314. util::fail('已休店');
  315. }
  316. $mainId = intval($this->mainId);
  317. $custom = $this->custom;
  318. if (empty($custom) && !empty($currenCustom)) {
  319. $custom = $currenCustom;
  320. }
  321. $hd = $this->hd;
  322. $user = $this->user;
  323. $saleGoodsId = intval($row['goodsId']);
  324. $goodsInfo = GoodsClass::getById($saleGoodsId, true);
  325. if (empty($goodsInfo) || intval($goodsInfo->mainId) !== $mainId) {
  326. util::fail('没有找到商品');
  327. }
  328. $stock = intval($goodsInfo->stock ?? 0);
  329. if ($stock < $goodsNum) {
  330. util::fail('库存不足');
  331. }
  332. $unitPrice = floatval($row['price'] ?? 0);
  333. if ($unitPrice <= 0) {
  334. util::fail('团购价格异常');
  335. }
  336. $goodsPrice = bcmul($unitPrice, $goodsNum, 2);
  337. $sendType = intval($form->sendType ?? dict::getDict('sendType', 'shopGet'));
  338. $sendCost = 0;
  339. // 跑腿配送暂不在拼团链路单独报价,统一引导自取或送货上门(运费0,与花店协商)
  340. if ($sendType == dict::getDict('sendType', 'thirdSend')) {
  341. util::fail('拼团暂不支持跑腿,请选择自取或送货上门');
  342. }
  343. if ($sendType == dict::getDict('sendType', 'carGet')) {
  344. if (empty($form->receiveUserName)) {
  345. util::fail('请填写收花人名字');
  346. }
  347. if (empty($form->receiveMobile) || !stringUtil::isMobile($form->receiveMobile)) {
  348. util::fail('请填写正确的收花人手机号');
  349. }
  350. }
  351. $post = [
  352. 'sendType' => $sendType,
  353. 'receiveUserName' => strval($form->receiveUserName ?? ''),
  354. 'receiveMobile' => strval($form->receiveMobile ?? ''),
  355. 'address' => strval($form->address ?? ''),
  356. 'floor' => strval($form->floor ?? ''),
  357. 'city' => strval($form->city ?? ''),
  358. 'long' => strval($form->long ?? ''),
  359. 'lat' => strval($form->lat ?? ''),
  360. 'showAddress' => strval($form->showAddress ?? ''),
  361. 'remark' => strval($form->remark ?? ''),
  362. 'reachDate' => !empty($form->reachDate) ? $form->reachDate : date('Y-m-d'),
  363. 'reachPeriod' => strval($form->reachPeriod ?? ''),
  364. 'sendCost' => $sendCost,
  365. 'sendDistance' => 0,
  366. 'sjId' => intval($this->sjId ?: ($shop->sjId ?? 0)),
  367. 'shopId' => $this->shopId,
  368. 'mainId' => $mainId,
  369. 'userId' => $this->userId,
  370. 'customId' => intval($custom->id),
  371. 'customName' => strval($custom->name ?? ''),
  372. 'customNamePy' => stringUtil::py(strval($custom->name ?? '')),
  373. 'hdId' => intval($hd->id ?? 0),
  374. 'hdName' => strval($hd->name ?? ''),
  375. 'bookName' => strval($user->name ?? ''),
  376. 'bookMobile' => !empty($form->bookMobile) && stringUtil::isMobile($form->bookMobile)
  377. ? $form->bookMobile
  378. : strval($user->mobile ?? ''),
  379. 'payWay' => 0,
  380. 'store' => 0,
  381. 'modifyPrice' => stringUtil::calcAdd($sendCost, $goodsPrice),
  382. 'deadline' => time() + 1800,
  383. 'orderSn' => orderSn::getOrderSn(),
  384. 'fromType' => dict::getDict('fromType', 'mall'),
  385. 'cash' => 0,
  386. 'needPrint' => dict::getDict('needPrint', 'need'),
  387. 'groupBuyId' => intval($groupBuyId),
  388. // 拼团价已是活动价:跳过会员折和门店满减,避免 createHdOrder/addOrder 再叠折扣
  389. 'skipMemberDiscount' => 1,
  390. 'skipShopMeetCut' => 1,
  391. 'product' => [[
  392. 'productId' => $saleGoodsId,
  393. 'unitType' => 0,
  394. 'property' => 0,
  395. 'unitPrice' => $unitPrice,
  396. 'num' => $goodsNum,
  397. ]],
  398. ];
  399. if ($sendType != 1 && empty($post['reachPeriod'])) {
  400. util::fail('请选择配送时间');
  401. }
  402. return OrderService::createHdOrder($post, $custom, 0);
  403. }
  404. }