HomePageDisplayClass.php 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324
  1. <?php
  2. namespace bizHd\homePageConfig\classes;
  3. use biz\shop\classes\ShopClass;
  4. use bizHd\goods\classes\GoodsClass;
  5. use bizHd\goods\classes\GoodsSettingClass;
  6. use common\components\business;
  7. use common\components\util;
  8. /**
  9. * 门店首页"展示数据"组装类
  10. * 把 HomePageConfigClass/HomePageModuleClass 存的原始配置,格式化为可直接渲染的结构
  11. * (拼好图片完整URL、过滤未开启/未上架内容、解析出真实商品列表、普通商品套上商城涨价等)。
  12. * 供 app-mall(顾客端商城首页)与 app-hd(商家端配置预览)共用同一套数据,
  13. * 保证"预览"看到的效果与顾客端真实首页完全一致。
  14. */
  15. class HomePageDisplayClass
  16. {
  17. /**
  18. * 一次性组装首页全部模块的展示数据
  19. *
  20. * @param int $mainId
  21. * @param int $shopId
  22. * @return array
  23. */
  24. public static function buildHome($mainId, $shopId)
  25. {
  26. // 三个普通商品模块共用一次涨价配置,避免重复查库
  27. $rise = GoodsSettingClass::getRise($mainId);
  28. return [
  29. 'modules' => HomePageConfigClass::getModules($mainId, $shopId),
  30. 'topNav' => self::formatTopNav($mainId, HomePageConfigClass::getTopNav($mainId, $shopId)),
  31. 'banner' => self::formatBanner(HomePageConfigClass::getBanner($mainId, $shopId)),
  32. 'navGrid' => self::formatNavGrid(HomePageModuleClass::getNavGrid($mainId, $shopId)),
  33. 'seckill' => self::formatActivity(HomePageModuleClass::getSeckill($mainId, $shopId, true)),
  34. 'groupBuy' => self::formatActivity(HomePageModuleClass::getGroupBuy($mainId, $shopId, true)),
  35. 'hot' => self::formatGoodsSection($mainId, $shopId, 'hot', $rise),
  36. 'new' => self::formatGoodsSection($mainId, $shopId, 'new', $rise),
  37. 'pullGoods' => self::formatGoodsSection($mainId, $shopId, 'pullGoods', $rise),
  38. ];
  39. }
  40. /**
  41. * 格式化顶部导航:门店名称不落 Redis,实时取 xhShop.merchantName
  42. *
  43. * @param int $mainId 门店 shopId
  44. * @param array $data HomePageConfigClass::getTopNav 返回值
  45. * @return array
  46. */
  47. public static function formatTopNav($mainId, $data)
  48. {
  49. $shop = ShopClass::getById($mainId, false, 'merchantName');
  50. $data['merchantName'] = is_array($shop) ? ($shop['merchantName'] ?? '') : '';
  51. return $data;
  52. }
  53. /**
  54. * 格式化轮播图:关闭时清空列表;开启时补上完整图片URL
  55. *
  56. * @param array $data HomePageConfigClass::getBanner 返回值
  57. * @return array
  58. */
  59. public static function formatBanner($data)
  60. {
  61. if (empty($data['enabled'])) {
  62. return ['enabled' => 0, 'interval' => intval($data['interval'] ?? 3), 'list' => []];
  63. }
  64. $list = [];
  65. foreach ($data['list'] as $item) {
  66. $img = $item['img'] ?? '';
  67. $formatted = $img !== '' ? business::formatUploadImg($img) : ['url' => ''];
  68. $list[] = [
  69. 'img' => $img,
  70. 'imgUrl' => $formatted['url'] ?? '',
  71. 'type' => intval($item['type'] ?? 1),
  72. 'value' => strval($item['value'] ?? ''),
  73. ];
  74. }
  75. return ['enabled' => 1, 'interval' => intval($data['interval'] ?? 3), 'list' => $list];
  76. }
  77. /**
  78. * 格式化金刚区:关闭时清空列表;开启时过滤未启用的单项,补上图标完整URL
  79. *
  80. * @param array $data HomePageModuleClass::getNavGrid 返回值
  81. * @return array
  82. */
  83. public static function formatNavGrid($data)
  84. {
  85. if (empty($data['enabled'])) {
  86. return ['enabled' => 0, 'cols' => intval($data['cols'] ?? 5), 'list' => []];
  87. }
  88. $list = [];
  89. foreach ($data['list'] as $item) {
  90. if (empty($item['enabled'])) {
  91. continue;
  92. }
  93. $icon = $item['icon'] ?? '';
  94. $iconUrl = $icon;
  95. $iconType = intval($item['iconType'] ?? 1);
  96. // 默认图标库 preset:N → OSS 静态图完整 URL(与前端默认图库一致)
  97. if (preg_match('/^preset:(\d+)$/', $icon, $m)) {
  98. $n = intval($m[1]);
  99. if ($n >= 1 && $n <= 12) {
  100. $path = 'hhb/images/navGrid-default/navGrid-default' . $n . '.png';
  101. $formatted = business::formatUploadImg($path);
  102. $iconUrl = $formatted['url'] ?? $path;
  103. }
  104. } elseif ($iconType === 2 && $icon !== '' && strpos($icon, 'http') !== 0) {
  105. // 商家上传图标转完整 URL
  106. $formatted = business::formatUploadImg($icon);
  107. $iconUrl = $formatted['url'] ?? $icon;
  108. }
  109. $list[] = [
  110. 'id' => $item['id'],
  111. 'name' => $item['name'],
  112. 'icon' => $icon,
  113. 'iconUrl' => $iconUrl,
  114. 'iconType' => $iconType,
  115. 'type' => intval($item['type'] ?? 3),
  116. 'value' => strval($item['value'] ?? ''),
  117. ];
  118. }
  119. return ['enabled' => 1, 'cols' => intval($data['cols'] ?? 5), 'list' => $list];
  120. }
  121. /**
  122. * 过滤上架中的活动商品并补上封面完整URL(不做条数截断)
  123. *
  124. * @param array $rawList
  125. * @return array
  126. */
  127. public static function filterActivityGoods($rawList)
  128. {
  129. $goods = [];
  130. foreach ($rawList as $item) {
  131. if (empty($item['status'])) {
  132. continue;
  133. }
  134. $cover = $item['cover'] ?? '';
  135. if ($cover !== '' && strpos($cover, 'http') !== 0) {
  136. $formatted = business::formatUploadImg($cover);
  137. $item['coverUrl'] = $formatted['url'] ?? $cover;
  138. } else {
  139. $item['coverUrl'] = $cover;
  140. }
  141. // 列表页统一用 id 字段,与普通商品列表对齐
  142. $item['id'] = intval($item['goodsId'] ?? ($item['id'] ?? 0));
  143. $goods[] = $item;
  144. }
  145. return $goods;
  146. }
  147. /**
  148. * 格式化秒杀/团购活动:只返回上架中的活动商品,并按 displayCount 截断;附带 goodsTotal
  149. *
  150. * @param array $data HomePageModuleClass::getSeckill/getGroupBuy 返回值
  151. * @return array
  152. */
  153. public static function formatActivity($data)
  154. {
  155. if (empty($data['enabled'])) {
  156. $data['goods'] = [];
  157. $data['goodsTotal'] = 0;
  158. return $data;
  159. }
  160. $goods = self::filterActivityGoods($data['goods'] ?? []);
  161. $data['goodsTotal'] = count($goods);
  162. $limit = !empty($data['expand']) ? 0 : HomePageModuleClass::resolveHomeDisplayLimit($data['displayCount'] ?? 0);
  163. if ($limit > 0 && count($goods) > $limit) {
  164. $goods = array_slice($goods, 0, $limit);
  165. }
  166. $data['goods'] = $goods;
  167. return $data;
  168. }
  169. /**
  170. * 格式化热门推荐/今日上新/下拉商品:关闭时清空商品;开启时按真实商品数量自动推导列数与展示条数
  171. * 输出价格为商城涨价后的售价(与详情页 obeyRiseRule 一致);秒杀/团购不走本方法
  172. *
  173. * @param int $mainId
  174. * @param int $shopId
  175. * @param string $moduleKey hot|new|pullGoods
  176. * @param array|null $rise 已查好的涨价配置;单模块接口未传时内部再查一次
  177. * @return array
  178. */
  179. public static function formatGoodsSection($mainId, $shopId, $moduleKey, $rise = null)
  180. {
  181. $data = HomePageModuleClass::getGoodsSection($mainId, $shopId, $moduleKey);
  182. if (empty($data['enabled'])) {
  183. $data['goods'] = [];
  184. $data['goodsTotal'] = 0;
  185. $data['layoutCols'] = 1;
  186. return $data;
  187. }
  188. $rows = HomePageModuleClass::matchGoodsRows($mainId, $data['type'], $data['value'], $data['sort']);
  189. $total = count($rows);
  190. $layout = HomePageModuleClass::resolveGoodsSectionLayout($total, $moduleKey);
  191. $data['layoutCols'] = $layout['layoutCols'];
  192. if ($layout['displayCount'] > 0 && $layout['displayCount'] < $total) {
  193. $rows = array_slice($rows, 0, $layout['displayCount']);
  194. }
  195. // 首页列表必须走与详情页相同的涨价公式,否则顾客看到原价、点进详情才变价
  196. $data['goods'] = self::applyMallRisePrice(HomePageModuleClass::formatGoodsRows($rows), $mainId, $rise);
  197. $data['goodsTotal'] = $total;
  198. return $data;
  199. }
  200. /**
  201. * 「更多」列表页分页入口:按模块返回全部可展示商品
  202. *
  203. * @param int $mainId
  204. * @param int $shopId
  205. * @param string $moduleKey seckill|groupBuy|hot|new|pullGoods
  206. * @param int $page
  207. * @param int $pageSize
  208. * @return array {list, total, totalPage, page, pageSize, moreData, title}
  209. */
  210. public static function getSectionGoodsPage($mainId, $shopId, $moduleKey, $page = 1, $pageSize = 10)
  211. {
  212. $mainId = intval($mainId);
  213. $shopId = intval($shopId);
  214. $moduleKey = strval($moduleKey);
  215. $page = max(1, intval($page));
  216. $pageSize = max(1, intval($pageSize));
  217. if ($mainId <= 0) {
  218. util::fail('无效门店');
  219. }
  220. // moduleKey / page / pageSize 形态校验由 GetSectionGoodsForm 完成
  221. $title = '';
  222. $list = [];
  223. $total = 0;
  224. // 秒杀/团购活动的起止时间,供列表页「更多」点击进详情时一并带上活动信息
  225. $activityEndTime = 0;
  226. $activityStartTime = 0;
  227. if ($moduleKey === 'seckill' || $moduleKey === 'groupBuy') {
  228. $data = $moduleKey === 'seckill'
  229. ? HomePageModuleClass::getSeckill($mainId, $shopId, true)
  230. : HomePageModuleClass::getGroupBuy($mainId, $shopId, true);
  231. $title = strval($data['title'] ?? '');
  232. if ($title === '') {
  233. $title = $moduleKey === 'groupBuy' ? '团购优惠' : '限时秒杀';
  234. }
  235. $activityStartTime = intval($data['startTime'] ?? 0);
  236. $activityEndTime = intval($data['endTime'] ?? 0);
  237. $all = empty($data['enabled']) ? [] : self::filterActivityGoods($data['goods'] ?? []);
  238. $total = count($all);
  239. $list = array_slice($all, ($page - 1) * $pageSize, $pageSize);
  240. } else {
  241. $data = HomePageModuleClass::getGoodsSection($mainId, $shopId, $moduleKey);
  242. $title = strval($data['name'] ?? '');
  243. if (empty($data['enabled'])) {
  244. $list = [];
  245. $total = 0;
  246. } else {
  247. $paged = HomePageModuleClass::resolveDisplayGoodsPaged(
  248. $mainId,
  249. $data['type'],
  250. $data['value'],
  251. $data['sort'],
  252. $page,
  253. $pageSize
  254. );
  255. // 「更多」列表与首页模块同源,同样补涨价;秒杀/团购走活动价,上面分支不套涨价
  256. $list = self::applyMallRisePrice($paged['list'], $mainId);
  257. $total = $paged['total'];
  258. }
  259. }
  260. $totalPage = $pageSize > 0 ? intval(ceil($total / $pageSize)) : 0;
  261. $moreData = $totalPage > $page ? 1 : 0;
  262. return [
  263. 'list' => $list,
  264. 'total' => $total,
  265. 'totalPage' => $totalPage,
  266. 'page' => $page,
  267. 'pageSize' => $pageSize,
  268. 'moreData' => $moreData,
  269. 'title' => $title,
  270. 'startTime' => $activityStartTime,
  271. 'endTime' => $activityEndTime,
  272. ];
  273. }
  274. /**
  275. * 给首页普通商品套上店铺涨价后的售价
  276. * 只走 obeyRiseRule 纯计算,不走 getFinalPrice(后者每条商品 Redis 查聊天报价,
  277. * 首页游客 userId=0 必 miss,下拉商品全量时会拖慢约 1 秒)。
  278. * 无价商品(priceType=0)不涨价;秒杀/团购走活动价,不要调用本方法。
  279. *
  280. * @param array $goods formatGoodsRows 产出的商品列表
  281. * @param int $mainId 商户 mainId,未传入 $rise 时用来查涨价配置
  282. * @param array|null $rise GoodsSettingClass::getRise 的返回值,传入则不再查库
  283. * @return array
  284. */
  285. public static function applyMallRisePrice($goods, $mainId, $rise = null)
  286. {
  287. if (empty($goods) || !is_array($goods)) {
  288. return $goods;
  289. }
  290. if (!is_array($rise)) {
  291. $rise = intval($mainId) > 0 ? GoodsSettingClass::getRise($mainId) : [];
  292. }
  293. $riseSwitch = intval($rise['riseSwitch'] ?? 0);
  294. $riseType = intval($rise['riseType'] ?? 0);
  295. $riseAmount = floatval($rise['riseAmount'] ?? 0);
  296. // 未开启涨价时直接返回原价,避免对全量下拉商品做无意义循环
  297. if ($riseSwitch != 1 || $riseAmount <= 0) {
  298. return $goods;
  299. }
  300. foreach ($goods as $key => $item) {
  301. // 无价商品不参与涨价,与详情页 modifyAndRisePrice 一致
  302. if (isset($item['priceType']) && intval($item['priceType']) === 0) {
  303. continue;
  304. }
  305. $goods[$key] = GoodsClass::obeyRiseRule($item, $riseSwitch, $riseType, $riseAmount, [], []);
  306. }
  307. return $goods;
  308. }
  309. }