Package.php 22 KB

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