Files
xin-procurement/app/Http/Controllers/Mini/ProductController.php
T
2026-08-10 08:55:22 +08:00

106 lines
3.9 KiB
PHP

<?php
namespace App\Http\Controllers\Mini;
use App\Exceptions\RepositoryException;
use App\Models\ProductCategoryModel;
use App\Models\ProductModel;
use App\Models\ProductPriceModel;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Modules\AnnoRoute\Attribute\GetRoute;
use Modules\AnnoRoute\Attribute\RequestAttribute;
/**
* 小程序商品(分类树 + 列表)
*
* 商品浏览仅需登录:未绑定门店/门店未设客户等级的用户也可查看商品,仅价格不可见(price=null);
* 加购、下单仍由购物车/订单前置校验拦截,要求绑定门店并已设客户等级。
*/
#[RequestAttribute('/mini', 'mini', authGuard: 'users')]
class ProductController extends BaseMiniController
{
/** 分类树(仅含上架商品的分类及其祖先,保证树结构完整) */
#[GetRoute('/product/categories', authorize: true)]
public function categories(): JsonResponse
{
$activeCategoryIds = ProductModel::query()
->where('status', ProductModel::STATUS_ON)
->distinct()
->pluck('category_id')
->map(static fn ($id) => (int) $id)
->filter(static fn (int $id) => $id > 0);
$categories = ProductCategoryModel::query()
->where('status', ProductCategoryModel::STATUS_NORMAL)
->get()
->keyBy('id');
// 保留有上架商品的分类 + 其全部祖先
$keep = [];
foreach ($activeCategoryIds as $categoryId) {
$cursor = $categoryId;
$guard = 0;
while ($cursor > 0 && $guard++ < 20 && $categories->has($cursor)) {
$keep[$cursor] = true;
$cursor = (int) $categories[$cursor]->parent_id;
}
}
$filtered = array_values(array_filter(
$categories->toArray(),
static fn (array $item) => isset($keep[$item['id']])
));
return $this->success(ProductCategoryModel::buildTree($filtered));
}
/**
* 商品列表:价格取当前门店客户等级价;
* 未绑定门店/门店未设客户等级 → 仍可浏览,price 为 null(不可见价格,加购/下单另由前置校验拦截);
* ?category_id=&keyword=&page=&pageSize=
*/
#[GetRoute('/product/list', authorize: true)]
public function products(Request $request): JsonResponse
{
$user = $this->currentUser($request);
$levelId = $this->boundStore($user)?->level_id ?? 0;
$query = ProductModel::query()
->where('status', ProductModel::STATUS_ON)
->with('category:id,name');
if ($levelId > 0) {
$query->with(['prices' => static fn ($q) => $q->where('level_id', $levelId)]);
}
$categoryId = (int) $request->input('category_id', 0);
if ($categoryId > 0) {
$query->where('category_id', $categoryId);
}
$keyword = trim((string) $request->input('keyword', ''));
if ($keyword !== '') {
$query->where(static function ($q) use ($keyword) {
$q->where('name', 'like', '%' . $keyword . '%')
->orWhere('spec', 'like', '%' . $keyword . '%');
});
}
$pageSize = (int) $request->input('pageSize', 10);
$data = $query->orderBy('sort')
->orderBy('id')
->paginate($pageSize)
->toArray();
// 扁平化价格:prices[0].actual_price → price(访问器经 toArray 自动输出换算后实际价;
// 未绑定门店或未设等级价为 null——未加载 prices 时 ?? null 兜底)
// unset prices 同时移除 price_type/percent,门店端无法反推成本;cost_price 已被 $hidden 过滤
foreach ($data['data'] as &$row) {
$row['price'] = $row['prices'][0]['actual_price'] ?? null;
unset($row['prices']);
}
return $this->success($data);
}
}