first version
This commit is contained in:
@@ -0,0 +1,106 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Product;
|
||||
|
||||
use App\Exceptions\RepositoryException;
|
||||
use App\Http\Requests\Product\ProductCategoryFormRequest;
|
||||
use App\Models\ProductCategoryModel;
|
||||
use App\Models\ProductModel;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
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;
|
||||
|
||||
/**
|
||||
* 商品分类管理(多级分类:蔬菜/水果/其他)
|
||||
*/
|
||||
#[RequestAttribute('/product/category', 'product.category')]
|
||||
class ProductCategoryController extends BaseController
|
||||
{
|
||||
/** 分类树列表(后端组装 children,前端树表展示) */
|
||||
#[GetRoute(authorize: 'query')]
|
||||
public function query(): JsonResponse
|
||||
{
|
||||
return $this->success(ProductCategoryModel::getTreeData());
|
||||
}
|
||||
|
||||
/** 级联选项(商品表单分类下拉、对账筛选用;仅启用分类) */
|
||||
#[GetRoute('/tree', 'query')]
|
||||
public function tree(): JsonResponse
|
||||
{
|
||||
return $this->success(ProductCategoryModel::getTreeData(onlyEnabled: true));
|
||||
}
|
||||
|
||||
/** 创建分类 */
|
||||
#[PostRoute(authorize: 'create')]
|
||||
public function create(ProductCategoryFormRequest $request): JsonResponse
|
||||
{
|
||||
$validated = $request->validated();
|
||||
$parentId = (int) $validated['parent_id'];
|
||||
if ($parentId > 0 && ! ProductCategoryModel::whereKey($parentId)->exists()) {
|
||||
throw new RepositoryException('父级分类不存在');
|
||||
}
|
||||
ProductCategoryModel::create($validated);
|
||||
return $this->success();
|
||||
}
|
||||
|
||||
/** 编辑分类(防自引用成环) */
|
||||
#[PutRoute(route: '/{id}', authorize: 'update', where: ['id' => '[0-9]+'])]
|
||||
public function update(int $id, ProductCategoryFormRequest $request): JsonResponse
|
||||
{
|
||||
$model = ProductCategoryModel::find($id);
|
||||
if (empty($model)) {
|
||||
throw new RepositoryException('分类不存在');
|
||||
}
|
||||
$validated = $request->validated();
|
||||
$this->assertNoCycle($id, (int) $validated['parent_id']);
|
||||
$model->update($validated);
|
||||
return $this->success();
|
||||
}
|
||||
|
||||
/** 删除分类(有子分类或挂载商品时拒绝) */
|
||||
#[DeleteRoute(route: '/{id}', authorize: 'delete', where: ['id' => '[0-9]+'])]
|
||||
public function delete(int $id): JsonResponse
|
||||
{
|
||||
$model = ProductCategoryModel::find($id);
|
||||
if (empty($model)) {
|
||||
throw new RepositoryException('分类不存在');
|
||||
}
|
||||
if (ProductCategoryModel::where('parent_id', $id)->exists()) {
|
||||
throw new RepositoryException('该分类下存在子分类,无法删除');
|
||||
}
|
||||
if (ProductModel::where('category_id', $id)->exists()) {
|
||||
throw new RepositoryException('该分类下存在商品,无法删除');
|
||||
}
|
||||
$model->delete();
|
||||
return $this->success();
|
||||
}
|
||||
|
||||
/**
|
||||
* 沿父链向上检查,防止 parent_id 指向自身或子孙分类形成环
|
||||
*/
|
||||
private function assertNoCycle(int $id, int $parentId): void
|
||||
{
|
||||
if ($parentId === 0) {
|
||||
return;
|
||||
}
|
||||
if ($parentId === $id) {
|
||||
throw new RepositoryException('父级分类不能是自身');
|
||||
}
|
||||
$cursor = $parentId;
|
||||
$guard = 0;
|
||||
while ($cursor > 0 && $guard++ < 100) {
|
||||
$next = ProductCategoryModel::whereKey($cursor)->value('parent_id');
|
||||
if ($next === null) {
|
||||
throw new RepositoryException('父级分类不存在');
|
||||
}
|
||||
if ((int) $next === $id) {
|
||||
throw new RepositoryException('父级分类不能是子级分类,会形成循环');
|
||||
}
|
||||
$cursor = (int) $next;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,240 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Product;
|
||||
|
||||
use App\Exceptions\RepositoryException;
|
||||
use App\Http\Requests\Product\BatchPriceRequest;
|
||||
use App\Http\Requests\Product\ProductFormRequest;
|
||||
use App\Models\CustomerLevelModel;
|
||||
use App\Models\NoticeModel;
|
||||
use App\Models\ProductModel;
|
||||
use App\Models\ProductPriceModel;
|
||||
use App\Models\StoreModel;
|
||||
use App\Models\UserModel;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
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;
|
||||
|
||||
/**
|
||||
* 商品档案管理(A1 商品列表 / A2 价格矩阵与批量调价 / 多等级价格体系)
|
||||
*/
|
||||
#[RequestAttribute('/product/goods', 'product.goods')]
|
||||
class ProductController extends BaseController
|
||||
{
|
||||
protected array $searchField = [
|
||||
'name' => 'like',
|
||||
'category_id' => '=',
|
||||
'supplier_id' => '=',
|
||||
'status' => '=',
|
||||
];
|
||||
|
||||
protected array $quickSearchField = ['name', 'spec'];
|
||||
|
||||
/** A1 商品列表(含分类/供应商/各等级价格) */
|
||||
#[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', 'prices.level:id,name'])
|
||||
)
|
||||
->orderBy('sort')
|
||||
->orderBy('id', 'desc')
|
||||
->paginate($pageSize)
|
||||
->toArray();
|
||||
return $this->success($data);
|
||||
}
|
||||
|
||||
/** 创建商品(事务内建商品 + 同步等级价格) */
|
||||
#[PostRoute(authorize: 'create')]
|
||||
public function create(ProductFormRequest $request): JsonResponse
|
||||
{
|
||||
$validated = $request->validated();
|
||||
$prices = $validated['prices'] ?? [];
|
||||
unset($validated['prices']);
|
||||
|
||||
$product = DB::transaction(function () use ($validated, $prices) {
|
||||
$product = ProductModel::create($validated);
|
||||
foreach ($prices as $row) {
|
||||
ProductPriceModel::create([
|
||||
'product_id' => $product->id,
|
||||
'level_id' => (int) $row['level_id'],
|
||||
'price' => $row['price'],
|
||||
]);
|
||||
}
|
||||
return $product;
|
||||
});
|
||||
|
||||
return $this->success(['id' => $product->id]);
|
||||
}
|
||||
|
||||
/** 编辑商品(prices 按 level_id upsert,删除已移除的等级行;未提交 prices 键时保持原价) */
|
||||
#[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('商品不存在');
|
||||
}
|
||||
$validated = $request->validated();
|
||||
$prices = $validated['prices'] ?? [];
|
||||
unset($validated['prices']);
|
||||
|
||||
DB::transaction(function () use ($product, $validated, $prices, $request) {
|
||||
$product->update($validated);
|
||||
if ($request->has('prices')) {
|
||||
$levelIds = [];
|
||||
foreach ($prices as $row) {
|
||||
$levelId = (int) $row['level_id'];
|
||||
$levelIds[] = $levelId;
|
||||
ProductPriceModel::updateOrCreate(
|
||||
['product_id' => $product->id, 'level_id' => $levelId],
|
||||
['price' => $row['price']],
|
||||
);
|
||||
}
|
||||
$product->prices()->whereNotIn('level_id', $levelIds)->delete();
|
||||
}
|
||||
});
|
||||
|
||||
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('商品不存在');
|
||||
}
|
||||
DB::transaction(function () use ($product) {
|
||||
$product->prices()->delete();
|
||||
$product->delete();
|
||||
});
|
||||
return $this->success();
|
||||
}
|
||||
|
||||
/**
|
||||
* A2 价格矩阵:行=商品(支持 category_id / keyword 过滤),列=全部启用等级,值=price(缺失为 null)
|
||||
*/
|
||||
#[GetRoute('/priceMatrix', 'query')]
|
||||
public function priceMatrix(Request $request): JsonResponse
|
||||
{
|
||||
$query = ProductModel::query()->with('prices:id,product_id,level_id,price');
|
||||
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 . '%');
|
||||
});
|
||||
}
|
||||
$products = $query->orderBy('sort')->orderBy('id')->get();
|
||||
|
||||
$levels = CustomerLevelModel::query()
|
||||
->where('status', CustomerLevelModel::STATUS_NORMAL)
|
||||
->orderBy('sort')
|
||||
->get(['id', 'name']);
|
||||
|
||||
$rows = $products->map(function (ProductModel $product) use ($levels) {
|
||||
$priceMap = $product->prices->keyBy('level_id');
|
||||
$row = [
|
||||
'id' => $product->id,
|
||||
'name' => $product->name,
|
||||
'spec' => $product->spec,
|
||||
'unit' => $product->unit,
|
||||
];
|
||||
foreach ($levels as $level) {
|
||||
$row['price_' . $level->id] = isset($priceMap[$level->id])
|
||||
? (float) $priceMap[$level->id]->price
|
||||
: null;
|
||||
}
|
||||
return $row;
|
||||
});
|
||||
|
||||
return $this->success([
|
||||
'levels' => $levels->toArray(),
|
||||
'rows' => $rows->toArray(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* A2 批量调价:事务写入,写完后给受影响门店生成 Notice(type=price)
|
||||
*/
|
||||
#[PutRoute('/batchPrice', 'batchPrice')]
|
||||
public function batchPrice(BatchPriceRequest $request): JsonResponse
|
||||
{
|
||||
$updates = $request->validated('updates');
|
||||
|
||||
DB::transaction(function () use ($updates) {
|
||||
$productIds = [];
|
||||
$levelIds = [];
|
||||
foreach ($updates as $row) {
|
||||
ProductPriceModel::updateOrCreate(
|
||||
['product_id' => (int) $row['product_id'], 'level_id' => (int) $row['level_id']],
|
||||
['price' => $row['price']],
|
||||
);
|
||||
$productIds[(int) $row['product_id']] = true;
|
||||
$levelIds[(int) $row['level_id']] = true;
|
||||
}
|
||||
|
||||
$productNames = ProductModel::whereIn('id', array_keys($productIds))
|
||||
->pluck('name')
|
||||
->implode('、');
|
||||
$content = mb_substr('以下商品价格已调整:' . $productNames . ',下次登录小程序后按新价格显示', 0, 500);
|
||||
|
||||
// 受影响门店:客户等级在本次调价等级范围内的正常门店,通知其绑定的正常用户
|
||||
$userIds = UserModel::query()
|
||||
->where('type', UserModel::TYPE_STORE)
|
||||
->where('status', UserModel::STATUS_NORMAL)
|
||||
->whereIn('store_id', function ($q) use ($levelIds) {
|
||||
$q->select('id')
|
||||
->from('store')
|
||||
->where('status', StoreModel::STATUS_NORMAL)
|
||||
->whereIn('level_id', array_keys($levelIds));
|
||||
})
|
||||
->pluck('id');
|
||||
|
||||
foreach ($userIds as $userId) {
|
||||
NoticeModel::create([
|
||||
'user_id' => $userId,
|
||||
'type' => NoticeModel::TYPE_PRICE,
|
||||
'title' => '商品价格变更',
|
||||
'content' => $content,
|
||||
'data' => [
|
||||
'product_ids' => array_keys($productIds),
|
||||
'level_ids' => array_keys($levelIds),
|
||||
],
|
||||
'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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user