Package.php 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754
  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. $image = $this->request->post('image', '');
  288. $sort = $this->request->post('sort', 0);
  289. if (empty($packageId) || empty($specName)) {
  290. $this->error(__('参数不完整'));
  291. }
  292. if ($price <= 0) {
  293. $this->error(__('价格必须大于0'));
  294. }
  295. // 验证条件
  296. if ($cardType == 1 && $unitValue <= 0) {
  297. $this->error(__('次卡类型必须填写有效次数'));
  298. }
  299. if ($cardType == 2 && $pointsValue <= 0) {
  300. $this->error(__('积分类型必须填写有效积分数量'));
  301. }
  302. // 检查套餐是否存在
  303. $packageInfo = Db::name('package')
  304. ->where('id', $packageId)
  305. ->find();
  306. if (!$packageInfo) {
  307. $this->error(__('套餐不存在'));
  308. }
  309. // 验证规格名称是否已存在(同一套餐下不能重复)
  310. $checkSpec = Db::name('package_price')
  311. ->where('package_id', $packageId)
  312. ->where('spec_name', $specName);
  313. if (!empty($id)) {
  314. $checkSpec->where('id', '<>', $id);
  315. }
  316. if ($checkSpec->find()) {
  317. $this->error(__('同一套餐下规格名称不能重复'));
  318. }
  319. $data = [
  320. 'package_id' => $packageId,
  321. 'spec_name' => $specName,
  322. 'price' => $price,
  323. 'stock' => $stock,
  324. 'type' => $type,
  325. 'image' => $image,
  326. 'card_type' => $cardType,
  327. 'unit_value' => $unitValue,
  328. 'points_value' => $pointsValue,
  329. 'description' => $description,
  330. 'sort' => $sort,
  331. 'update_time' => date('Y-m-d H:i:s')
  332. ];
  333. try {
  334. if (!empty($id)) {
  335. // 编辑
  336. $result = Db::name('package_price')
  337. ->where('id', $id)
  338. ->update($data);
  339. $message = '编辑成功';
  340. } else {
  341. // 新增
  342. $data['create_time'] = date('Y-m-d H:i:s');
  343. $result = Db::name('package_price')
  344. ->insert($data);
  345. $id = Db::name('package_price')->getLastInsID();
  346. $message = '新增成功';
  347. }
  348. } catch (\Exception $e) {
  349. $this->error('操作失败: ' . $e->getMessage());
  350. }
  351. if ($result) {
  352. $this->success($message, ['id' => $id]);
  353. } else {
  354. $this->error(__('操作失败'));
  355. }
  356. }
  357. /**
  358. * 删除套餐规格
  359. * @return void
  360. */
  361. public function deleteSpec():void
  362. {
  363. $id = $this->request->post('id');
  364. if (empty($id)) {
  365. $this->error(__('参数错误'));
  366. }
  367. // 检查是否有用户购买记录
  368. try {
  369. $hasPurchase = Db::name('user_package')
  370. ->where('package_price_id', $id)
  371. ->count();
  372. if ($hasPurchase > 0) {
  373. $this->error(__('该规格已有用户购买记录,不能删除'));
  374. }
  375. } catch (\Exception $e) {
  376. // 表不存在,跳过检查
  377. }
  378. // 检查是否有企业购买记录
  379. try {
  380. $hasPurchase = Db::name('company_package')
  381. ->where('package_price_id', $id)
  382. ->count();
  383. if ($hasPurchase > 0) {
  384. $this->error(__('该规格已有企业购买记录,不能删除'));
  385. }
  386. } catch (\Exception $e) {
  387. // 表不存在,跳过检查
  388. }
  389. try {
  390. $result = Db::name('package_price')
  391. ->where('id', $id)
  392. ->delete();
  393. } catch (\Exception $e) {
  394. $this->error('删除失败: ' . $e->getMessage());
  395. }
  396. if ($result) {
  397. $this->success('删除成功');
  398. } else {
  399. $this->error(__('删除失败'));
  400. }
  401. }
  402. /**
  403. * 获取套餐规格列表
  404. * @return void
  405. */
  406. public function specList():void
  407. {
  408. $packageId = $this->request->get('package_id');
  409. if (empty($packageId)) {
  410. $this->error(__('参数错误'));
  411. }
  412. $specList = Db::name('package_price')
  413. ->where('package_id', $packageId)
  414. ->order('sort', 'asc')
  415. ->select()
  416. ->toArray();
  417. $this->success('查询成功', $specList);
  418. }
  419. /**
  420. * 批量更新套餐状态
  421. * @return void
  422. */
  423. public function batchStatus():void
  424. {
  425. $ids = $this->request->post('ids');
  426. $status = $this->request->post('status');
  427. if (empty($ids) || empty($status)) {
  428. $this->error(__('参数错误'));
  429. }
  430. if (!is_array($ids)) {
  431. $ids = explode(',', $ids);
  432. }
  433. try {
  434. $result = Db::name('package')
  435. ->whereIn('id', $ids)
  436. ->update([
  437. 'status' => $status,
  438. 'update_time' => date('Y-m-d H:i:s')
  439. ]);
  440. } catch (\Exception $e) {
  441. $this->error('操作失败: ' . $e->getMessage());
  442. }
  443. if ($result) {
  444. $statusText = $status == 1 ? '启用' : '禁用';
  445. $this->success("成功{$statusText}{$result}个套餐");
  446. } else {
  447. $this->error(__('操作失败'));
  448. }
  449. }
  450. /**
  451. * 批量更新规格库存
  452. * @return void
  453. */
  454. public function batchUpdateStock():void
  455. {
  456. $ids = $this->request->post('ids');
  457. $stock = $this->request->post('stock');
  458. if (empty($ids) || $stock === null) {
  459. $this->error(__('参数错误'));
  460. }
  461. if (!is_array($ids)) {
  462. $ids = explode(',', $ids);
  463. }
  464. try {
  465. $result = Db::name('package_price')
  466. ->whereIn('id', $ids)
  467. ->update([
  468. 'stock' => $stock,
  469. 'update_time' => date('Y-m-d H:i:s')
  470. ]);
  471. } catch (\Exception $e) {
  472. $this->error('操作失败: ' . $e->getMessage());
  473. }
  474. if ($result) {
  475. $this->success("成功更新{$result}个规格的库存");
  476. } else {
  477. $this->error(__('操作失败'));
  478. }
  479. }
  480. /**
  481. * 获取套餐类型选项
  482. * @return void
  483. */
  484. public function typeOptions():void
  485. {
  486. $options = [
  487. ['value' => 1, 'label' => '个人套餐'],
  488. ['value' => 2, 'label' => '团队套餐'],
  489. ['value' => 3, 'label' => '企业套餐']
  490. ];
  491. $this->success('获取成功', $options);
  492. }
  493. /**
  494. * 获取套餐状态选项
  495. * @return void
  496. */
  497. public function statusOptions():void
  498. {
  499. $options = [
  500. ['value' => 1, 'label' => '启用'],
  501. ['value' => 2, 'label' => '禁用']
  502. ];
  503. $this->success('获取成功', $options);
  504. }
  505. /**
  506. * 获取卡类型选项
  507. * @return void
  508. */
  509. public function cardTypeOptions():void
  510. {
  511. $options = [
  512. ['value' => 1, 'label' => '次卡'],
  513. ['value' => 2, 'label' => '按积分'],
  514. ['value' => 3, 'label' => '日卡'],
  515. ['value' => 4, 'label' => '周卡'],
  516. ['value' => 5, 'label' => '季卡'],
  517. ['value' => 6, 'label' => '月卡'],
  518. ['value' => 7, 'label' => '年卡']
  519. ];
  520. $this->success('获取成功', $options);
  521. }
  522. /**
  523. * 套餐统计信息
  524. * @return void
  525. */
  526. public function statistics():void
  527. {
  528. // 套餐总数
  529. $totalPackage = Db::name('package')->count();
  530. // 启用套餐数
  531. $activePackage = Db::name('package')
  532. ->where('status', 1)
  533. ->count();
  534. // 规格总数
  535. $totalSpec = Db::name('package_price')->count();
  536. // 总库存
  537. $totalStock = Db::name('package_price')
  538. ->sum('stock');
  539. // 总已售
  540. $totalSold = Db::name('package_price')
  541. ->sum('sold_count');
  542. $stats = [
  543. 'total_package' => $totalPackage,
  544. 'active_package' => $activePackage,
  545. 'total_spec' => $totalSpec,
  546. 'total_stock' => $totalStock,
  547. 'total_sold' => $totalSold
  548. ];
  549. $this->success('获取成功', $stats);
  550. }
  551. /**
  552. * 获取所有套餐选项(用于下拉选择)
  553. * @return void
  554. */
  555. public function selectOptions():void
  556. {
  557. $packageList = Db::name('package')
  558. ->where('status', 1)
  559. ->field('id as value, name as label')
  560. ->order('sort', 'asc')
  561. ->order('id', 'asc')
  562. ->select()
  563. ->toArray();
  564. $this->success('获取成功', $packageList);
  565. }
  566. /**
  567. * 获取套餐规格选项(用于购买时选择)
  568. * @return void
  569. */
  570. public function specOptions():void
  571. {
  572. $packageId = $this->request->get('package_id');
  573. if (empty($packageId)) {
  574. $this->error(__('参数错误'));
  575. }
  576. $specList = Db::name('package_price')
  577. ->where('package_id', $packageId)
  578. ->where('stock', '>', 0) // 只显示有库存的规格
  579. ->field('id as value, spec_name as label, price, stock, type, card_type, unit_value, points_value, description')
  580. ->order('sort', 'asc')
  581. ->select()
  582. ->toArray();
  583. $this->success('获取成功', $specList);
  584. }
  585. /**
  586. * 更新规格销售数量(模拟用户购买)
  587. * @return void
  588. */
  589. public function updateSoldCount():void
  590. {
  591. $id = $this->request->post('id');
  592. $quantity = $this->request->post('quantity', 1);
  593. if (empty($id) || $quantity <= 0) {
  594. $this->error(__('参数错误'));
  595. }
  596. try {
  597. Db::startTrans();
  598. // 获取当前规格信息
  599. $specInfo = Db::name('package_price')
  600. ->where('id', $id)
  601. ->lock(true)
  602. ->find();
  603. if (!$specInfo) {
  604. throw new \Exception('规格不存在');
  605. }
  606. if ($specInfo['stock'] < $quantity) {
  607. throw new \Exception('库存不足');
  608. }
  609. // 更新库存和销售数量
  610. $result = Db::name('package_price')
  611. ->where('id', $id)
  612. ->update([
  613. 'stock' => Db::raw('stock - ' . $quantity),
  614. 'sold_count' => Db::raw('sold_count + ' . $quantity),
  615. 'update_time' => date('Y-m-d H:i:s')
  616. ]);
  617. // 如果库存为0,自动标记为售罄
  618. if ($specInfo['stock'] - $quantity <= 0) {
  619. Db::name('package_price')
  620. ->where('id', $id)
  621. ->update([
  622. 'status' => 2, // 售罄
  623. 'update_time' => date('Y-m-d H:i:s')
  624. ]);
  625. }
  626. Db::commit();
  627. } catch (\Exception $e) {
  628. Db::rollback();
  629. $this->error('操作失败: ' . $e->getMessage());
  630. }
  631. if ($result) {
  632. $this->success('更新成功');
  633. } else {
  634. $this->error(__('更新失败'));
  635. }
  636. }
  637. }