DiscountService.php 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517
  1. <?php
  2. namespace app\common\Service;
  3. use think\Db;
  4. use think\Model;
  5. use app\common\Enum\StatusEnum;
  6. use app\common\Enum\ActivityEnum;
  7. use app\common\Enum\GoodsEnum;
  8. /**
  9. * 折扣活动服务类
  10. *
  11. * 外层折扣字段说明:
  12. * - has_discount: 是否有折扣活动
  13. * - discount_price: 折扣价格(单规格商品直接使用,多规格商品显示最低价)
  14. * - discount: 折扣率(单规格商品直接使用,多规格商品显示最低折扣)
  15. * - discount_amount: 折扣金额(原价-折扣价,仅单规格商品)
  16. * - min_discount_price: 最低折扣价格
  17. * - max_discount_price: 最高折扣价格
  18. * - min_discount: 最低折扣率
  19. * - max_discount: 最高折扣率
  20. * - price_range: 价格区间文本(如:¥99 或 ¥99-199)
  21. */
  22. class DiscountService
  23. {
  24. /**
  25. * 查询当前时间段内的商品折扣信息
  26. * @param array $goodsIds 商品ID数组,为空则查询所有参与折扣的商品
  27. * @param int $limit 限制数量
  28. * @return array
  29. */
  30. public static function getCurrentDiscountGoods($goodsIds = [], $limit = 0)
  31. {
  32. $currentTime = time();
  33. // 第一步:查询当前时间有效的活动ID
  34. $activeActivityIds = Db::table('shop_activity')
  35. ->where('start_time', '<=', $currentTime)
  36. ->where('end_time', '>=', $currentTime)
  37. ->where('activity_status', ActivityEnum::ACTIVITY_STATUS_ONGOING)
  38. ->where('status', StatusEnum::ENABLED)
  39. ->column('id');
  40. if (empty($activeActivityIds)) {
  41. return [];
  42. }
  43. // 第二步:查询这些活动的SKU数据
  44. $query = Db::table('shop_activity_sku')
  45. ->alias('sku')
  46. ->join('shop_goods goods', 'sku.goods_id = goods.id')
  47. ->join('shop_goods_sku spec', 'sku.sku_id = spec.id', 'left')
  48. ->join(['shop_activity'=> 'activity'], 'sku.activity_id = activity.id')
  49. ->where('sku.activity_id', 'in', $activeActivityIds)
  50. ->where('goods.status', GoodsEnum::STATUS_ON_SALE) // 已上架的商品
  51. ->where('sku.stocks', '>', 0) // 有折扣库存的商品
  52. ->field([
  53. 'goods.id as goods_id',
  54. 'goods.title',
  55. 'goods.sub_title',
  56. 'goods.image',
  57. 'goods.price as original_price',
  58. 'goods.market_price',
  59. 'goods.stocks as goods_stocks',
  60. 'goods.sales',
  61. 'goods.spec_type',
  62. 'sku.id as activity_sku_id',
  63. 'sku.sku_id',
  64. 'sku.discount',
  65. 'sku.discount_price',
  66. 'sku.stocks as discount_stocks',
  67. 'sku.sale_quantity',
  68. 'activity.id as activity_id',
  69. 'activity.name as activity_name',
  70. 'activity.start_time',
  71. 'activity.end_time',
  72. 'spec.price as sku_price',
  73. 'spec.image as sku_image',
  74. // 'spec.specs as sku_specs'
  75. ]);
  76. // 如果指定了商品ID,则添加条件
  77. if (!empty($goodsIds)) {
  78. $query->where('sku.goods_id', 'in', $goodsIds);
  79. }
  80. // 如果设置了限制数量
  81. if ($limit > 0) {
  82. $query->limit($limit);
  83. }
  84. // 按折扣力度排序,折扣越大越靠前
  85. $query->order('sku.discount', 'asc');
  86. $result = $query->select();
  87. // 处理返回数据
  88. $processedData = [];
  89. foreach ($result as $item) {
  90. $goodsId = $item['goods_id'];
  91. $skuId = $item['sku_id'];
  92. // 如果商品还没有在结果中,初始化
  93. if (!isset($processedData[$goodsId])) {
  94. $processedData[$goodsId] = [
  95. 'goods_id' => $goodsId,
  96. 'title' => $item['title'],
  97. 'sub_title' => $item['sub_title'],
  98. 'image' => $item['image'],
  99. 'original_price' => $item['original_price'],
  100. 'market_price' => $item['market_price'],
  101. 'goods_stocks' => $item['goods_stocks'],
  102. 'sales' => $item['sales'],
  103. 'spec_type' => $item['spec_type'],
  104. 'activity_id' => $item['activity_id'],
  105. 'activity_name' => $item['activity_name'],
  106. 'start_time' => $item['start_time'],
  107. 'end_time' => $item['end_time'],
  108. 'has_discount' => true,
  109. 'discount_info' => [],
  110. // 初始化折扣价格相关字段
  111. 'min_discount_price' => null,
  112. 'max_discount_price' => null,
  113. 'min_discount' => null,
  114. 'max_discount' => null,
  115. 'discount_price' => null,
  116. 'discount' => null,
  117. 'discount_amount' => null,
  118. 'price_range' => ''
  119. ];
  120. }
  121. // 添加折扣信息
  122. $discountInfo = [
  123. 'activity_sku_id' => $item['activity_sku_id'],
  124. 'sku_id' => $skuId,
  125. 'discount' => $item['discount'],
  126. 'discount_price' => $item['discount_price'],
  127. 'discount_stocks' => $item['discount_stocks'],
  128. 'sale_quantity' => $item['sale_quantity']
  129. ];
  130. // 如果是多规格商品,添加规格信息
  131. if ($skuId > 0) {
  132. $discountInfo['sku_price'] = $item['sku_price'];
  133. $discountInfo['sku_image'] = $item['sku_image'];
  134. // $discountInfo['sku_specs'] = $item['sku_specs'];
  135. }
  136. $processedData[$goodsId]['discount_info'][] = $discountInfo;
  137. }
  138. // 处理外层折扣价格字段
  139. foreach ($processedData as &$goods) {
  140. $discountPrices = [];
  141. $discounts = [];
  142. // 收集所有折扣价格和折扣率
  143. foreach ($goods['discount_info'] as $discount) {
  144. $discountPrices[] = $discount['discount_price'];
  145. $discounts[] = $discount['discount'];
  146. }
  147. if (!empty($discountPrices)) {
  148. // 计算最低和最高折扣价格
  149. $goods['min_discount_price'] = min($discountPrices);
  150. $goods['max_discount_price'] = max($discountPrices);
  151. $goods['min_discount'] = min($discounts);
  152. $goods['max_discount'] = max($discounts);
  153. // 单规格商品处理
  154. if ($goods['spec_type'] == 0) {
  155. $goods['discount_price'] = $goods['min_discount_price'];
  156. $goods['discount'] = $goods['min_discount'];
  157. $goods['discount_amount'] = round($goods['original_price'] - $goods['discount_price'], 2);
  158. $goods['price_range'] = '¥' . $goods['discount_price'];
  159. }
  160. // 多规格商品处理
  161. else {
  162. // 设置价格区间
  163. if ($goods['min_discount_price'] == $goods['max_discount_price']) {
  164. $goods['price_range'] = '¥' . $goods['min_discount_price'];
  165. } else {
  166. $goods['price_range'] = '¥' . $goods['min_discount_price'] . '-' . $goods['max_discount_price'];
  167. }
  168. // 对于多规格,使用最低价格作为主要显示价格
  169. $goods['discount_price'] = $goods['min_discount_price'];
  170. $goods['discount'] = $goods['min_discount'];
  171. }
  172. }
  173. }
  174. return array_values($processedData);
  175. }
  176. /**
  177. * 限时查询一个当前活动的信息和商品信息 活动信息直接加一个商品数组字段
  178. * @param mixed $activityId
  179. * @return array|bool|Model|string|\PDOStatement|null
  180. */
  181. public static function getCurrentActivity()
  182. {
  183. $currentTime = time();
  184. // 查询活动信息
  185. $activity = Db::table('shop_activity')
  186. // ->where('id', $activityId)
  187. ->where('start_time', '<=', $currentTime)
  188. ->where('end_time', '>=', $currentTime)
  189. ->where('activity_status', ActivityEnum::ACTIVITY_STATUS_ONGOING)
  190. ->where('status', StatusEnum::ENABLED)
  191. ->find();
  192. if (!$activity) {
  193. return null;
  194. }
  195. // 查询该活动下的商品信息
  196. $goods = Db::table('shop_activity_sku')
  197. ->alias('sku')
  198. ->join('shop_goods goods', 'sku.goods_id = goods.id')
  199. ->join('shop_goods_sku spec', 'sku.sku_id = spec.id', 'left')
  200. ->where('goods.status', GoodsEnum::STATUS_ON_SALE)
  201. ->where('sku.stocks', '>', 0)
  202. ->field([
  203. 'goods.id as goods_id',
  204. 'goods.title',
  205. 'goods.sub_title',
  206. 'goods.image',
  207. 'goods.price as original_price',
  208. 'goods.market_price',
  209. 'goods.stocks as goods_stocks',
  210. 'goods.sales',
  211. 'goods.spec_type',
  212. 'sku.id as activity_sku_id',
  213. 'sku.sku_id',
  214. 'sku.discount',
  215. 'sku.discount_price',
  216. 'sku.stocks as discount_stocks',
  217. 'sku.sale_quantity',
  218. 'spec.price as sku_price',
  219. 'spec.image as sku_image'
  220. ])
  221. ->order('sku.discount', 'asc')
  222. ->select();
  223. // 处理商品数据
  224. $processedGoods = [];
  225. foreach ($goods as $item) {
  226. $goodsId = $item['goods_id'];
  227. if (!isset($processedGoods[$goodsId])) {
  228. $processedGoods[$goodsId] = [
  229. 'goods_id' => $goodsId,
  230. 'title' => $item['title'],
  231. 'sub_title' => $item['sub_title'],
  232. 'image' => $item['image'],
  233. 'original_price' => $item['original_price'],
  234. 'market_price' => $item['market_price'],
  235. 'goods_stocks' => $item['goods_stocks'],
  236. 'sales' => $item['sales'],
  237. 'spec_type' => $item['spec_type'],
  238. 'has_discount' => true,
  239. 'discount_info' => [],
  240. // 初始化折扣价格相关字段
  241. 'min_discount_price' => null,
  242. 'max_discount_price' => null,
  243. 'min_discount' => null,
  244. 'max_discount' => null,
  245. 'discount_price' => null,
  246. 'discount' => null,
  247. 'discount_amount' => null,
  248. 'price_range' => ''
  249. ];
  250. }
  251. $discountInfo = [
  252. 'activity_sku_id' => $item['activity_sku_id'],
  253. 'sku_id' => $item['sku_id'],
  254. 'discount' => $item['discount'],
  255. 'discount_price' => $item['discount_price'],
  256. 'discount_stocks' => $item['discount_stocks'],
  257. 'sale_quantity' => $item['sale_quantity']
  258. ];
  259. if ($item['sku_id'] > 0) {
  260. $discountInfo['sku_price'] = $item['sku_price'];
  261. $discountInfo['sku_image'] = $item['sku_image'];
  262. }
  263. $processedGoods[$goodsId]['discount_info'][] = $discountInfo;
  264. }
  265. // 处理外层折扣价格字段
  266. foreach ($processedGoods as &$goodsItem) {
  267. $discountPrices = [];
  268. $discounts = [];
  269. // 收集所有折扣价格和折扣率
  270. foreach ($goodsItem['discount_info'] as $discount) {
  271. $discountPrices[] = $discount['discount_price'];
  272. $discounts[] = $discount['discount'];
  273. }
  274. if (!empty($discountPrices)) {
  275. // 计算最低和最高折扣价格
  276. $goodsItem['min_discount_price'] = min($discountPrices);
  277. $goodsItem['max_discount_price'] = max($discountPrices);
  278. $goodsItem['min_discount'] = min($discounts);
  279. $goodsItem['max_discount'] = max($discounts);
  280. // 单规格商品处理
  281. if ($goodsItem['spec_type'] == 0) {
  282. $goodsItem['discount_price'] = $goodsItem['min_discount_price'];
  283. $goodsItem['discount'] = $goodsItem['min_discount'];
  284. $goodsItem['discount_amount'] = round($goodsItem['original_price'] - $goodsItem['discount_price'], 2);
  285. $goodsItem['price_range'] = '¥' . $goodsItem['discount_price'];
  286. }
  287. // 多规格商品处理
  288. else {
  289. // 设置价格区间
  290. if ($goodsItem['min_discount_price'] == $goodsItem['max_discount_price']) {
  291. $goodsItem['price_range'] = '¥' . $goodsItem['min_discount_price'];
  292. } else {
  293. $goodsItem['price_range'] = '¥' . $goodsItem['min_discount_price'] . '-' . $goodsItem['max_discount_price'];
  294. }
  295. // 对于多规格,使用最低价格作为主要显示价格
  296. $goodsItem['discount_price'] = $goodsItem['min_discount_price'];
  297. $goodsItem['discount'] = $goodsItem['min_discount'];
  298. }
  299. }
  300. }
  301. // 在活动信息中添加商品数组字段
  302. $activity['goods'] = array_values($processedGoods);
  303. return $activity;
  304. }
  305. // 限时zhe'kou一个当前活动的信息和商品信息 活动信息直接加一个商品数组字段
  306. public static function getCurrentActivityInfoAndPrize($activityId)
  307. {
  308. $activity = Db::table('shop_activity')->where('id', $activityId)->find();
  309. $prize = Db::table('shop_activity_sku_prize')->where('activity_id', $activityId)->find();
  310. return ['activity' => $activity, 'prize' => $prize];
  311. }
  312. /**
  313. * 根据商品ID和SKU ID获取折扣信息
  314. * @param int $goodsId 商品ID
  315. * @param int $skuId SKU ID,单规格商品传0
  316. * @return array|null
  317. */
  318. public static function getGoodsDiscountInfo($goodsId, $skuId = 0)
  319. {
  320. $currentTime = time();
  321. // 第一步:查询当前时间有效的活动ID
  322. $activeActivityIds = Db::table('shop_activity')
  323. ->where('start_time', '<=', $currentTime)
  324. ->where('end_time', '>=', $currentTime)
  325. ->where('activity_status', ActivityEnum::ACTIVITY_STATUS_ONGOING)
  326. ->where('status', 'normal')
  327. ->column('id');
  328. if (empty($activeActivityIds)) {
  329. return null;
  330. }
  331. // 第二步:查询指定商品的折扣信息
  332. $discountInfo = Db::table('shop_activity_sku_prize')
  333. ->alias('sku')
  334. ->join('shop_activity activity', 'sku.activity_id = activity.id')
  335. ->where('sku.goods_id', $goodsId)
  336. ->where('sku.sku_id', $skuId)
  337. ->where('sku.activity_id', 'in', $activeActivityIds)
  338. ->where('sku.status', 1)
  339. ->where('sku.stocks', '>', 0)
  340. ->field([
  341. 'sku.id as activity_sku_id',
  342. 'sku.discount',
  343. 'sku.discount_price',
  344. 'sku.stocks as discount_stocks',
  345. 'sku.sale_quantity',
  346. 'activity.id as activity_id',
  347. 'activity.name as activity_name',
  348. 'activity.start_time',
  349. 'activity.end_time'
  350. ])
  351. ->find();
  352. return $discountInfo ?: null;
  353. }
  354. /**
  355. * 批量获取商品的折扣信息
  356. * @param array $goodsIds 商品ID数组
  357. * @return array 键为商品ID,值为折扣信息数组
  358. */
  359. public static function getBatchGoodsDiscountInfo($goodsIds)
  360. {
  361. if (empty($goodsIds)) {
  362. return [];
  363. }
  364. $currentTime = time();
  365. // 第一步:查询当前时间有效的活动ID
  366. $activeActivityIds = Db::table('shop_activity')
  367. ->where('start_time', '<=', $currentTime)
  368. ->where('end_time', '>=', $currentTime)
  369. ->where('activity_status', ActivityEnum::ACTIVITY_STATUS_ONGOING)
  370. ->where('status', 'normal')
  371. ->column('id');
  372. if (empty($activeActivityIds)) {
  373. return [];
  374. }
  375. // 第二步:查询指定商品的折扣信息
  376. $result = Db::table('shop_activity_sku_prize')
  377. ->alias('sku')
  378. ->join('shop_activity activity', 'sku.activity_id = activity.id')
  379. ->where('sku.goods_id', 'in', $goodsIds)
  380. ->where('sku.activity_id', 'in', $activeActivityIds)
  381. ->where('sku.status', 1)
  382. ->where('sku.stocks', '>', 0)
  383. ->field([
  384. 'sku.goods_id',
  385. 'sku.sku_id',
  386. 'sku.id as activity_sku_id',
  387. 'sku.discount',
  388. 'sku.discount_price',
  389. 'sku.stocks as discount_stocks',
  390. 'sku.sale_quantity',
  391. 'activity.id as activity_id',
  392. 'activity.name as activity_name'
  393. ])
  394. ->order('sku.discount', 'asc')
  395. ->select();
  396. // 按商品ID分组
  397. $discountData = [];
  398. foreach ($result as $item) {
  399. $goodsId = $item['goods_id'];
  400. if (!isset($discountData[$goodsId])) {
  401. $discountData[$goodsId] = [];
  402. }
  403. $discountData[$goodsId][] = $item;
  404. }
  405. return $discountData;
  406. }
  407. /**
  408. * 检查商品是否参与当前折扣活动
  409. * @param int $goodsId 商品ID
  410. * @param int $skuId SKU ID
  411. * @return bool
  412. */
  413. public static function hasActiveDiscount($goodsId, $skuId = 0)
  414. {
  415. $discountInfo = self::getGoodsDiscountInfo($goodsId, $skuId);
  416. return !empty($discountInfo);
  417. }
  418. /**
  419. * 计算折扣后的价格
  420. * @param float $originalPrice 原价
  421. * @param float $discount 折扣(如9.0表示9折)
  422. * @return float
  423. */
  424. public static function calculateDiscountPrice($originalPrice, $discount)
  425. {
  426. return round($originalPrice * $discount / 10, 2);
  427. }
  428. /**
  429. * 获取活动剩余时间(秒)
  430. * @param int $endTime 活动结束时间戳
  431. * @return int
  432. */
  433. public static function getActivityRemainingTime($endTime)
  434. {
  435. return max(0, $endTime - time());
  436. }
  437. /**
  438. * 格式化活动剩余时间
  439. * @param int $endTime 活动结束时间戳
  440. * @return array
  441. */
  442. public static function formatRemainingTime($endTime)
  443. {
  444. $remaining = self::getActivityRemainingTime($endTime);
  445. if ($remaining <= 0) {
  446. return ['days' => 0, 'hours' => 0, 'minutes' => 0, 'seconds' => 0];
  447. }
  448. $days = floor($remaining / 86400);
  449. $hours = floor(($remaining % 86400) / 3600);
  450. $minutes = floor(($remaining % 3600) / 60);
  451. $seconds = $remaining % 60;
  452. return [
  453. 'days' => $days,
  454. 'hours' => $hours,
  455. 'minutes' => $minutes,
  456. 'seconds' => $seconds
  457. ];
  458. }
  459. }