PC-7\Administrator преди 10 месеца
родител
ревизия
097a549ddc

+ 780 - 0
app/admin/controller/package/Package.php

@@ -0,0 +1,780 @@
+<?php
+/**
+ * 套餐管理控制器
+ */
+
+namespace app\admin\controller\package;
+
+use app\common\controller\Backend;
+use think\facade\Db;
+use think\facade\Validate;
+
+class Package extends Backend
+{
+    /**
+     * 套餐列表
+     * @return void
+     */
+    public function index()
+    {
+        $where = [];
+
+        // 套餐名称搜索
+        if (!empty($this->request->get('name'))) {
+            $name = $this->request->get('name');
+            $where[] = ['p.name', 'like', '%' . $name . '%'];
+        }
+
+        // 套餐类型筛选
+        if (!empty($this->request->get('type'))) {
+            $type = $this->request->get('type');
+            $where['p.type'] = $type;
+        }
+
+        // 状态筛选
+        if (!empty($this->request->get('status'))) {
+            $status = $this->request->get('status');
+            $where['p.status'] = $status;
+        }
+
+        // 排序
+        $order = [];
+        if (!empty($this->request->get('order'))) {
+            $order_a = $this->request->get('order');
+            $order_a = explode(',', $order_a);
+            $order[$order_a[0]] = $order_a[1];
+        } else {
+            $order['p.sort'] = 'asc';
+            $order['p.id'] = 'desc';
+        }
+
+        // 分页
+        $page = $this->request->get('page', 1);
+        $limit = $this->request->get('limit', 20);
+
+        // 查询数据(统计规格数量和已售总数)
+        $specCountSubQuery = Db::name('package_price')
+            ->field('package_id, COUNT(*) as spec_count, SUM(sold_count) as total_sold')
+            ->where('status', 1)
+            ->group('package_id')
+            ->buildSql();
+
+        $packageList = Db::name('package')
+            ->alias('p')
+            ->leftJoin([$specCountSubQuery => 'sc'], 'p.id = sc.package_id')
+            ->field('p.*, IFNULL(sc.spec_count, 0) as spec_count, IFNULL(sc.total_sold, 0) as total_sold')
+            ->where($where)
+            ->order($order)
+            ->paginate([
+                'list_rows' => $limit,
+                'page' => $page
+            ]);
+
+        // 转换功能特性字段
+        $list = $packageList->items();
+        foreach ($list as &$item) {
+            if (!empty($item['features'])) {
+                $item['features'] = json_decode($item['features'], true);
+            } else {
+                $item['features'] = [];
+            }
+        }
+
+        $this->success('查询成功', [
+            'list' => $list,
+            'total' => $packageList->total()
+        ]);
+    }
+
+    /**
+     * 新增/编辑套餐
+     * @return void
+     */
+    public function edit()
+    {
+        $id = $this->request->post('id');
+        $name = $this->request->post('name');
+        $type = $this->request->post('type', 1);
+        $description = $this->request->post('description', '');
+        $features = $this->request->post('features', []);
+        $sort = $this->request->post('sort', 0);
+        $status = $this->request->post('status', 1);
+
+        if (empty($name)) {
+            $this->error(__('套餐名称不能为空'));
+        }
+
+        // 验证套餐名称是否已存在(编辑时排除自己)
+        $checkName = Db::name('package')
+            ->where('name', $name);
+
+        if (!empty($id)) {
+            $checkName->where('id', '<>', $id);
+        }
+
+        if ($checkName->find()) {
+            $this->error(__('套餐名称已存在'));
+        }
+
+        // 处理功能特性字段
+        if (!is_array($features)) {
+            $features = [];
+        }
+
+        $data = [
+            'name' => $name,
+            'type' => $type,
+            'description' => $description,
+            'features' => json_encode($features, JSON_UNESCAPED_UNICODE),
+            'sort' => $sort,
+            'status' => $status,
+            'update_time' => date('Y-m-d H:i:s')
+        ];
+
+        try {
+            if (!empty($id)) {
+                // 编辑
+                $packageInfo = Db::name('package')
+                    ->where('id', $id)
+                    ->find();
+
+                if (!$packageInfo) {
+                    $this->error(__('套餐不存在'));
+                }
+
+                $result = Db::name('package')
+                    ->where('id', $id)
+                    ->update($data);
+                $message = '编辑成功';
+            } else {
+                // 新增
+                $data['create_time'] = date('Y-m-d H:i:s');
+                $result = Db::name('package')
+                    ->insert($data);
+                $id = Db::name('package')->getLastInsID();
+                $message = '新增成功';
+            }
+
+            if ($result) {
+                $this->success($message, ['id' => $id]);
+            } else {
+                $this->error(__('操作失败'));
+            }
+        } catch (\Exception $e) {
+            $this->error('操作失败: ' . $e->getMessage());
+        }
+    }
+
+    /**
+     * 删除套餐
+     * @return void
+     */
+    public function delete()
+    {
+        $id = $this->request->post('id');
+
+        if (empty($id)) {
+            $this->error(__('参数错误'));
+        }
+
+        // 检查是否有规格存在
+        $hasSpec = Db::name('package_price')
+            ->where('package_id', $id)
+            ->count();
+
+        if ($hasSpec > 0) {
+            $this->error(__('该套餐下有关联的规格,请先删除规格'));
+        }
+
+        // 检查是否有用户购买记录(如果user_package表存在)
+        try {
+            $hasUserPackage = Db::name('user_package')
+                ->where('package_id', $id)
+                ->count();
+
+            if ($hasUserPackage > 0) {
+                $this->error(__('该套餐已有用户购买记录,不能删除'));
+            }
+        } catch (\Exception $e) {
+            // 表不存在,跳过检查
+        }
+
+        // 检查是否有企业购买记录(如果company_package表存在)
+        try {
+            $hasCompanyPackage = Db::name('company_package')
+                ->where('package_id', $id)
+                ->count();
+
+            if ($hasCompanyPackage > 0) {
+                $this->error(__('该套餐已有企业购买记录,不能删除'));
+            }
+        } catch (\Exception $e) {
+            // 表不存在,跳过检查
+        }
+
+        try {
+            $result = Db::name('package')
+                ->where('id', $id)
+                ->delete();
+
+            if ($result) {
+                $this->success('删除成功');
+            } else {
+                $this->error(__('删除失败'));
+            }
+        } catch (\Exception $e) {
+            $this->error('删除失败: ' . $e->getMessage());
+        }
+    }
+
+    /**
+     * 套餐详情
+     * @return void
+     */
+    public function detail()
+    {
+        $id = $this->request->get('id');
+
+        if (empty($id)) {
+            $this->error(__('参数错误'));
+        }
+
+        // 获取套餐基础信息
+        $packageInfo = Db::name('package')
+            ->where('id', $id)
+            ->find();
+
+        if (!$packageInfo) {
+            $this->error(__('套餐不存在'));
+        }
+
+        // 处理功能特性字段
+        if (!empty($packageInfo['features'])) {
+            $packageInfo['features'] = json_decode($packageInfo['features'], true);
+        } else {
+            $packageInfo['features'] = [];
+        }
+
+        // 获取套餐规格列表
+        $specList = Db::name('package_price')
+            ->where('package_id', $id)
+            ->order('sort', 'asc')
+            ->select()
+            ->toArray();
+
+        // 统计库存和销售情况
+        $totalStock = 0;
+        $totalSold = 0;
+        foreach ($specList as $spec) {
+            $totalStock += $spec['stock'];
+            $totalSold += $spec['sold_count'];
+        }
+
+        // 统计用户购买数量(如果user_package表存在)
+        $userPurchaseCount = 0;
+        try {
+            $userPurchaseCount = Db::name('user_package')
+                ->where('package_id', $id)
+                ->count();
+        } catch (\Exception $e) {
+            // 表不存在,设为0
+        }
+
+        // 统计企业购买数量(如果company_package表存在)
+        $companyPurchaseCount = 0;
+        try {
+            $companyPurchaseCount = Db::name('company_package')
+                ->where('package_id', $id)
+                ->count();
+        } catch (\Exception $e) {
+            // 表不存在,设为0
+        }
+
+        $result = [
+            'package_info' => $packageInfo,
+            'spec_list' => $specList,
+            'statistics' => [
+                'spec_count' => count($specList),
+                'total_stock' => $totalStock,
+                'total_sold' => $totalSold,
+                'user_purchase_count' => $userPurchaseCount,
+                'company_purchase_count' => $companyPurchaseCount
+            ]
+        ];
+
+        $this->success('查询成功', $result);
+    }
+
+    /**
+     * 添加/编辑套餐规格
+     * @return void
+     */
+    public function editSpec()
+    {
+        $id = $this->request->post('id');
+        $packageId = $this->request->post('package_id');
+        $specName = $this->request->post('spec_name');
+        $price = $this->request->post('price', 0);
+        $originalPrice = $this->request->post('original_price', 0);
+        $stock = $this->request->post('stock', 0);
+        $unitValue = $this->request->post('unit_value', '');
+        $description = $this->request->post('description', '');
+        $sort = $this->request->post('sort', 0);
+
+        if (empty($packageId) || empty($specName)) {
+            $this->error(__('参数不完整'));
+        }
+
+        if ($price <= 0) {
+            $this->error(__('价格必须大于0'));
+        }
+
+        // 检查套餐是否存在
+        $packageInfo = Db::name('package')
+            ->where('id', $packageId)
+            ->find();
+
+        if (!$packageInfo) {
+            $this->error(__('套餐不存在'));
+        }
+
+        // 验证规格名称是否已存在(同一套餐下不能重复)
+        $checkSpec = Db::name('package_price')
+            ->where('package_id', $packageId)
+            ->where('spec_name', $specName);
+
+        if (!empty($id)) {
+            $checkSpec->where('id', '<>', $id);
+        }
+
+        if ($checkSpec->find()) {
+            $this->error(__('同一套餐下规格名称不能重复'));
+        }
+
+        $data = [
+            'package_id' => $packageId,
+            'spec_name' => $specName,
+            'price' => $price,
+            'original_price' => $originalPrice,
+            'stock' => $stock,
+            'unit_value' => $unitValue,
+            'description' => $description,
+            'sort' => $sort,
+            'update_time' => date('Y-m-d H:i:s')
+        ];
+
+        try {
+            if (!empty($id)) {
+                // 编辑
+                $result = Db::name('package_price')
+                    ->where('id', $id)
+                    ->update($data);
+                $message = '编辑成功';
+            } else {
+                // 新增
+                $data['create_time'] = date('Y-m-d H:i:s');
+                $result = Db::name('package_price')
+                    ->insert($data);
+                $id = Db::name('package_price')->getLastInsID();
+                $message = '新增成功';
+            }
+
+            if ($result) {
+                $this->success($message, ['id' => $id]);
+            } else {
+                $this->error(__('操作失败'));
+            }
+        } catch (\Exception $e) {
+            $this->error('操作失败: ' . $e->getMessage());
+        }
+    }
+
+    /**
+     * 删除套餐规格
+     * @return void
+     */
+    public function deleteSpec()
+    {
+        $id = $this->request->post('id');
+
+        if (empty($id)) {
+            $this->error(__('参数错误'));
+        }
+
+        // 检查是否有用户购买记录
+        try {
+            $hasPurchase = Db::name('user_package')
+                ->where('package_price_id', $id)
+                ->count();
+
+            if ($hasPurchase > 0) {
+                $this->error(__('该规格已有用户购买记录,不能删除'));
+            }
+        } catch (\Exception $e) {
+            // 表不存在,跳过检查
+        }
+
+        // 检查是否有企业购买记录
+        try {
+            $hasPurchase = Db::name('company_package')
+                ->where('package_price_id', $id)
+                ->count();
+
+            if ($hasPurchase > 0) {
+                $this->error(__('该规格已有企业购买记录,不能删除'));
+            }
+        } catch (\Exception $e) {
+            // 表不存在,跳过检查
+        }
+
+        try {
+            $result = Db::name('package_price')
+                ->where('id', $id)
+                ->delete();
+
+            if ($result) {
+                $this->success('删除成功');
+            } else {
+                $this->error(__('删除失败'));
+            }
+        } catch (\Exception $e) {
+            $this->error('删除失败: ' . $e->getMessage());
+        }
+    }
+
+    /**
+     * 获取套餐规格列表
+     * @return void
+     */
+    public function specList()
+    {
+        $packageId = $this->request->get('package_id');
+
+        if (empty($packageId)) {
+            $this->error(__('参数错误'));
+        }
+
+        $specList = Db::name('package_price')
+            ->where('package_id', $packageId)
+            ->order('sort', 'asc')
+            ->select()
+            ->toArray();
+
+        $this->success('查询成功', $specList);
+    }
+
+    /**
+     * 批量更新套餐状态
+     * @return void
+     */
+    public function batchStatus()
+    {
+        $ids = $this->request->post('ids');
+        $status = $this->request->post('status');
+
+        if (empty($ids) || empty($status)) {
+            $this->error(__('参数错误'));
+        }
+
+        if (!is_array($ids)) {
+            $ids = explode(',', $ids);
+        }
+
+        try {
+            $result = Db::name('package')
+                ->whereIn('id', $ids)
+                ->update([
+                    'status' => $status,
+                    'update_time' => date('Y-m-d H:i:s')
+                ]);
+
+            if ($result) {
+                $statusText = $status == 1 ? '启用' : '禁用';
+                $this->success("成功{$statusText}{$result}个套餐");
+            } else {
+                $this->error(__('操作失败'));
+            }
+        } catch (\Exception $e) {
+            $this->error('操作失败: ' . $e->getMessage());
+        }
+    }
+
+    /**
+     * 批量更新规格库存
+     * @return void
+     */
+    public function batchUpdateStock()
+    {
+        $ids = $this->request->post('ids');
+        $stock = $this->request->post('stock');
+
+        if (empty($ids) || $stock === null) {
+            $this->error(__('参数错误'));
+        }
+
+        if (!is_array($ids)) {
+            $ids = explode(',', $ids);
+        }
+
+        try {
+            $result = Db::name('package_price')
+                ->whereIn('id', $ids)
+                ->update([
+                    'stock' => $stock,
+                    'update_time' => date('Y-m-d H:i:s')
+                ]);
+
+            if ($result) {
+                $this->success("成功更新{$result}个规格的库存");
+            } else {
+                $this->error(__('操作失败'));
+            }
+        } catch (\Exception $e) {
+            $this->error('操作失败: ' . $e->getMessage());
+        }
+    }
+
+    /**
+     * 获取套餐类型选项
+     * @return void
+     */
+    public function typeOptions()
+    {
+        $options = [
+            ['value' => 1, 'label' => '个人套餐'],
+            ['value' => 2, 'label' => '团队套餐'],
+            ['value' => 3, 'label' => '企业套餐']
+        ];
+
+        $this->success('获取成功', $options);
+    }
+
+    /**
+     * 获取套餐状态选项
+     * @return void
+     */
+    public function statusOptions()
+    {
+        $options = [
+            ['value' => 1, 'label' => '启用'],
+            ['value' => 2, 'label' => '禁用']
+        ];
+
+        $this->success('获取成功', $options);
+    }
+
+    /**
+     * 获取规格状态选项
+     * @return void
+     */
+    public function specStatusOptions()
+    {
+        $options = [
+            ['value' => 1, 'label' => '正常'],
+            ['value' => 2, 'label' => '售罄']
+        ];
+
+        $this->success('获取成功', $options);
+    }
+
+    /**
+     * 套餐统计信息
+     * @return void
+     */
+    public function statistics()
+    {
+        // 套餐总数
+        $totalPackage = Db::name('package')->count();
+
+        // 启用套餐数
+        $activePackage = Db::name('package')
+            ->where('status', 1)
+            ->count();
+
+        // 规格总数
+        $totalSpec = Db::name('package_price')->count();
+
+        // 总库存
+        $totalStock = Db::name('package_price')
+            ->sum('stock');
+
+        // 总已售
+        $totalSold = Db::name('package_price')
+            ->sum('sold_count');
+
+        // 按类型统计
+        $typeStats = Db::name('package')
+            ->field('type, COUNT(*) as count')
+            ->where('status', 1)
+            ->group('type')
+            ->select()
+            ->toArray();
+
+        $stats = [
+            'total_package' => $totalPackage,
+            'active_package' => $activePackage,
+            'total_spec' => $totalSpec,
+            'total_stock' => $totalStock,
+            'total_sold' => $totalSold,
+            'type_stats' => $typeStats
+        ];
+
+        $this->success('获取成功', $stats);
+    }
+
+    /**
+     * 导出套餐数据
+     * @return void
+     */
+    public function export()
+    {
+        $where = [];
+
+        // 根据筛选条件构建查询条件
+        if (!empty($this->request->get('name'))) {
+            $name = $this->request->get('name');
+            $where[] = ['name', 'like', '%' . $name . '%'];
+        }
+
+        if (!empty($this->request->get('status'))) {
+            $status = $this->request->get('status');
+            $where['status'] = $status;
+        }
+
+        // 查询数据
+        $packageList = Db::name('package')
+            ->where($where)
+            ->order('sort', 'asc')
+            ->order('id', 'desc')
+            ->select()
+            ->toArray();
+
+        // 状态映射
+        $typeMap = [1 => '个人套餐', 2 => '团队套餐', 3 => '企业套餐'];
+        $statusMap = [1 => '启用', 2 => '禁用'];
+
+        foreach ($packageList as &$item) {
+            $item['type'] = $typeMap[$item['type']] ?? '未知';
+            $item['status'] = $statusMap[$item['status']] ?? '未知';
+
+            // 获取规格信息
+            $specs = Db::name('package_price')
+                ->where('package_id', $item['id'])
+                ->order('sort', 'asc')
+                ->select()
+                ->toArray();
+
+            $specInfo = [];
+            foreach ($specs as $spec) {
+                $specInfo[] = "{$spec['spec_name']} - ¥{$spec['price']} (库存:{$spec['stock']})";
+            }
+
+            $item['spec_info'] = implode('; ', $specInfo);
+        }
+
+        $this->success('查询成功', $packageList);
+    }
+
+    /**
+     * 获取所有套餐选项(用于下拉选择)
+     * @return void
+     */
+    public function selectOptions()
+    {
+        $packageList = Db::name('package')
+            ->where('status', 1)
+            ->field('id as value, name as label, type')
+            ->order('sort', 'asc')
+            ->order('id', 'asc')
+            ->select()
+            ->toArray();
+
+        $this->success('获取成功', $packageList);
+    }
+
+    /**
+     * 获取套餐规格选项(用于购买时选择)
+     * @return void
+     */
+    public function specOptions()
+    {
+        $packageId = $this->request->get('package_id');
+
+        if (empty($packageId)) {
+            $this->error(__('参数错误'));
+        }
+
+        $specList = Db::name('package_price')
+            ->where('package_id', $packageId)
+            ->where('status', 1)
+            ->where('stock', '>', 0) // 只显示有库存的规格
+            ->field('id as value, spec_name as label, price, stock, unit_value, description')
+            ->order('sort', 'asc')
+            ->select()
+            ->toArray();
+
+        $this->success('获取成功', $specList);
+    }
+
+    /**
+     * 更新规格销售数量(模拟用户购买)
+     * @return void
+     */
+    public function updateSoldCount()
+    {
+        $id = $this->request->post('id');
+        $quantity = $this->request->post('quantity', 1);
+
+        if (empty($id) || $quantity <= 0) {
+            $this->error(__('参数错误'));
+        }
+
+        try {
+            Db::startTrans();
+
+            // 获取当前规格信息
+            $specInfo = Db::name('package_price')
+                ->where('id', $id)
+                ->lock(true)
+                ->find();
+
+            if (!$specInfo) {
+                throw new \Exception('规格不存在');
+            }
+
+            if ($specInfo['stock'] < $quantity) {
+                throw new \Exception('库存不足');
+            }
+
+            // 更新库存和销售数量
+            $result = Db::name('package_price')
+                ->where('id', $id)
+                ->update([
+                    'stock' => Db::raw('stock - ' . $quantity),
+                    'sold_count' => Db::raw('sold_count + ' . $quantity),
+                    'update_time' => date('Y-m-d H:i:s')
+                ]);
+
+            // 如果库存为0,自动标记为售罄
+            if ($specInfo['stock'] - $quantity <= 0) {
+                Db::name('package_price')
+                    ->where('id', $id)
+                    ->update([
+                        'status' => 2, // 售罄
+                        'update_time' => date('Y-m-d H:i:s')
+                    ]);
+            }
+
+            Db::commit();
+
+            if ($result) {
+                $this->success('更新成功');
+            } else {
+                $this->error(__('更新失败'));
+            }
+        } catch (\Exception $e) {
+            Db::rollback();
+            $this->error('操作失败: ' . $e->getMessage());
+        }
+    }
+}

+ 592 - 0
app/admin/controller/product/Product.php

@@ -0,0 +1,592 @@
+<?php
+/**
+ * 产品管理控制器
+ */
+
+namespace app\admin\controller\product;
+
+use app\common\controller\Backend;
+use think\facade\Db;
+use think\facade\Validate;
+
+class Product extends Backend
+{
+
+    protected array $noNeedLogin = ['*'];
+    /**
+     * 产品列表
+     * @return void
+     */
+    public function index():void
+    {
+        $where = [];
+
+        // 产品名称搜索
+        if (!empty($this->request->get('name'))) {
+            $name = $this->request->get('name');
+            $where[] = ['p.name', 'like', '%' . $name . '%'];
+        }
+
+        // 产品编码搜索
+        if (!empty($this->request->get('code'))) {
+            $code = $this->request->get('code');
+            $where[] = ['p.code', 'like', '%' . $code . '%'];
+        }
+
+        // 分类筛选
+        if (!empty($this->request->get('category_id'))) {
+            $categoryId = $this->request->get('category_id');
+            $where['p.category_id'] = $categoryId;
+        }
+
+        // 库存状态筛选
+        if (!empty($this->request->get('stock_status'))) {
+            $stockStatus = $this->request->get('stock_status');
+            $where['p.stock_status'] = $stockStatus;
+        }
+
+        // 产品状态筛选
+        if (!empty($this->request->get('status'))) {
+            $status = $this->request->get('status');
+            $where['p.status'] = $status;
+        }
+
+        // 排序
+        $order = [];
+        if (!empty($this->request->get('order'))) {
+            $order_a = $this->request->get('order');
+            $order_a = explode(',', $order_a);
+            $order[$order_a[0]] = $order_a[1];
+        } else {
+            $order['p.sort'] = 'asc';
+            $order['p.id'] = 'desc';
+        }
+
+        // 分页
+        $page = $this->request->get('page', 1);
+        $limit = $this->request->get('limit', 20);
+
+        // 查询数据
+        $productList = Db::name('product')
+            ->alias('p')
+            ->leftJoin('product_category pc', 'p.category_id = pc.id')
+            ->field('p.*, pc.name as category_name')
+            ->where($where)
+            ->order($order)
+            ->paginate([
+                'list_rows' => $limit,
+                'page' => $page
+            ]);
+
+        // 转换图片字段
+        $list = $productList->items();
+        foreach ($list as &$item) {
+            if (!empty($item['image'])) {
+                $item['image'] = json_decode($item['image'], true);
+            } else {
+                $item['image'] = [];
+            }
+
+            if (!empty($item['key_attributes'])) {
+                $item['key_attributes'] = json_decode($item['key_attributes'], true);
+            } else {
+                $item['key_attributes'] = [];
+            }
+        }
+
+        $this->success('查询成功', [
+            'list' => $list,
+            'total' => $productList->total()
+        ]);
+    }
+
+    /**
+     * 新增/编辑产品
+     * @return void
+     */
+    public function edit():void
+    {
+        $id = $this->request->post('id');
+        $name = $this->request->post('name');
+        $code = $this->request->post('code');
+        $categoryId = $this->request->post('category_id');
+        $description = $this->request->post('description', '');
+        $detail = $this->request->post('detail', '');
+        $keyAttributes = $this->request->post('key_attributes', []);
+        $image = $this->request->post('image');
+        $stockStatus = $this->request->post('stock_status', 1);
+        $status = $this->request->post('status', 1);
+        $sort = $this->request->post('sort', 0);
+
+        if (empty($name)) {
+            $this->error(__('产品名称不能为空'));
+        }
+
+        if (empty($code)) {
+            $this->error(__('产品编码不能为空'));
+        }
+
+        // 验证产品编码是否已存在(编辑时排除自己)
+        $checkCode = Db::name('product')
+            ->where('code', $code);
+
+        if (!empty($id)) {
+            $checkCode->where('id', '<>', $id);
+        }
+
+        if ($checkCode->find()) {
+            $this->error(__('产品编码已存在'));
+        }
+
+        // 处理图片和属性字段
+        if (empty($image)) {
+            $image = "";
+        }
+
+        if (!is_array($keyAttributes)) {
+            $keyAttributes = [];
+        }
+
+        $data = [
+            'name' => $name,
+            'code' => $code,
+            'category_id' => $categoryId ?: null,
+            'description' => $description,
+            'detail' => $detail,
+            'key_attributes' => json_encode($keyAttributes, JSON_UNESCAPED_UNICODE),
+            'image' => $image,
+            'stock_status' => $stockStatus,
+            'status' => $status,
+            'sort' => $sort,
+            'update_time' => date('Y-m-d H:i:s')
+        ];
+        try {
+            if (!empty($id)) {
+                // 编辑
+                $productInfo = Db::name('product')
+                    ->where('id', $id)
+                    ->find();
+
+                if (!$productInfo) {
+                    $this->error(__('产品不存在'));
+                }
+
+                $result = Db::name('product')
+                    ->where('id', $id)
+                    ->update($data);
+                $message = '编辑成功';
+            } else {
+                // 新增
+                $data['create_time'] = date('Y-m-d H:i:s');
+                $result = Db::name('product')
+                    ->insert($data);
+                $id = Db::name('product')->getLastInsID();
+                $message = '新增成功';
+            }
+
+        } catch (\Exception $e) {
+            $this->error('操作失败: ' . $e->getMessage());
+        }
+        if ($result) {
+            $this->success($message, ['id' => $id]);
+        } else {
+            $this->error(__('操作失败'));
+        }
+    }
+
+    /**
+     * 删除产品
+     * @return void
+     */
+    public function delete():void
+    {
+        $id = $this->request->post('id');
+
+        if (empty($id)) {
+            $this->error(__('参数错误'));
+        }
+
+        // 检查是否有套餐关联
+        $hasPackage = Db::name('package')
+            ->where('product_id', $id)
+            ->count();
+
+        if ($hasPackage > 0) {
+            $this->error(__('该产品下有关联的套餐,请先删除套餐'));
+        }
+
+        // 检查是否有订单关联
+        $hasOrder = Db::name('order_item')
+            ->where('product_id', $id)
+            ->count();
+
+        if ($hasOrder > 0) {
+            $this->error(__('该产品已有订单记录,不能删除'));
+        }
+
+        try {
+            Db::startTrans();
+
+            // 删除产品
+            $result = Db::name('product')
+                ->where('id', $id)
+                ->delete();
+
+            // 删除产品规格
+            Db::name('product_spec')
+                ->where('product_id', $id)
+                ->delete();
+
+            Db::commit();
+
+        } catch (\Exception $e) {
+            Db::rollback();
+            $this->error('删除失败: ' . $e->getMessage());
+        }
+        if ($result) {
+            $this->success('删除成功');
+        } else {
+            $this->error(__('删除失败'));
+        }
+    }
+
+    /**
+     * 产品详情
+     * @return void
+     */
+    public function detail():void
+    {
+        $id = $this->request->get('id');
+
+        if (empty($id)) {
+            $this->error(__('参数错误'));
+        }
+
+        // 获取产品基础信息
+        $productInfo = Db::name('product')
+            ->alias('p')
+            ->leftJoin('product_category pc', 'p.category_id = pc.id')
+            ->where('p.id', $id)
+            ->field('p.*, pc.name as category_name')
+            ->find();
+
+        if (!$productInfo) {
+            $this->error(__('产品不存在'));
+        }
+
+        if (empty($productInfo['image'])) {
+            $productInfo['image'] = [];
+        }
+
+        if (!empty($productInfo['key_attributes'])) {
+            $productInfo['key_attributes'] = json_decode($productInfo['key_attributes'], true);
+        } else {
+            $productInfo['key_attributes'] = [];
+        }
+
+        // 获取产品规格
+        $specList = Db::name('product_spec')
+            ->where('product_id', $id)
+            ->order('sort', 'asc')
+            ->select()
+            ->toArray();
+
+        foreach ($specList as &$spec) {
+            if (!empty($spec['value'])) {
+                $spec['value'] = json_decode($spec['value'], true);
+            } else {
+                $spec['value'] = [];
+            }
+        }
+
+        // 获取关联套餐数量
+        $packageCount = Db::name('package')
+            ->where('product_id', $id)
+            ->count();
+
+        // 获取订单数量
+        $orderCount = Db::name('order_item')
+            ->where('product_id', $id)
+            ->count();
+
+        $result = [
+            'product_info' => $productInfo,
+            'spec_list' => $specList,
+            'package_count' => $packageCount,
+            'order_count' => $orderCount
+        ];
+
+        $this->success('查询成功', $result);
+    }
+
+    /**
+     * 批量更新产品状态
+     * @return void
+     */
+    public function batchStatus():void
+    {
+        $ids = $this->request->post('ids');
+        $status = $this->request->post('status');
+
+        if (empty($ids) || empty($status)) {
+            $this->error(__('参数错误'));
+        }
+
+        if (!is_array($ids)) {
+            $ids = explode(',', $ids);
+        }
+
+        try {
+            $result = Db::name('product')
+                ->whereIn('id', $ids)
+                ->update([
+                    'status' => $status,
+                    'update_time' => date('Y-m-d H:i:s')
+                ]);
+
+        } catch (\Exception $e) {
+            $this->error('操作失败: ' . $e->getMessage());
+        }
+
+        if ($result) {
+            $statusText = $status == 1 ? '上架' : '下架';
+            $this->success("成功{$statusText}{$result}个产品");
+        } else {
+            $this->error(__('操作失败'));
+        }
+    }
+
+    /**
+     * 批量更新库存状态
+     * @return void
+     */
+    public function batchStockStatus():void
+    {
+        $ids = $this->request->post('ids');
+        $stockStatus = $this->request->post('stock_status');
+
+        if (empty($ids) || empty($stockStatus)) {
+            $this->error(__('参数错误'));
+        }
+
+        if (!is_array($ids)) {
+            $ids = explode(',', $ids);
+        }
+
+        try {
+            $result = Db::name('product')
+                ->whereIn('id', $ids)
+                ->update([
+                    'stock_status' => $stockStatus,
+                    'update_time' => date('Y-m-d H:i:s')
+                ]);
+
+        } catch (\Exception $e) {
+            $this->error('操作失败: ' . $e->getMessage());
+        }
+
+        if ($result) {
+            $statusText = [
+                1 => '充足',
+                2 => '缺货',
+                3 => '下架'
+            ][$stockStatus] ?? '未知';
+            $this->success("成功更新{$result}个产品的库存状态为{$statusText}");
+        } else {
+            $this->error(__('操作失败'));
+        }
+
+    }
+
+    /**
+     * 添加/编辑产品规格
+     * @return void
+     */
+    public function editSpec():void
+    {
+        $id = $this->request->post('id');
+        $productId = $this->request->post('product_id');
+        $name = $this->request->post('name');
+        $value = $this->request->post('value', []);
+        $sort = $this->request->post('sort', 0);
+
+        if (empty($productId) || empty($name)) {
+            $this->error(__('参数不完整'));
+        }
+
+        // 检查产品是否存在
+        $productInfo = Db::name('product')
+            ->where('id', $productId)
+            ->find();
+
+        if (!$productInfo) {
+            $this->error(__('产品不存在'));
+        }
+
+        // 处理规格值
+        if (!is_array($value)) {
+            $value = [];
+        }
+
+        $data = [
+            'product_id' => $productId,
+            'name' => $name,
+            'value' => json_encode($value, JSON_UNESCAPED_UNICODE),
+            'sort' => $sort
+        ];
+
+        try {
+            if (!empty($id)) {
+                // 编辑
+                $result = Db::name('product_spec')
+                    ->where('id', $id)
+                    ->update($data);
+                $message = '编辑成功';
+            } else {
+                // 新增
+                $data['create_time'] = date('Y-m-d H:i:s');
+                $result = Db::name('product_spec')
+                    ->insert($data);
+                $id = Db::name('product_spec')->getLastInsID();
+                $message = '新增成功';
+            }
+
+        } catch (\Exception $e) {
+            $this->error('操作失败: ' . $e->getMessage());
+        }
+        if ($result) {
+            $this->success($message, ['id' => $id]);
+        } else {
+            $this->error(__('操作失败'));
+        }
+    }
+
+    /**
+     * 删除产品规格
+     * @return void
+     */
+    public function deleteSpec():void
+    {
+        $id = $this->request->post('id');
+
+        if (empty($id)) {
+            $this->error(__('参数错误'));
+        }
+
+        try {
+            $result = Db::name('product_spec')
+                ->where('id', $id)
+                ->delete();
+
+        } catch (\Exception $e) {
+            $this->error('删除失败: ' . $e->getMessage());
+        }
+
+        if ($result) {
+            $this->success('删除成功');
+        } else {
+            $this->error(__('删除失败'));
+        }
+    }
+
+    /**
+     * 获取产品规格列表
+     * @return void
+     */
+    public function specList():void
+    {
+        $productId = $this->request->get('product_id');
+
+        if (empty($productId)) {
+            $this->error(__('参数错误'));
+        }
+
+        $specList = Db::name('product_spec')
+            ->where('product_id', $productId)
+            ->order('sort', 'asc')
+            ->select()
+            ->toArray();
+
+        foreach ($specList as &$spec) {
+            if (!empty($spec['value'])) {
+                $spec['value'] = json_decode($spec['value'], true);
+            } else {
+                $spec['value'] = [];
+            }
+        }
+
+        $this->success('查询成功', $specList);
+    }
+
+    /**
+     * 获取产品状态选项
+     * @return void
+     */
+    public function statusOptions():void
+    {
+        $options = [
+            ['value' => 1, 'label' => '上架'],
+            ['value' => 2, 'label' => '下架']
+        ];
+
+        $this->success('获取成功', $options);
+    }
+
+    /**
+     * 获取库存状态选项
+     * @return void
+     */
+    public function stockStatusOptions():void
+    {
+        $options = [
+            ['value' => 1, 'label' => '充足'],
+            ['value' => 2, 'label' => '缺货'],
+            ['value' => 3, 'label' => '下架']
+        ];
+
+        $this->success('获取成功', $options);
+    }
+
+    /**
+     * 导出产品列表
+     * @return void
+     */
+    public function export():void
+    {
+        $where = [];
+
+        // 根据筛选条件构建查询条件
+        if (!empty($this->request->get('name'))) {
+            $name = $this->request->get('name');
+            $where[] = ['p.name', 'like', '%' . $name . '%'];
+        }
+
+        if (!empty($this->request->get('status'))) {
+            $status = $this->request->get('status');
+            $where['p.status'] = $status;
+        }
+
+        // 查询数据
+        $productList = Db::name('product')
+            ->alias('p')
+            ->leftJoin('product_category pc', 'p.category_id = pc.id')
+            ->field('p.id, p.name, p.code, pc.name as category_name, 
+                     p.stock_status, p.status, p.sort, p.create_time')
+            ->where($where)
+            ->order('p.sort', 'asc')
+            ->order('p.id', 'desc')
+            ->select()
+            ->toArray();
+
+        // 状态映射
+        $stockStatusMap = [1 => '充足', 2 => '缺货', 3 => '下架'];
+        $statusMap = [1 => '上架', 2 => '下架'];
+
+        foreach ($productList as &$item) {
+            $item['stock_status'] = $stockStatusMap[$item['stock_status']] ?? '未知';
+            $item['status'] = $statusMap[$item['status']] ?? '未知';
+        }
+
+        $this->success('查询成功', $productList);
+    }
+}

+ 435 - 0
app/admin/controller/product/ProductCategory.php

@@ -0,0 +1,435 @@
+<?php
+/**
+ * 产品分类管理控制器
+ */
+
+namespace app\admin\controller\product;
+
+use app\common\controller\Backend;
+use think\facade\Db;
+
+class ProductCategory extends Backend
+{
+    /**
+     * 分类列表(树形结构)
+     * @return void
+     */
+    public function index()
+    {
+        $where = [];
+
+        // 分类名称搜索
+        if (!empty($this->request->get('name'))) {
+            $name = $this->request->get('name');
+            $where[] = ['name', 'like', '%' . $name . '%'];
+        }
+
+        // 状态筛选
+        if (!empty($this->request->get('status'))) {
+            $status = $this->request->get('status');
+            $where['status'] = $status;
+        }
+
+        // 排序
+        $order = [];
+        if (!empty($this->request->get('order'))) {
+            $order_a = $this->request->get('order');
+            $order_a = explode(',', $order_a);
+            $order[$order_a[0]] = $order_a[1];
+        } else {
+            $order['sort'] = 'asc';
+            $order['id'] = 'asc';
+        }
+
+        // 查询所有分类
+        $categoryList = Db::name('product_category')
+            ->where($where)
+            ->order($order)
+            ->select()
+            ->toArray();
+
+        // 构建树形结构
+        $tree = $this->buildTree($categoryList);
+
+        $this->success('查询成功', $tree);
+    }
+
+    /**
+     * 构建树形结构
+     * @param array $items
+     * @param int $parentId
+     * @return array
+     */
+    private function buildTree(array $items, $parentId = 0)
+    {
+        $tree = [];
+        foreach ($items as $item) {
+            if ($item['parent_id'] == $parentId) {
+                $children = $this->buildTree($items, $item['id']);
+                if ($children) {
+                    $item['children'] = $children;
+                }
+                $tree[] = $item;
+            }
+        }
+        return $tree;
+    }
+
+    /**
+     * 新增/编辑分类
+     * @return void
+     */
+    public function edit()
+    {
+        $id = $this->request->post('id');
+        $name = $this->request->post('name');
+        $parentId = $this->request->post('parent_id', 0);
+        $icon = $this->request->post('icon', '');
+        $sort = $this->request->post('sort', 0);
+        $status = $this->request->post('status', 1);
+
+        if (empty($name)) {
+            $this->error(__('分类名称不能为空'));
+        }
+
+        // 验证分类名称是否已存在(同一父级下不能重复)
+        $checkName = Db::name('product_category')
+            ->where('name', $name)
+            ->where('parent_id', $parentId);
+
+        if (!empty($id)) {
+            $checkName->where('id', '<>', $id);
+        }
+
+        if ($checkName->find()) {
+            $this->error(__('同级分类下已存在相同名称'));
+        }
+
+        // 检查父级分类是否存在
+        if ($parentId > 0) {
+            $parentCategory = Db::name('product_category')
+                ->where('id', $parentId)
+                ->find();
+
+            if (!$parentCategory) {
+                $this->error(__('父级分类不存在'));
+            }
+
+            // 防止循环引用(不能将分类设置为自己的子分类)
+            if (!empty($id) && $parentId == $id) {
+                $this->error(__('不能将分类设置为自己的父级'));
+            }
+        }
+
+        $data = [
+            'name' => $name,
+            'parent_id' => $parentId,
+            'icon' => $icon,
+            'sort' => $sort,
+            'status' => $status
+        ];
+
+        try {
+            if (!empty($id)) {
+                // 编辑
+                $categoryInfo = Db::name('product_category')
+                    ->where('id', $id)
+                    ->find();
+
+                if (!$categoryInfo) {
+                    $this->error(__('分类不存在'));
+                }
+
+                $result = Db::name('product_category')
+                    ->where('id', $id)
+                    ->update($data);
+                $message = '编辑成功';
+            } else {
+                // 新增
+                $data['create_time'] = date('Y-m-d H:i:s');
+                $result = Db::name('product_category')
+                    ->insert($data);
+                $id = Db::name('product_category')->getLastInsID();
+                $message = '新增成功';
+            }
+
+            if ($result) {
+                $this->success($message, ['id' => $id]);
+            } else {
+                $this->error(__('操作失败'));
+            }
+        } catch (\Exception $e) {
+            $this->error('操作失败: ' . $e->getMessage());
+        }
+    }
+
+    /**
+     * 删除分类
+     * @return void
+     */
+    public function delete()
+    {
+        $id = $this->request->post('id');
+
+        if (empty($id)) {
+            $this->error(__('参数错误'));
+        }
+
+        // 检查是否有子分类
+        $hasChildren = Db::name('product_category')
+            ->where('parent_id', $id)
+            ->count();
+
+        if ($hasChildren > 0) {
+            $this->error(__('该分类下有子分类,请先删除子分类'));
+        }
+
+        // 检查是否有产品关联
+        $hasProduct = Db::name('product')
+            ->where('category_id', $id)
+            ->count();
+
+        if ($hasProduct > 0) {
+            $this->error(__('该分类下有关联的产品,请先删除或转移产品'));
+        }
+
+        try {
+            $result = Db::name('product_category')
+                ->where('id', $id)
+                ->delete();
+
+            if ($result) {
+                $this->success('删除成功');
+            } else {
+                $this->error(__('删除失败'));
+            }
+        } catch (\Exception $e) {
+            $this->error('删除失败: ' . $e->getMessage());
+        }
+    }
+
+    /**
+     * 分类详情
+     * @return void
+     */
+    public function detail()
+    {
+        $id = $this->request->get('id');
+
+        if (empty($id)) {
+            $this->error(__('参数错误'));
+        }
+
+        $categoryInfo = Db::name('product_category')
+            ->where('id', $id)
+            ->find();
+
+        if (!$categoryInfo) {
+            $this->error(__('分类不存在'));
+        }
+
+        // 获取父级分类信息
+        if ($categoryInfo['parent_id'] > 0) {
+            $parentInfo = Db::name('product_category')
+                ->where('id', $categoryInfo['parent_id'])
+                ->field('id, name')
+                ->find();
+            $categoryInfo['parent_info'] = $parentInfo;
+        } else {
+            $categoryInfo['parent_info'] = null;
+        }
+
+        // 统计子分类数量
+        $childrenCount = Db::name('product_category')
+            ->where('parent_id', $id)
+            ->count();
+        $categoryInfo['children_count'] = $childrenCount;
+
+        // 统计产品数量
+        $productCount = Db::name('product')
+            ->where('category_id', $id)
+            ->count();
+        $categoryInfo['product_count'] = $productCount;
+
+        $this->success('查询成功', $categoryInfo);
+    }
+
+    /**
+     * 批量更新分类状态
+     * @return void
+     */
+    public function batchStatus()
+    {
+        $ids = $this->request->post('ids');
+        $status = $this->request->post('status');
+
+        if (empty($ids) || empty($status)) {
+            $this->error(__('参数错误'));
+        }
+
+        if (!is_array($ids)) {
+            $ids = explode(',', $ids);
+        }
+
+        try {
+            $result = Db::name('product_category')
+                ->whereIn('id', $ids)
+                ->update([
+                    'status' => $status
+                ]);
+
+            if ($result) {
+                $statusText = $status == 1 ? '启用' : '禁用';
+                $this->success("成功{$statusText}{$result}个分类");
+            } else {
+                $this->error(__('操作失败'));
+            }
+        } catch (\Exception $e) {
+            $this->error('操作失败: ' . $e->getMessage());
+        }
+    }
+
+    /**
+     * 获取分类下拉选项(用于选择父级分类)
+     * @return void
+     */
+    public function selectOptions()
+    {
+        // 获取所有启用的分类
+        $categoryList = Db::name('product_category')
+            ->where('status', 1)
+            ->order('sort', 'asc')
+            ->order('id', 'asc')
+            ->select()
+            ->toArray();
+
+        // 构建树形结构用于前端展示
+        $tree = $this->buildTreeForSelect($categoryList);
+
+        $this->success('查询成功', $tree);
+    }
+
+    /**
+     * 构建用于下拉选择的树形结构
+     * @param array $items
+     * @param int $parentId
+     * @param int $level
+     * @return array
+     */
+    private function buildTreeForSelect(array $items, $parentId = 0, $level = 0)
+    {
+        $tree = [];
+        $prefix = str_repeat(' ', $level * 2);
+
+        foreach ($items as $item) {
+            if ($item['parent_id'] == $parentId) {
+                $item['label'] = ($level > 0 ? $prefix . '├─ ' : '') . $item['name'];
+                $item['value'] = $item['id'];
+
+                $children = $this->buildTreeForSelect($items, $item['id'], $level + 1);
+                if ($children) {
+                    $item['children'] = $children;
+                }
+                $tree[] = $item;
+            }
+        }
+        return $tree;
+    }
+
+    /**
+     * 获取分类状态选项
+     * @return void
+     */
+    public function statusOptions()
+    {
+        $options = [
+            ['value' => 1, 'label' => '启用'],
+            ['value' => 2, 'label' => '禁用']
+        ];
+
+        $this->success('获取成功', $options);
+    }
+
+    /**
+     * 移动分类排序
+     * @return void
+     */
+    public function moveSort()
+    {
+        $id = $this->request->post('id');
+        $direction = $this->request->post('direction'); // up:上移, down:下移
+
+        if (empty($id) || empty($direction)) {
+            $this->error(__('参数错误'));
+        }
+
+        $categoryInfo = Db::name('product_category')
+            ->where('id', $id)
+            ->find();
+
+        if (!$categoryInfo) {
+            $this->error(__('分类不存在'));
+        }
+
+        try {
+            Db::startTrans();
+
+            // 获取同级分类
+            $siblings = Db::name('product_category')
+                ->where('parent_id', $categoryInfo['parent_id'])
+                ->order('sort', 'asc')
+                ->order('id', 'asc')
+                ->select()
+                ->toArray();
+
+            // 找到当前分类在列表中的位置
+            $currentIndex = -1;
+            foreach ($siblings as $index => $sibling) {
+                if ($sibling['id'] == $id) {
+                    $currentIndex = $index;
+                    break;
+                }
+            }
+
+            if ($currentIndex === -1) {
+                throw new \Exception('分类在同级中不存在');
+            }
+
+            if ($direction == 'up' && $currentIndex > 0) {
+                // 上移:与上一个交换排序值
+                $prevSibling = $siblings[$currentIndex - 1];
+
+                Db::name('product_category')
+                    ->where('id', $id)
+                    ->update(['sort' => $prevSibling['sort']]);
+
+                Db::name('product_category')
+                    ->where('id', $prevSibling['id'])
+                    ->update(['sort' => $categoryInfo['sort']]);
+
+            } elseif ($direction == 'down' && $currentIndex < count($siblings) - 1) {
+                // 下移:与下一个交换排序值
+                $nextSibling = $siblings[$currentIndex + 1];
+
+                Db::name('product_category')
+                    ->where('id', $id)
+                    ->update(['sort' => $nextSibling['sort']]);
+
+                Db::name('product_category')
+                    ->where('id', $nextSibling['id'])
+                    ->update(['sort' => $categoryInfo['sort']]);
+            } else {
+                // 已经在最前或最后,无需移动
+                Db::rollback();
+                $this->success('无需移动');
+            }
+
+            Db::commit();
+            $this->success('移动成功');
+
+        } catch (\Exception $e) {
+            Db::rollback();
+            $this->error('移动失败: ' . $e->getMessage());
+        }
+    }
+}

+ 1 - 1
app/admin/controller/user_center/User.php → app/admin/controller/userCenter/User.php

@@ -1,6 +1,6 @@
 <?php
 
-namespace app\admin\controller\user_center;
+namespace app\admin\controller\userCenter;
 use app\common\controller\Backend;
 use think\facade\Db;