Files
xin-procurement/app/Http/Controllers/Product/ProductController.php
T
2026-08-23 14:26:37 +08:00

309 lines
12 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
namespace App\Http\Controllers\Product;
use App\Exceptions\RepositoryException;
use App\Exports\ProductExport;
use App\Http\Requests\Product\BatchPriceRequest;
use App\Http\Requests\Product\ProductFormRequest;
use App\Imports\ProductImport;
use App\Models\CustomerLevelModel;
use App\Models\NoticeModel;
use App\Models\ProductModel;
use App\Models\StoreModel;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
use Maatwebsite\Excel\Facades\Excel;
use Modules\AnnoRoute\Attribute\DeleteRoute;
use Modules\AnnoRoute\Attribute\GetRoute;
use Modules\AnnoRoute\Attribute\PostRoute;
use Modules\AnnoRoute\Attribute\PutRoute;
use Modules\AnnoRoute\Attribute\RequestAttribute;
use Modules\Common\Http\Controllers\BaseController;
use Modules\SystemTool\Services\SysFileService;
use Symfony\Component\HttpFoundation\Response;
/**
* 商品档案管理(售价 = 成本价 × (100 + 客户等级上浮比例) / 100,不再维护等级价格行)
*/
#[RequestAttribute('/product/goods', 'product.goods')]
class ProductController extends BaseController
{
protected array $searchField = [
'name' => '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 批量调价:批量调整成本价(等级售价随之按上浮比例联动),事务写入,
* 写完后给全部正常门店的用户生成 Noticetype=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<int, CustomerLevelModel>
*/
private function enabledLevels(): \Illuminate\Database\Eloquent\Collection
{
return CustomerLevelModel::query()
->where('status', CustomerLevelModel::STATUS_NORMAL)
->orderBy('sort')
->get(['id', 'name', 'percent']);
}
}