Package.php 21 KB

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