Package.php 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752
  1. <?php
  2. /**
  3. * 套餐管理控制器
  4. */
  5. namespace app\admin\controller\package;
  6. use app\common\controller\Backend;
  7. use think\facade\Db;
  8. use think\facade\Validate;
  9. class Package extends Backend
  10. {
  11. /**
  12. * 套餐列表
  13. * @return void
  14. */
  15. public function index():void
  16. {
  17. $where = [];
  18. // 套餐名称搜索
  19. if (!empty($this->request->get('name'))) {
  20. $name = $this->request->get('name');
  21. $where[] = ['p.name', 'like', '%' . $name . '%'];
  22. }
  23. // 套餐类型筛选
  24. if (!empty($this->request->get('type'))) {
  25. $type = $this->request->get('type');
  26. $where['p.type'] = $type;
  27. }
  28. // 状态筛选
  29. if (!empty($this->request->get('status'))) {
  30. $status = $this->request->get('status');
  31. $where['p.status'] = $status;
  32. }
  33. // 排序
  34. $order = [];
  35. if (!empty($this->request->get('order'))) {
  36. $order_a = $this->request->get('order');
  37. $order_a = explode(',', $order_a);
  38. $order[$order_a[0]] = $order_a[1];
  39. } else {
  40. $order['sort'] = 'asc';
  41. $order['id'] = 'desc';
  42. }
  43. // 分页
  44. $page = $this->request->get('page', 1);
  45. $limit = $this->request->get('limit', 20);
  46. // 查询套餐列表并计算库存状态
  47. $packageList = Db::name('package')
  48. ->where($where)
  49. ->order($order)
  50. ->paginate([
  51. 'list_rows' => $limit,
  52. 'page' => $page
  53. ]);
  54. // 为每个套餐计算库存状态
  55. $list = $packageList->items();
  56. foreach ($list as &$package) {
  57. // 获取该套餐的所有规格
  58. $specs = Db::name('package_price')
  59. ->where('package_id', $package['id'])
  60. ->select()
  61. ->toArray();
  62. // 计算总库存和总已售
  63. $totalStock = 0;
  64. $totalSold = 0;
  65. foreach ($specs as $spec) {
  66. $totalStock += $spec['stock'];
  67. $totalSold += $spec['sold_count'];
  68. }
  69. // 根据库存设置库存状态
  70. if ($totalStock == 0) {
  71. $package['stock_status'] = '售罄';
  72. } elseif ($totalStock <= 10) {
  73. $package['stock_status'] = '即将售罄';
  74. } else {
  75. $package['stock_status'] = '充足';
  76. }
  77. $package['remaining_stock'] = $totalStock;
  78. $package['sold_count'] = $totalSold;
  79. $package['spec_count'] = count($specs);
  80. }
  81. $this->success('查询成功', [
  82. 'list' => $list,
  83. 'total' => $packageList->total()
  84. ]);
  85. }
  86. /**
  87. * 新增/编辑套餐
  88. * @return void
  89. */
  90. public function edit():void
  91. {
  92. $id = $this->request->post('id');
  93. $name = $this->request->post('name');
  94. $description = $this->request->post('description', '');
  95. $sort = $this->request->post('sort', 0);
  96. $status = $this->request->post('status', 1);
  97. if (empty($name)) {
  98. $this->error(__('套餐名称不能为空'));
  99. }
  100. // 验证套餐名称是否已存在(编辑时排除自己)
  101. $checkName = Db::name('package')
  102. ->where('name', $name);
  103. if (!empty($id)) {
  104. $checkName->where('id', '<>', $id);
  105. }
  106. if ($checkName->find()) {
  107. $this->error(__('套餐名称已存在'));
  108. }
  109. $data = [
  110. 'name' => $name,
  111. 'description' => $description,
  112. 'sort' => $sort,
  113. 'status' => $status,
  114. 'update_time' => date('Y-m-d H:i:s')
  115. ];
  116. try {
  117. if (!empty($id)) {
  118. // 编辑
  119. $packageInfo = Db::name('package')
  120. ->where('id', $id)
  121. ->find();
  122. if (!$packageInfo) {
  123. $this->error(__('套餐不存在'));
  124. }
  125. $result = Db::name('package')
  126. ->where('id', $id)
  127. ->update($data);
  128. $message = '编辑成功';
  129. } else {
  130. // 新增
  131. $data['create_time'] = date('Y-m-d H:i:s');
  132. $result = Db::name('package')
  133. ->insert($data);
  134. $id = Db::name('package')->getLastInsID();
  135. $message = '新增成功';
  136. }
  137. } catch (\Exception $e) {
  138. $this->error('操作失败: ' . $e->getMessage());
  139. }
  140. if ($result) {
  141. $this->success($message, ['id' => $id]);
  142. } else {
  143. $this->error(__('操作失败'));
  144. }
  145. }
  146. /**
  147. * 删除套餐
  148. * @return void
  149. */
  150. public function delete():void
  151. {
  152. $id = $this->request->post('id');
  153. if (empty($id)) {
  154. $this->error(__('参数错误'));
  155. }
  156. // 检查是否有规格存在
  157. $hasSpec = Db::name('package_price')
  158. ->where('package_id', $id)
  159. ->count();
  160. if ($hasSpec > 0) {
  161. $this->error(__('该套餐下有关联的规格,请先删除规格'));
  162. }
  163. // 检查是否有用户购买记录(如果user_package表存在)
  164. try {
  165. $hasUserPackage = Db::name('user_package')
  166. ->where('package_id', $id)
  167. ->count();
  168. if ($hasUserPackage > 0) {
  169. $this->error(__('该套餐已有用户购买记录,不能删除'));
  170. }
  171. } catch (\Exception $e) {
  172. // 表不存在,跳过检查
  173. }
  174. // 检查是否有企业购买记录(如果company_package表存在)
  175. try {
  176. $hasCompanyPackage = Db::name('company_package')
  177. ->where('package_id', $id)
  178. ->count();
  179. if ($hasCompanyPackage > 0) {
  180. $this->error(__('该套餐已有企业购买记录,不能删除'));
  181. }
  182. } catch (\Exception $e) {
  183. // 表不存在,跳过检查
  184. }
  185. try {
  186. $result = Db::name('package')
  187. ->where('id', $id)
  188. ->delete();
  189. } catch (\Exception $e) {
  190. $this->error('删除失败: ' . $e->getMessage());
  191. }
  192. if ($result) {
  193. $this->success('删除成功');
  194. } else {
  195. $this->error(__('删除失败'));
  196. }
  197. }
  198. /**
  199. * 套餐详情
  200. * @return void
  201. */
  202. public function detail():void
  203. {
  204. $id = $this->request->get('id');
  205. if (empty($id)) {
  206. $this->error(__('参数错误'));
  207. }
  208. // 获取套餐基础信息
  209. $packageInfo = Db::name('package')
  210. ->where('id', $id)
  211. ->find();
  212. if (!$packageInfo) {
  213. $this->error(__('套餐不存在'));
  214. }
  215. // 获取套餐规格列表
  216. $specList = Db::name('package_price')
  217. ->where('package_id', $id)
  218. ->order('sort', 'asc')
  219. ->select()
  220. ->toArray();
  221. // 计算规格库存状态和汇总数据
  222. $totalStock = 0;
  223. $totalSold = 0;
  224. $cardTypeMap = [
  225. 1 => '次卡',
  226. 2 => '按积分',
  227. 3 => '日卡',
  228. 4 => '周卡',
  229. 5 => '季卡',
  230. 6 => '月卡',
  231. 7 => '年卡'
  232. ];
  233. $typeMap = [
  234. 1 => '个人套餐',
  235. 2 => '团队套餐',
  236. ];
  237. foreach ($specList as &$spec) {
  238. // 计算库存状态
  239. if ($spec['stock'] == 0) {
  240. $spec['stock_status'] = '售罄';
  241. } elseif ($spec['stock'] <= 10) {
  242. $spec['stock_status'] = '即将售罄';
  243. } else {
  244. $spec['stock_status'] = '充足';
  245. }
  246. // 翻译类型和卡类型
  247. $spec['type_text'] = $typeMap[$spec['type']] ?? '未知';
  248. $spec['card_type_text'] = $cardTypeMap[$spec['card_type']] ?? '未知';
  249. // 格式化收费方式显示
  250. if ($spec['card_type'] == 1 && $spec['unit_value'] > 0) {
  251. $spec['charge_text'] = '按次数:' . $spec['unit_value'] . '次';
  252. } elseif ($spec['card_type'] == 2 && $spec['points_value'] > 0) {
  253. $spec['charge_text'] = '按积分:' . $spec['points_value'] . '积分';
  254. } else {
  255. $spec['charge_text'] = $spec['card_type_text'];
  256. }
  257. $totalStock += $spec['stock'];
  258. $totalSold += $spec['sold_count'];
  259. }
  260. $result = [
  261. 'package_info' => $packageInfo,
  262. 'spec_list' => $specList,
  263. 'statistics' => [
  264. 'total_stock' => $totalStock,
  265. 'total_sold' => $totalSold,
  266. 'spec_count' => count($specList)
  267. ]
  268. ];
  269. $this->success('查询成功', $result);
  270. }
  271. /**
  272. * 添加/编辑套餐规格
  273. * @return void
  274. */
  275. public function editSpec():void
  276. {
  277. $id = $this->request->post('id');
  278. $packageId = $this->request->post('package_id');
  279. $specName = $this->request->post('spec_name');
  280. $price = $this->request->post('price', 0);
  281. $stock = $this->request->post('stock', 0);
  282. $type = $this->request->post('type', 1);
  283. $cardType = $this->request->post('card_type', 1);
  284. $unitValue = $this->request->post('unit_value', 0);
  285. $pointsValue = $this->request->post('points_value', 0);
  286. $description = $this->request->post('description', '');
  287. $sort = $this->request->post('sort', 0);
  288. if (empty($packageId) || empty($specName)) {
  289. $this->error(__('参数不完整'));
  290. }
  291. if ($price <= 0) {
  292. $this->error(__('价格必须大于0'));
  293. }
  294. // 验证条件
  295. if ($cardType == 1 && $unitValue <= 0) {
  296. $this->error(__('次卡类型必须填写有效次数'));
  297. }
  298. if ($cardType == 2 && $pointsValue <= 0) {
  299. $this->error(__('积分类型必须填写有效积分数量'));
  300. }
  301. // 检查套餐是否存在
  302. $packageInfo = Db::name('package')
  303. ->where('id', $packageId)
  304. ->find();
  305. if (!$packageInfo) {
  306. $this->error(__('套餐不存在'));
  307. }
  308. // 验证规格名称是否已存在(同一套餐下不能重复)
  309. $checkSpec = Db::name('package_price')
  310. ->where('package_id', $packageId)
  311. ->where('spec_name', $specName);
  312. if (!empty($id)) {
  313. $checkSpec->where('id', '<>', $id);
  314. }
  315. if ($checkSpec->find()) {
  316. $this->error(__('同一套餐下规格名称不能重复'));
  317. }
  318. $data = [
  319. 'package_id' => $packageId,
  320. 'spec_name' => $specName,
  321. 'price' => $price,
  322. 'stock' => $stock,
  323. 'type' => $type,
  324. 'card_type' => $cardType,
  325. 'unit_value' => $unitValue,
  326. 'points_value' => $pointsValue,
  327. 'description' => $description,
  328. 'sort' => $sort,
  329. 'update_time' => date('Y-m-d H:i:s')
  330. ];
  331. try {
  332. if (!empty($id)) {
  333. // 编辑
  334. $result = Db::name('package_price')
  335. ->where('id', $id)
  336. ->update($data);
  337. $message = '编辑成功';
  338. } else {
  339. // 新增
  340. $data['create_time'] = date('Y-m-d H:i:s');
  341. $result = Db::name('package_price')
  342. ->insert($data);
  343. $id = Db::name('package_price')->getLastInsID();
  344. $message = '新增成功';
  345. }
  346. } catch (\Exception $e) {
  347. $this->error('操作失败: ' . $e->getMessage());
  348. }
  349. if ($result) {
  350. $this->success($message, ['id' => $id]);
  351. } else {
  352. $this->error(__('操作失败'));
  353. }
  354. }
  355. /**
  356. * 删除套餐规格
  357. * @return void
  358. */
  359. public function deleteSpec():void
  360. {
  361. $id = $this->request->post('id');
  362. if (empty($id)) {
  363. $this->error(__('参数错误'));
  364. }
  365. // 检查是否有用户购买记录
  366. try {
  367. $hasPurchase = Db::name('user_package')
  368. ->where('package_price_id', $id)
  369. ->count();
  370. if ($hasPurchase > 0) {
  371. $this->error(__('该规格已有用户购买记录,不能删除'));
  372. }
  373. } catch (\Exception $e) {
  374. // 表不存在,跳过检查
  375. }
  376. // 检查是否有企业购买记录
  377. try {
  378. $hasPurchase = Db::name('company_package')
  379. ->where('package_price_id', $id)
  380. ->count();
  381. if ($hasPurchase > 0) {
  382. $this->error(__('该规格已有企业购买记录,不能删除'));
  383. }
  384. } catch (\Exception $e) {
  385. // 表不存在,跳过检查
  386. }
  387. try {
  388. $result = Db::name('package_price')
  389. ->where('id', $id)
  390. ->delete();
  391. } catch (\Exception $e) {
  392. $this->error('删除失败: ' . $e->getMessage());
  393. }
  394. if ($result) {
  395. $this->success('删除成功');
  396. } else {
  397. $this->error(__('删除失败'));
  398. }
  399. }
  400. /**
  401. * 获取套餐规格列表
  402. * @return void
  403. */
  404. public function specList():void
  405. {
  406. $packageId = $this->request->get('package_id');
  407. if (empty($packageId)) {
  408. $this->error(__('参数错误'));
  409. }
  410. $specList = Db::name('package_price')
  411. ->where('package_id', $packageId)
  412. ->order('sort', 'asc')
  413. ->select()
  414. ->toArray();
  415. $this->success('查询成功', $specList);
  416. }
  417. /**
  418. * 批量更新套餐状态
  419. * @return void
  420. */
  421. public function batchStatus():void
  422. {
  423. $ids = $this->request->post('ids');
  424. $status = $this->request->post('status');
  425. if (empty($ids) || empty($status)) {
  426. $this->error(__('参数错误'));
  427. }
  428. if (!is_array($ids)) {
  429. $ids = explode(',', $ids);
  430. }
  431. try {
  432. $result = Db::name('package')
  433. ->whereIn('id', $ids)
  434. ->update([
  435. 'status' => $status,
  436. 'update_time' => date('Y-m-d H:i:s')
  437. ]);
  438. } catch (\Exception $e) {
  439. $this->error('操作失败: ' . $e->getMessage());
  440. }
  441. if ($result) {
  442. $statusText = $status == 1 ? '启用' : '禁用';
  443. $this->success("成功{$statusText}{$result}个套餐");
  444. } else {
  445. $this->error(__('操作失败'));
  446. }
  447. }
  448. /**
  449. * 批量更新规格库存
  450. * @return void
  451. */
  452. public function batchUpdateStock():void
  453. {
  454. $ids = $this->request->post('ids');
  455. $stock = $this->request->post('stock');
  456. if (empty($ids) || $stock === null) {
  457. $this->error(__('参数错误'));
  458. }
  459. if (!is_array($ids)) {
  460. $ids = explode(',', $ids);
  461. }
  462. try {
  463. $result = Db::name('package_price')
  464. ->whereIn('id', $ids)
  465. ->update([
  466. 'stock' => $stock,
  467. 'update_time' => date('Y-m-d H:i:s')
  468. ]);
  469. } catch (\Exception $e) {
  470. $this->error('操作失败: ' . $e->getMessage());
  471. }
  472. if ($result) {
  473. $this->success("成功更新{$result}个规格的库存");
  474. } else {
  475. $this->error(__('操作失败'));
  476. }
  477. }
  478. /**
  479. * 获取套餐类型选项
  480. * @return void
  481. */
  482. public function typeOptions():void
  483. {
  484. $options = [
  485. ['value' => 1, 'label' => '个人套餐'],
  486. ['value' => 2, 'label' => '团队套餐'],
  487. ['value' => 3, 'label' => '企业套餐']
  488. ];
  489. $this->success('获取成功', $options);
  490. }
  491. /**
  492. * 获取套餐状态选项
  493. * @return void
  494. */
  495. public function statusOptions():void
  496. {
  497. $options = [
  498. ['value' => 1, 'label' => '启用'],
  499. ['value' => 2, 'label' => '禁用']
  500. ];
  501. $this->success('获取成功', $options);
  502. }
  503. /**
  504. * 获取卡类型选项
  505. * @return void
  506. */
  507. public function cardTypeOptions():void
  508. {
  509. $options = [
  510. ['value' => 1, 'label' => '次卡'],
  511. ['value' => 2, 'label' => '按积分'],
  512. ['value' => 3, 'label' => '日卡'],
  513. ['value' => 4, 'label' => '周卡'],
  514. ['value' => 5, 'label' => '季卡'],
  515. ['value' => 6, 'label' => '月卡'],
  516. ['value' => 7, 'label' => '年卡']
  517. ];
  518. $this->success('获取成功', $options);
  519. }
  520. /**
  521. * 套餐统计信息
  522. * @return void
  523. */
  524. public function statistics():void
  525. {
  526. // 套餐总数
  527. $totalPackage = Db::name('package')->count();
  528. // 启用套餐数
  529. $activePackage = Db::name('package')
  530. ->where('status', 1)
  531. ->count();
  532. // 规格总数
  533. $totalSpec = Db::name('package_price')->count();
  534. // 总库存
  535. $totalStock = Db::name('package_price')
  536. ->sum('stock');
  537. // 总已售
  538. $totalSold = Db::name('package_price')
  539. ->sum('sold_count');
  540. $stats = [
  541. 'total_package' => $totalPackage,
  542. 'active_package' => $activePackage,
  543. 'total_spec' => $totalSpec,
  544. 'total_stock' => $totalStock,
  545. 'total_sold' => $totalSold
  546. ];
  547. $this->success('获取成功', $stats);
  548. }
  549. /**
  550. * 获取所有套餐选项(用于下拉选择)
  551. * @return void
  552. */
  553. public function selectOptions():void
  554. {
  555. $packageList = Db::name('package')
  556. ->where('status', 1)
  557. ->field('id as value, name as label')
  558. ->order('sort', 'asc')
  559. ->order('id', 'asc')
  560. ->select()
  561. ->toArray();
  562. $this->success('获取成功', $packageList);
  563. }
  564. /**
  565. * 获取套餐规格选项(用于购买时选择)
  566. * @return void
  567. */
  568. public function specOptions():void
  569. {
  570. $packageId = $this->request->get('package_id');
  571. if (empty($packageId)) {
  572. $this->error(__('参数错误'));
  573. }
  574. $specList = Db::name('package_price')
  575. ->where('package_id', $packageId)
  576. ->where('stock', '>', 0) // 只显示有库存的规格
  577. ->field('id as value, spec_name as label, price, stock, type, card_type, unit_value, points_value, description')
  578. ->order('sort', 'asc')
  579. ->select()
  580. ->toArray();
  581. $this->success('获取成功', $specList);
  582. }
  583. /**
  584. * 更新规格销售数量(模拟用户购买)
  585. * @return void
  586. */
  587. public function updateSoldCount():void
  588. {
  589. $id = $this->request->post('id');
  590. $quantity = $this->request->post('quantity', 1);
  591. if (empty($id) || $quantity <= 0) {
  592. $this->error(__('参数错误'));
  593. }
  594. try {
  595. Db::startTrans();
  596. // 获取当前规格信息
  597. $specInfo = Db::name('package_price')
  598. ->where('id', $id)
  599. ->lock(true)
  600. ->find();
  601. if (!$specInfo) {
  602. throw new \Exception('规格不存在');
  603. }
  604. if ($specInfo['stock'] < $quantity) {
  605. throw new \Exception('库存不足');
  606. }
  607. // 更新库存和销售数量
  608. $result = Db::name('package_price')
  609. ->where('id', $id)
  610. ->update([
  611. 'stock' => Db::raw('stock - ' . $quantity),
  612. 'sold_count' => Db::raw('sold_count + ' . $quantity),
  613. 'update_time' => date('Y-m-d H:i:s')
  614. ]);
  615. // 如果库存为0,自动标记为售罄
  616. if ($specInfo['stock'] - $quantity <= 0) {
  617. Db::name('package_price')
  618. ->where('id', $id)
  619. ->update([
  620. 'status' => 2, // 售罄
  621. 'update_time' => date('Y-m-d H:i:s')
  622. ]);
  623. }
  624. Db::commit();
  625. } catch (\Exception $e) {
  626. Db::rollback();
  627. $this->error('操作失败: ' . $e->getMessage());
  628. }
  629. if ($result) {
  630. $this->success('更新成功');
  631. } else {
  632. $this->error(__('更新失败'));
  633. }
  634. }
  635. }