'like', 'category_id' => '=', 'supplier_id' => '=', 'market' => 'like', 'status' => '=', ]; protected array $quickSearchField = ['name', 'spec']; /** A1 商品列表(含分类/供应商;prices 为按启用等级上浮比例换算的展示价;cost_price 在 $hidden 中,后台列表需显式恢复) */ #[GetRoute(authorize: 'query')] public function query(Request $request): JsonResponse { $params = $request->all(); $pageSize = $params['pageSize'] ?? 10; $data = $this->buildSearch( $params, ProductModel::query()->with(['category:id,name', 'supplier:id,name']) ) ->orderBy('sort') ->orderBy('id', 'desc') ->paginate($pageSize); $data->getCollection()->makeVisible('cost_price'); // 按启用等级直接换算展示价(无等级价格表,价格由成本价 × 等级上浮比例得出) $levels = $this->enabledLevels(); $data->getCollection()->transform(static function (ProductModel $product) use ($levels) { $row = $product->toArray(); $row['prices'] = $levels->map(static fn (CustomerLevelModel $level) => [ 'level_id' => $level->id, 'level' => ['id' => $level->id, 'name' => $level->name], 'percent' => (float) $level->percent, 'price' => CustomerLevelModel::calcLevelPrice($product->cost_price, $level->percent), ])->values()->all(); return $row; }); return $this->success($data->toArray()); } /** 上传商品分类图片文件 */ #[PostRoute('/upload', 'create')] public function uploadImage(Request $request, SysFileService $service): JsonResponse { $data = $request->validate(['file' => 'required|file']); $result = $service->upload( $data['file'], 10, 20, Auth::id() ); return $this->success($result); } /** 创建商品 */ #[PostRoute(authorize: 'create')] public function create(ProductFormRequest $request): JsonResponse { $product = ProductModel::create($request->validated()); return $this->success(['id' => $product->id]); } /** * 商品导出(列格式与导入模板一致,导出文件修改后可直接重新导入); * template=1 时仅输出列头 + 示例行(导入模板) */ #[GetRoute('/export', 'export')] public function export(Request $request): Response { $template = $request->boolean('template'); $categoryId = max(0, (int) $request->query('category_id', 0)); $filename = $template ? '商品导入模板.xlsx' : '商品列表_' . now()->format('Ymd_His') . '.xlsx'; return Excel::download(new ProductExport($categoryId, $template), $filename); } /** * Excel 批量导入商品:整表校验,有错全部不导入; * 分类按名称/路径匹配末级分类,供应商按名称匹配、不存在自动创建;一律新增商品 */ #[PostRoute('/import', 'import')] public function import(Request $request): JsonResponse { $data = $request->validate([ 'file' => 'required|file|mimes:xlsx,xls|max:10240', ], [ 'file.required' => '请选择要导入的 Excel 文件', 'file.mimes' => '仅支持 xlsx/xls 格式的 Excel 文件', 'file.max' => '文件大小不能超过 10MB', ]); $import = new ProductImport(); Excel::import($import, $data['file']); if ($import->errors !== []) { return $this->error( ['errors' => $import->errors], '共发现 ' . count($import->errors) . ' 处数据错误,请修正后重新导入' ); } $msg = '成功导入 ' . $import->created . ' 件商品'; if ($import->suppliersCreated !== []) { $msg .= ',自动创建供应商:' . implode('、', $import->suppliersCreated); } return $this->success(['created' => $import->created], $msg); } /** 编辑商品 */ #[PutRoute(route: '/{id}', authorize: 'update', where: ['id' => '[0-9]+'])] public function update(int $id, ProductFormRequest $request): JsonResponse { $product = ProductModel::find($id); if (empty($product)) { throw new RepositoryException('商品不存在'); } $product->update($request->validated()); return $this->success(); } /** 删除商品(软删除) */ #[DeleteRoute(route: '/{id}', authorize: 'delete', where: ['id' => '[0-9]+'])] public function delete(int $id): JsonResponse { $product = ProductModel::find($id); if (empty($product)) { throw new RepositoryException('商品不存在'); } $product->delete(); return $this->success(); } /** 批量删除商品(软删除),复用 delete 权限点 */ #[DeleteRoute('/batch', 'delete')] public function batchDelete(Request $request): JsonResponse { $data = $request->validate([ 'ids' => 'required|array|min:1', 'ids.*' => 'integer|distinct', ], [ 'ids.required' => '请选择要删除的商品', 'ids.min' => '请选择要删除的商品', 'ids.*.integer' => '商品 ID 格式错误', 'ids.*.distinct' => '存在重复的商品', ]); ProductModel::whereIn('id', $data['ids'])->delete(); return $this->success(); } /** * A2 价格矩阵:行=商品(支持 category_id / keyword 过滤 + page/pageSize 服务端分页), * 列=全部启用等级,值=按等级上浮比例换算的售价(成本价未设置为 null);仅成本价可编辑 */ #[GetRoute('/priceMatrix', 'query')] public function priceMatrix(Request $request): JsonResponse { $query = ProductModel::query(); if (($categoryId = (int) $request->input('category_id', 0)) > 0) { $query->where('category_id', $categoryId); } $keyword = trim((string) $request->input('keyword', '')); if ($keyword !== '') { $query->where(function ($q) use ($keyword) { $q->where('name', 'like', '%' . $keyword . '%') ->orWhere('spec', 'like', '%' . $keyword . '%'); }); } $pageSize = max(1, min(100, (int) $request->input('pageSize', 20))); $page = max(1, (int) $request->input('page', 1)); $products = $query->orderBy('sort')->orderBy('id')->paginate($pageSize, ['*'], 'page', $page); $levels = $this->enabledLevels(); $rows = $products->getCollection()->map(static function (ProductModel $product) use ($levels) { $row = [ 'id' => $product->id, 'name' => $product->name, 'spec' => $product->spec, 'unit' => $product->unit, 'cost_price' => (float) $product->cost_price, ]; foreach ($levels as $level) { $row['price_' . $level->id] = $product->cost_price > 0 ? (float) CustomerLevelModel::calcLevelPrice($product->cost_price, $level->percent) : null; } return $row; }); return $this->success([ 'levels' => $levels->map(static fn (CustomerLevelModel $level) => [ 'id' => $level->id, 'name' => $level->name, 'percent' => (float) $level->percent, ])->values()->all(), 'rows' => $rows->values()->toArray(), 'total' => $products->total(), ]); } /** * A2 批量调价:批量调整成本价(等级售价随之按上浮比例联动),事务写入, * 写完后给全部正常门店的用户生成 Notice(type=price) */ #[PutRoute('/batchPrice', 'batchPrice')] public function batchPrice(BatchPriceRequest $request): JsonResponse { $updates = $request->validated('updates'); DB::transaction(function () use ($updates) { $productIds = []; foreach ($updates as $row) { $productId = (int) $row['product_id']; $productIds[$productId] = true; ProductModel::whereKey($productId)->update(['cost_price' => $row['cost_price']]); } $productNames = ProductModel::whereIn('id', array_keys($productIds)) ->pluck('name') ->implode('、'); $content = mb_substr('以下商品价格已调整:' . $productNames . ',下次登录小程序后按新价格显示', 0, 500); // 成本价变更影响所有等级的售价:通知全部正常门店 $storeIds = StoreModel::query() ->where('status', StoreModel::STATUS_NORMAL) ->pluck('id'); foreach ($storeIds as $storeId) { NoticeModel::create([ 'store_id' => $storeId, 'type' => NoticeModel::TYPE_PRICE, 'title' => '商品价格变更', 'content' => $content, 'data' => [ 'product_ids' => array_keys($productIds), ], 'is_read' => NoticeModel::UNREAD, ]); } }); return $this->success(); } /** 商品下拉选项(仅上架,下单等场景用) */ #[GetRoute('/options', 'query')] public function options(Request $request): JsonResponse { $query = ProductModel::query()->where('status', ProductModel::STATUS_ON); $keyword = trim((string) $request->input('keyword', '')); if ($keyword !== '') { $query->where('name', 'like', '%' . $keyword . '%'); } $data = $query->orderBy('sort') ->get(['id', 'name', 'spec', 'unit']) ->toArray(); return $this->success($data); } /** * 启用中的客户等级(列表/矩阵共用的等级列来源) * * @return \Illuminate\Database\Eloquent\Collection */ private function enabledLevels(): \Illuminate\Database\Eloquent\Collection { return CustomerLevelModel::query() ->where('status', CustomerLevelModel::STATUS_NORMAL) ->orderBy('sort') ->get(['id', 'name', 'percent']); } }