UserProductService.php 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549
  1. <?php
  2. namespace app\common\service;
  3. use think\facade\Db;
  4. use think\facade\Log;
  5. class UserProductService
  6. {
  7. /**
  8. * 创建用户产品使用记录(支付成功后调用)
  9. */
  10. public function createUserProductRecord($userId, $orderItem, $package)
  11. {
  12. Db::startTrans();
  13. try {
  14. // 1. 首先检查是否已存在相同的记录
  15. $existing = Db::table('ba_user_product_usage')
  16. ->where('order_item_id', $orderItem['id'])
  17. ->where('user_id', $userId)
  18. ->lock(true)
  19. ->find();
  20. if ($existing) {
  21. Db::commit();
  22. Log::info('用户产品记录已存在,跳过创建', [
  23. 'order_item_id' => $orderItem['id'],
  24. 'user_id' => $userId,
  25. 'existing_id' => $existing['id']
  26. ]);
  27. return $existing['id'];
  28. }
  29. // 2. 创建使用记录(原有逻辑)
  30. $usageData = [
  31. 'user_id' => $userId,
  32. 'order_id' => $orderItem['order_id'],
  33. 'order_item_id' => $orderItem['id'],
  34. 'product_id' => $orderItem['product_id'],
  35. 'package_price_id' => $orderItem['package_price_id'],
  36. 'package_type' => $package['type'],
  37. 'card_type' => $package['card_type'],
  38. 'start_time' => date('Y-m-d H:i:s'),
  39. 'status' => 1,
  40. 'create_time' => date('Y-m-d H:i:s'),
  41. 'update_time' => date('Y-m-d H:i:s')
  42. ];
  43. // 3. 根据卡类型和购买数量计算初始值
  44. $quantity = $orderItem['quantity'] ?? 1;
  45. switch ($package['card_type']) {
  46. case 1: // 次卡
  47. $unitValue = $package['unit_value'] ?? 0;
  48. $usageData['remaining_times'] = $unitValue * $quantity;
  49. break;
  50. case 2: // 积分卡
  51. $pointsValue = $package['points_value'] ?? 0;
  52. $usageData['remaining_points'] = $pointsValue * $quantity;
  53. break;
  54. case 3: // 日卡
  55. $usageData['end_time'] = date('Y-m-d 23:59:59', strtotime('+1 day'));
  56. break;
  57. case 4: // 周卡
  58. $usageData['end_time'] = date('Y-m-d 23:59:59', strtotime('+7 days'));
  59. break;
  60. case 5: // 季卡
  61. $usageData['end_time'] = date('Y-m-d 23:59:59', strtotime('+90 days'));
  62. break;
  63. case 6: // 月卡
  64. $usageData['end_time'] = date('Y-m-d 23:59:59', strtotime('+30 days'));
  65. break;
  66. case 7: // 年卡
  67. $usageData['end_time'] = date('Y-m-d 23:59:59', strtotime('+365 days'));
  68. break;
  69. }
  70. $usageId = Db::table('ba_user_product_usage')->insertGetId($usageData);
  71. // 4. 如果是个人套餐,同时更新ba_user_product表
  72. if ($package['type'] == 1) {
  73. $this->updateUserProductTable($userId, $orderItem, $package, $usageData);
  74. }
  75. Db::commit();
  76. Log::info('用户产品记录创建成功', [
  77. 'usage_id' => $usageId,
  78. 'user_id' => $userId,
  79. 'order_item_id' => $orderItem['id']
  80. ]);
  81. return $usageId;
  82. } catch (\Throwable $e) {
  83. Db::rollback();
  84. Log::error('创建用户产品记录失败: ' . $e->getMessage());
  85. throw $e;
  86. }
  87. }
  88. /**
  89. * 更新用户产品表(兼容原有表)
  90. */
  91. private function updateUserProductTable($userId, $orderItem, $package, $usageData)
  92. {
  93. try {
  94. // 检查是否已有记录
  95. $userProduct = Db::table('ba_user_product')
  96. ->where('user_id', $userId)
  97. ->where('product_id', $orderItem['product_id'])
  98. ->where('package_price_id', $orderItem['package_price_id'])
  99. ->find();
  100. if ($userProduct) {
  101. // 更新现有记录
  102. $updateData = [
  103. 'update_time' => date('Y-m-d H:i:s')
  104. ];
  105. // 根据卡类型累加
  106. if ($package['card_type'] == 1) { // 次卡
  107. $updateData['remaining_times'] = Db::raw('remaining_times + ' . $usageData['remaining_times']);
  108. } elseif ($package['card_type'] == 2) { // 积分卡
  109. $updateData['remaining_points'] = Db::raw('remaining_points + ' . $usageData['remaining_points']);
  110. } elseif ($package['card_type'] >= 3) { // 日期卡
  111. // 日期卡取最新的结束时间
  112. if (empty($userProduct['expire_time']) ||
  113. strtotime($usageData['end_time']) > strtotime($userProduct['expire_time'])) {
  114. $updateData['expire_time'] = $usageData['end_time'];
  115. }
  116. }
  117. Db::table('ba_user_product')
  118. ->where('id', $userProduct['id'])
  119. ->update($updateData);
  120. } else {
  121. // 创建新记录
  122. $newData = [
  123. 'user_id' => $userId,
  124. 'product_id' => $orderItem['product_id'],
  125. 'package_price_id' => $orderItem['package_price_id'],
  126. 'remaining_points' => $usageData['remaining_points'] ?? 0,
  127. 'remaining_times' => $usageData['remaining_times'] ?? 0,
  128. 'expire_time' => $usageData['end_time'] ?? null,
  129. 'create_time' => date('Y-m-d H:i:s')
  130. ];
  131. Db::table('ba_user_product')->insert($newData);
  132. }
  133. } catch (\Throwable $e) {
  134. Log::error('更新用户产品表失败: ' . $e->getMessage());
  135. // 这里不抛出异常,因为这是辅助功能,主功能不受影响
  136. }
  137. }
  138. /**
  139. * 企业套餐分配时创建用户产品记录
  140. */
  141. public function createCompanyProductUsage($allocationRecord, $assignedToUserId,$card_type)
  142. {
  143. try {
  144. // 从分配记录创建用户产品使用记录
  145. $usageData = [
  146. 'user_id' => $assignedToUserId,
  147. 'order_id' => $allocationRecord['order_id'],
  148. 'order_item_id' => $allocationRecord['order_item_id'],
  149. 'product_id' => $allocationRecord['product_id'],
  150. 'package_price_id' => $allocationRecord['package_price_id'],
  151. 'package_type' => 2, // 企业套餐
  152. 'card_type' => $card_type,
  153. 'company_id' => $allocationRecord['company_id'],
  154. 'allocation_id' => $allocationRecord['id'],
  155. 'is_allocated' => 1,
  156. 'start_time' => date('Y-m-d H:i:s'),
  157. 'status' => 1,
  158. 'create_time' => date('Y-m-d H:i:s'),
  159. 'update_time' => date('Y-m-d H:i:s')
  160. ];
  161. // 设置有效期和剩余量
  162. if ($card_type >= 3) { // 日期卡
  163. $usageData['end_time'] = $this->calculateEndTime($card_type);
  164. } elseif ($card_type == 1) { // 次卡
  165. $usageData['remaining_times'] = $allocationRecord['remaining_times'];
  166. } elseif ($card_type == 2) { // 积分卡
  167. $usageData['remaining_points'] = $allocationRecord['remaining_points'];
  168. }
  169. return Db::table('ba_user_product_usage')->insertGetId($usageData);
  170. } catch (\Throwable $e) {
  171. Log::error('创建企业产品使用记录失败: ' . $e->getMessage());
  172. throw $e;
  173. }
  174. }
  175. /**
  176. * 获取我的产品列表(整合个人套餐和企业套餐)
  177. */
  178. public function getUserProducts($userId)
  179. {
  180. try {
  181. // 1. 查询个人套餐(直接从使用记录表)
  182. $personalProducts = Db::table('ba_user_product_usage')
  183. ->alias('upu')
  184. ->leftJoin('ba_product p', 'p.id = upu.product_id')
  185. ->leftJoin('ba_package_price pp', 'pp.id = upu.package_price_id')
  186. ->where('upu.user_id', $userId)
  187. ->where('upu.package_type', 1) // 个人套餐
  188. // ->where('upu.status', 1) // 有效
  189. ->field('upu.*,
  190. p.name as product_name, p.type as product_type, p.link,
  191. pp.spec_name, pp.type as package_type, pp.card_type,pp.image')
  192. ->select()
  193. ->toArray();
  194. // 2. 查询已分配给用户的企业套餐
  195. $companyProducts = Db::table('ba_user_product_usage')
  196. ->alias('upu')
  197. ->leftJoin('ba_product p', 'p.id = upu.product_id')
  198. ->leftJoin('ba_package_price pp', 'pp.id = upu.package_price_id')
  199. ->leftJoin('ba_company_package_allocation cpa', 'cpa.id = upu.allocation_id')
  200. ->where('upu.user_id', $userId)
  201. ->where('upu.package_type', 2) // 企业套餐
  202. // ->where('upu.status', 1) // 有效
  203. ->where('upu.is_allocated', 1) // 已分配
  204. ->field('upu.*,
  205. p.name as product_name, p.type as product_type, p.link,
  206. pp.spec_name, pp.type as package_type, pp.card_type,
  207. cpa.assigned_by_user_id,pp.image')
  208. ->select()
  209. ->toArray();
  210. // 3. 合并结果
  211. $allProducts = array_merge($personalProducts, $companyProducts);
  212. // 4. 按产品ID和套餐规格ID分组,合并相同产品(只合并次卡和积分卡)
  213. $groupedProducts = [];
  214. foreach ($allProducts as $product) {
  215. $key = $product['product_id'] . '_' . $product['package_price_id'];
  216. // 日期卡不合并,每次分配单独显示
  217. if ($product['card_type'] >= 3) {
  218. $groupedProducts[] = $this->formatProductItem($product, false);
  219. continue;
  220. }
  221. // 次卡和积分卡合并
  222. if (!isset($groupedProducts[$key])) {
  223. $groupedProducts[$key] = $this->formatProductItem($product, true);
  224. } else {
  225. // 累加剩余次数/积分
  226. $groupedProducts[$key]['remaining_times'] += $product['remaining_times'];
  227. $groupedProducts[$key]['remaining_points'] += $product['remaining_points'];
  228. $groupedProducts[$key]['total_quantity'] += 1;
  229. }
  230. }
  231. // 5. 处理合并后的数组
  232. $result = [];
  233. foreach ($groupedProducts as $product) {
  234. $result[] = $product;
  235. }
  236. return $result;
  237. } catch (\Throwable $e) {
  238. Log::error('获取用户产品列表失败: ' . $e->getMessage());
  239. return [];
  240. }
  241. }
  242. /**
  243. * 用户使用产品(根据产品ID自动匹配可用套餐,优先级:日期卡 > 次卡 > 积分卡)
  244. * @param int $userId
  245. * @param int $productId
  246. * @return bool
  247. * @throws \Exception
  248. */
  249. public function useProductByProductId($userId, $productId)
  250. {
  251. Db::startTrans();
  252. try {
  253. // 1. 获取该产品关联的所有套餐规格ID
  254. $packagePriceIds = Db::table('ba_product')
  255. ->alias('p')
  256. ->leftJoin('ba_package_price pp', 'pp.package_id = p.package_id')
  257. ->where('p.id', $productId)
  258. ->where('p.status', 1)
  259. ->column('pp.id');
  260. if (empty($packagePriceIds)) {
  261. throw new \Exception('产品未配置套餐');
  262. }
  263. $now = date('Y-m-d H:i:s');
  264. // 2. 查询用户所有可能有效的使用记录(加锁防止并发)
  265. $usageList = Db::table('ba_user_product_usage')
  266. ->where('user_id', $userId)
  267. ->whereIn('package_price_id', $packagePriceIds)
  268. ->where('status', 1) // 有效
  269. ->lock(true)
  270. ->select()
  271. ->toArray();
  272. if (empty($usageList)) {
  273. throw new \Exception('没有可用的套餐');
  274. }
  275. // 3. 批量获取所有套餐规格的 cost_value 映射(用于积分卡)
  276. $priceIds = array_column($usageList, 'package_price_id');
  277. $costMap = Db::table('ba_package_price')
  278. ->whereIn('id', $priceIds)
  279. ->column('cost_value', 'id');
  280. // 4. 按卡类型分类并过滤出真正可用的记录
  281. $dateCardUsages = []; // 日期卡 (card_type >= 3)
  282. $timesCardUsages = []; // 次卡 (card_type = 1)
  283. $pointsCardUsages = []; // 积分卡 (card_type = 2)
  284. foreach ($usageList as $usage) {
  285. $cardType = $usage['card_type'];
  286. if ($cardType >= 3) { // 日期卡
  287. if (empty($usage['end_time']) || $usage['end_time'] > $now) {
  288. $dateCardUsages[] = $usage;
  289. }
  290. } elseif ($cardType == 1) { // 次卡
  291. if ($usage['remaining_times'] > 0) {
  292. $timesCardUsages[] = $usage;
  293. }
  294. } elseif ($cardType == 2) { // 积分卡
  295. $cost = $costMap[$usage['package_price_id']] ?? 0;
  296. if ($cost > 0 && $usage['remaining_points'] >= $cost) {
  297. $pointsCardUsages[] = $usage;
  298. }
  299. }
  300. }
  301. // 5. 按优先级选择一条记录
  302. $selectedUsage = null;
  303. $consumeValue = 0; // 积分卡本次消耗值
  304. if (!empty($dateCardUsages)) {
  305. // 日期卡按到期时间升序,优先使用即将到期的
  306. usort($dateCardUsages, function ($a, $b) {
  307. $aEnd = $a['end_time'] ?? '9999-12-31 23:59:59';
  308. $bEnd = $b['end_time'] ?? '9999-12-31 23:59:59';
  309. return strcmp($aEnd, $bEnd);
  310. });
  311. $selectedUsage = $dateCardUsages[0];
  312. } elseif (!empty($timesCardUsages)) {
  313. // 次卡按剩余次数升序,优先使用剩余少的
  314. usort($timesCardUsages, function ($a, $b) {
  315. return $a['remaining_times'] <=> $b['remaining_times'];
  316. });
  317. $selectedUsage = $timesCardUsages[0];
  318. } elseif (!empty($pointsCardUsages)) {
  319. // 积分卡按剩余积分升序,优先使用剩余少的
  320. usort($pointsCardUsages, function ($a, $b) {
  321. return $a['remaining_points'] <=> $b['remaining_points'];
  322. });
  323. $selectedUsage = $pointsCardUsages[0];
  324. $consumeValue = $costMap[$selectedUsage['package_price_id']];
  325. } else {
  326. throw new \Exception('没有可用的套餐');
  327. }
  328. // 6. 执行扣减操作
  329. $updateData = [
  330. 'last_use_time' => $now,
  331. 'update_time' => $now
  332. ];
  333. $cardType = $selectedUsage['card_type'];
  334. switch ($cardType) {
  335. case 1: // 次卡
  336. $updateData['remaining_times'] = Db::raw('remaining_times - 1');
  337. if ($selectedUsage['remaining_times'] - 1 <= 0) {
  338. $updateData['status'] = 3; // 已用完
  339. }
  340. break;
  341. case 2: // 积分卡
  342. $updateData['remaining_points'] = Db::raw('remaining_points - ' . $consumeValue);
  343. if ($selectedUsage['remaining_points'] - $consumeValue <= 0) {
  344. $updateData['status'] = 3; // 已用完
  345. }
  346. break;
  347. // 日期卡无需扣减,仅记录使用时间
  348. }
  349. Db::table('ba_user_product_usage')
  350. ->where('id', $selectedUsage['id'])
  351. ->update($updateData);
  352. // 7. 记录使用明细日志(可选)
  353. $this->logUsage($userId, $selectedUsage['id'], $productId, $consumeValue, $selectedUsage);
  354. Db::commit();
  355. return true;
  356. } catch (\Throwable $e) {
  357. Db::rollback();
  358. Log::error('用户使用产品失败:' . $e->getMessage(), [
  359. 'user_id' => $userId,
  360. 'product_id' => $productId
  361. ]);
  362. throw $e;
  363. }
  364. }
  365. /**
  366. * 记录使用明细日志
  367. */
  368. private function logUsage($userId, $usageId, $productId, $consumeValue, $usage)
  369. {
  370. $logData = [
  371. 'user_id' => $userId,
  372. 'usage_id' => $usageId,
  373. 'product_id' => $productId,
  374. 'consume_type' => $usage['card_type'] == 1 ? 1 : 2, // 1=次数,2=积分
  375. 'consume_value' => $consumeValue,
  376. 'before_value' => $usage['card_type'] == 1 ? $usage['remaining_times'] : $usage['remaining_points'],
  377. 'after_value' => $usage['card_type'] == 1 ? $usage['remaining_times'] - 1 : $usage['remaining_points'] - $consumeValue,
  378. 'create_time' => date('Y-m-d H:i:s')
  379. ];
  380. Db::table('ba_user_usage_log')->insert($logData);
  381. }
  382. /**
  383. * 格式化产品项
  384. */
  385. private function formatProductItem($product, $isMerged = false)
  386. {
  387. $item = [
  388. 'product_id' => $product['product_id'],
  389. 'product_name' => $product['product_name'],
  390. 'product_type' => $product['product_type'] ?? 1, // 默认1:网页
  391. 'product_link' => $product['link'] ?? '',
  392. 'image' => $product['image'] ?? '',
  393. 'package_spec' => $product['spec_name'],
  394. 'package_type' => $product['package_type'],
  395. 'package_type_text' => $product['package_type'] == 1 ? '个人套餐' : '企业套餐',
  396. 'card_type' => $product['card_type'],
  397. 'card_type_text' => $this->getCardTypeText($product['card_type']),
  398. 'total_quantity' => $isMerged ? 1 : 1,
  399. 'effective_end_time' => $product['end_time'] ?? null,
  400. 'remaining_times' => $product['remaining_times'] ?? 0,
  401. 'remaining_points' => $product['remaining_points'] ?? 0,
  402. 'display_info' => $this->getDisplayInfo($product),
  403. 'usage_id' => $product['id'],
  404. 'start_time' => $product['start_time'] ?? null,
  405. 'last_use_time' => $product['last_use_time'] ?? null,
  406. 'status' => $product['status'] ?? 1,
  407. 'buy_time' => $product['create_time']
  408. ];
  409. // 企业套餐特有字段
  410. if ($product['package_type'] == 2) {
  411. $item['is_allocated'] = $product['is_allocated'] ?? 0;
  412. $item['company_id'] = $product['company_id'] ?? null;
  413. $item['allocation_id'] = $product['allocation_id'] ?? null;
  414. // 获取企业套餐的席位信息(如果有)
  415. if ($item['company_id']) {
  416. $seatsInfo = Db::name('company_package_seats')
  417. ->where('company_id', $item['company_id'])
  418. ->where('package_price_id', $product['package_price_id'])
  419. ->find();
  420. if ($seatsInfo) {
  421. $item['total_seats'] = $seatsInfo['total_seats'];
  422. $item['assigned_seats'] = $seatsInfo['assigned_seats'];
  423. $item['available_seats'] = $seatsInfo['available_seats'];
  424. } else {
  425. $item['total_seats'] = 0;
  426. $item['assigned_seats'] = 0;
  427. $item['available_seats'] = 0;
  428. }
  429. } else {
  430. $item['total_seats'] = null;
  431. $item['assigned_seats'] = null;
  432. $item['available_seats'] = null;
  433. }
  434. } else {
  435. // 个人套餐没有席位信息
  436. $item['is_allocated'] = 0;
  437. $item['company_id'] = null;
  438. $item['allocation_id'] = null;
  439. $item['total_seats'] = null;
  440. $item['assigned_seats'] = null;
  441. $item['available_seats'] = null;
  442. }
  443. return $item;
  444. }
  445. /**
  446. * 获取显示信息
  447. */
  448. private function getDisplayInfo($product)
  449. {
  450. if ($product['card_type'] >= 3) { // 日期卡
  451. $endTime = $product['end_time'] ?? null;
  452. return $endTime ? '到期: ' . date('Y-m-d', strtotime($endTime)) : '永久有效';
  453. } elseif ($product['card_type'] == 1) { // 次卡
  454. $remainingTimes = $product['remaining_times'] ?? 0;
  455. return '剩余次数: ' . $remainingTimes;
  456. } elseif ($product['card_type'] == 2) { // 积分卡
  457. $remainingPoints = $product['remaining_points'] ?? 0;
  458. return '剩余积分: ' . $remainingPoints;
  459. }
  460. return '';
  461. }
  462. /**
  463. * 获取卡类型文本
  464. */
  465. private function getCardTypeText($type)
  466. {
  467. $map = [
  468. 1 => '次卡',
  469. 2 => '积分卡',
  470. 3 => '日卡',
  471. 4 => '周卡',
  472. 5 => '季卡',
  473. 6 => '月卡',
  474. 7 => '年卡'
  475. ];
  476. return $map[$type] ?? '其他';
  477. }
  478. /**
  479. * 计算到期时间
  480. */
  481. private function calculateEndTime($cardType)
  482. {
  483. $start = time();
  484. switch ($cardType) {
  485. case 3: // 日卡
  486. return date('Y-m-d 23:59:59', strtotime('+1 day', $start));
  487. case 4: // 周卡
  488. return date('Y-m-d 23:59:59', strtotime('+7 days', $start));
  489. case 5: // 季卡
  490. return date('Y-m-d 23:59:59', strtotime('+90 days', $start));
  491. case 6: // 月卡
  492. return date('Y-m-d 23:59:59', strtotime('+30 days', $start));
  493. case 7: // 年卡
  494. return date('Y-m-d 23:59:59', strtotime('+365 days', $start));
  495. default:
  496. return null;
  497. }
  498. }
  499. }