318 lines
11 KiB
PHP
318 lines
11 KiB
PHP
<?php
|
||
|
||
namespace App\Service\business;
|
||
|
||
use App\BaseApp\BaseService;
|
||
use App\Models\business\CatalogueModel;
|
||
use App\Models\business\PackageItemModel;
|
||
use App\Models\business\PackageModel;
|
||
use App\Models\business\PriceSheetModel;
|
||
use App\Service\common\MediaUrlService;
|
||
use Illuminate\Support\Facades\DB;
|
||
|
||
/**
|
||
* 后台套餐
|
||
*
|
||
* 明细必须带规格。保存明细时后端重算原价;套餐报价若前端没传,就按原价差额自动补。
|
||
*/
|
||
class PackageService extends BaseService
|
||
{
|
||
private MediaUrlService $media;
|
||
|
||
private PackageQuoteService $quote;
|
||
|
||
private PriceService $price;
|
||
|
||
public function __construct()
|
||
{
|
||
parent::__construct();
|
||
$this->model = PackageModel::class;
|
||
$this->selectField = [
|
||
'id', 'name', 'cover', 'subtitle', 'remark',
|
||
'original_amount', 'package_amount', 'is_hot', 'sort', 'status',
|
||
'created_at', 'updated_at',
|
||
];
|
||
$this->queryField = [
|
||
'name' => 'like',
|
||
'status' => '=',
|
||
'is_hot' => '=',
|
||
];
|
||
$this->orderBy = ['name' => 'sort', 'sort' => 'asc'];
|
||
$this->media = MediaUrlService::getInstance();
|
||
$this->quote = PackageQuoteService::getInstance();
|
||
$this->price = PriceService::getInstance();
|
||
}
|
||
|
||
public function list(): array
|
||
{
|
||
$result = $this->getPageList();
|
||
$ids = array_column($result['items'], 'id');
|
||
$counts = PackageItemModel::whereIn('package_id', $ids)
|
||
->where('deleted_at', 0)
|
||
->selectRaw('package_id, count(*) as total')
|
||
->groupBy('package_id')
|
||
->get()
|
||
->keyBy('package_id');
|
||
foreach ($result['items'] as &$item) {
|
||
$item['cover'] = $this->media->toPublic($item['cover'] ?? '');
|
||
$item['item_count'] = (int) ($counts[$item['id']]['total'] ?? 0);
|
||
$item = $this->withAmountText($item);
|
||
}
|
||
unset($item);
|
||
return $result;
|
||
}
|
||
|
||
public function option(): mixed
|
||
{
|
||
$this->optionField = ['id', 'name'];
|
||
return $this->getOption();
|
||
}
|
||
|
||
public function detail($id): mixed
|
||
{
|
||
$info = PackageModel::with([
|
||
'items' => fn ($query) => $query->where('deleted_at', 0)->orderBy('sort')->orderBy('id'),
|
||
'items.catalogue',
|
||
'items.priceSheet',
|
||
])->where('id', $id)->where('deleted_at', 0)->first();
|
||
if (empty($info)) {
|
||
$this->utils->errorThrow('套餐不存在');
|
||
}
|
||
$info = $info->toArray();
|
||
$info['cover'] = $this->media->toPublic($info['cover'] ?? '');
|
||
foreach ($info['items'] as &$item) {
|
||
$item['title'] = $item['catalogue']['title'] ?? '';
|
||
$item['cover'] = $this->media->toPublic($item['catalogue']['cover'] ?? '');
|
||
$item['specification'] = $item['price_sheet']['specification'] ?? '';
|
||
$item['dimension'] = $item['price_sheet']['dimension'] ?? '';
|
||
$item['unit_price_text'] = $this->price->centsToYuan((int) ($item['unit_price'] ?? 0));
|
||
}
|
||
unset($item);
|
||
return $this->withAmountText($info);
|
||
}
|
||
|
||
public function create($params): mixed
|
||
{
|
||
$row = $this->normalizeMain($params);
|
||
$row['created_at'] = time();
|
||
$id = PackageModel::insertGetId($row);
|
||
if (!empty($params['items']) && is_array($params['items'])) {
|
||
$this->replaceItems((int) $id, $params['items'], $params);
|
||
}
|
||
return $this->detail($id);
|
||
}
|
||
|
||
public function update($id, $params): mixed
|
||
{
|
||
$row = $this->normalizeMain($params, (int) $id);
|
||
$this->save($id, $row);
|
||
return $this->detail($id);
|
||
}
|
||
|
||
public function delete($ids): mixed
|
||
{
|
||
$ids = (array) $ids;
|
||
$now = time();
|
||
PackageModel::whereIn('id', $ids)->update(['deleted_at' => $now, 'updated_at' => $now]);
|
||
PackageItemModel::whereIn('package_id', $ids)->update(['deleted_at' => $now, 'updated_at' => $now]);
|
||
return true;
|
||
}
|
||
|
||
public function status($id, $status): mixed
|
||
{
|
||
return $this->save($id, ['status' => (int) $status]);
|
||
}
|
||
|
||
/**
|
||
* 搭配搜索:商品封面 + 规格报价。
|
||
* 单价用和保存时同一套 resolveUnitPrice,没有数字报价的规格标成不可选,避免搜到却存不进去。
|
||
*/
|
||
public function searchCatalog(): array
|
||
{
|
||
$keyword = trim((string) request()->input('keyword', ''));
|
||
$limit = max(1, min(50, (int) request()->input('limit', 20)));
|
||
|
||
$query = CatalogueModel::query()
|
||
->where('deleted_at', 0)
|
||
->with([
|
||
'priceSheet' => fn ($q) => $q->where('deleted_at', 0)->orderBy('id'),
|
||
]);
|
||
if ($keyword !== '') {
|
||
$like = '%' . $keyword . '%';
|
||
$query->where(function ($inner) use ($like) {
|
||
$inner->where('title', 'like', $like)
|
||
->orWhere('identifier', 'like', $like)
|
||
->orWhere('alias', 'like', $like);
|
||
});
|
||
}
|
||
|
||
$rows = $query->orderByDesc('id')->limit($limit)->get()->toArray();
|
||
$items = [];
|
||
foreach ($rows as $row) {
|
||
$sheets = [];
|
||
foreach ($row['price_sheet'] ?? [] as $sheet) {
|
||
$unit = $this->price->resolveUnitPrice((string) ($sheet['routine'] ?? ''), 'routine', 1);
|
||
$sheets[] = [
|
||
'id' => (int) ($sheet['id'] ?? 0),
|
||
'specification' => (string) ($sheet['specification'] ?? ''),
|
||
'dimension' => (string) ($sheet['dimension'] ?? ''),
|
||
'unit_price' => $unit,
|
||
'unit_price_text' => $this->price->centsToYuan($unit),
|
||
'selectable' => $unit > 0,
|
||
];
|
||
}
|
||
$items[] = [
|
||
'id' => (int) ($row['id'] ?? 0),
|
||
'title' => (string) ($row['title'] ?? ''),
|
||
'identifier' => (string) ($row['identifier'] ?? ''),
|
||
'cover' => $this->media->toPublic($row['cover'] ?? ''),
|
||
'sheets' => $sheets,
|
||
];
|
||
}
|
||
return ['items' => $items];
|
||
}
|
||
|
||
/**
|
||
* 整表替换搭配明细,并按原价差额补套餐报价
|
||
*/
|
||
public function saveItems(array $params): mixed
|
||
{
|
||
$id = (int) ($params['id'] ?? 0);
|
||
if ($id <= 0) {
|
||
$this->utils->errorThrow('请选择套餐');
|
||
}
|
||
$pkg = PackageModel::where('id', $id)->where('deleted_at', 0)->first();
|
||
if (empty($pkg)) {
|
||
$this->utils->errorThrow('套餐不存在');
|
||
}
|
||
$this->replaceItems($id, $params['items'] ?? [], $params);
|
||
return $this->detail($id);
|
||
}
|
||
|
||
/**
|
||
* 清洗主表字段。package_amount 按元进来,转成分;没传报价时新建默认等于原价。
|
||
*/
|
||
private function normalizeMain(array $params, int $id = 0): array
|
||
{
|
||
$row = [
|
||
'updated_at' => time(),
|
||
];
|
||
if (array_key_exists('name', $params)) {
|
||
$name = trim((string) $params['name']);
|
||
if ($name === '') {
|
||
$this->utils->errorThrow('请填写套餐名称');
|
||
}
|
||
$row['name'] = $name;
|
||
} elseif ($id === 0) {
|
||
$this->utils->errorThrow('请填写套餐名称');
|
||
}
|
||
if (array_key_exists('cover', $params)) {
|
||
$row['cover'] = $this->media->firstOf($params['cover']);
|
||
}
|
||
foreach (['subtitle', 'remark'] as $field) {
|
||
if (array_key_exists($field, $params)) {
|
||
$row[$field] = (string) $params[$field];
|
||
}
|
||
}
|
||
foreach (['is_hot', 'sort', 'status'] as $field) {
|
||
if (array_key_exists($field, $params)) {
|
||
$row[$field] = (int) $params[$field];
|
||
}
|
||
}
|
||
if (array_key_exists('package_amount', $params)) {
|
||
$row['package_amount'] = max(0, $this->price->yuanToCents($params['package_amount']));
|
||
}
|
||
if ($id === 0 && !isset($row['package_amount'])) {
|
||
$row['package_amount'] = 0;
|
||
$row['original_amount'] = 0;
|
||
}
|
||
return $row;
|
||
}
|
||
|
||
/**
|
||
* 校验规格、写入明细、重算原价。前端传了 package_amount(元)就用它,否则按差额补。
|
||
*/
|
||
private function replaceItems(int $packageId, array $items, array $params): void
|
||
{
|
||
$pkg = PackageModel::where('id', $packageId)->where('deleted_at', 0)->first();
|
||
$oldOriginal = (int) ($pkg['original_amount'] ?? 0);
|
||
$oldQuote = (int) ($pkg['package_amount'] ?? 0);
|
||
|
||
$rows = [];
|
||
$now = time();
|
||
$sort = 0;
|
||
$newOriginal = 0;
|
||
foreach ($items as $item) {
|
||
if (!is_array($item)) {
|
||
continue;
|
||
}
|
||
$catalogueId = (int) ($item['catalogue_id'] ?? 0);
|
||
$sheetId = (int) ($item['price_sheet_id'] ?? 0);
|
||
$qty = max(1, (int) ($item['quantity'] ?? 1));
|
||
if ($catalogueId <= 0 || $sheetId <= 0) {
|
||
$this->utils->errorThrow('每件商品都必须选择规格');
|
||
}
|
||
$sheet = PriceSheetModel::where('id', $sheetId)
|
||
->where('catalogue_id', $catalogueId)
|
||
->where('deleted_at', 0)
|
||
->first();
|
||
if (empty($sheet)) {
|
||
$this->utils->errorThrow('规格不存在或不属于该商品');
|
||
}
|
||
$unit = $this->price->resolveUnitPrice(
|
||
(string) ($sheet['routine'] ?? ''),
|
||
(string) ($item['material_key'] ?? 'routine'),
|
||
1
|
||
);
|
||
if ($unit <= 0) {
|
||
$this->utils->errorThrow('规格缺少有效报价');
|
||
}
|
||
$newOriginal += $unit * $qty;
|
||
$rows[] = [
|
||
'package_id' => $packageId,
|
||
'catalogue_id' => $catalogueId,
|
||
'price_sheet_id' => $sheetId,
|
||
'material_key' => (string) ($item['material_key'] ?? 'routine'),
|
||
'quantity' => $qty,
|
||
'unit_price' => $unit,
|
||
'sort' => (int) ($item['sort'] ?? $sort),
|
||
'remark' => (string) ($item['remark'] ?? ''),
|
||
'created_at' => $now,
|
||
'updated_at' => $now,
|
||
'deleted_at' => 0,
|
||
];
|
||
$sort++;
|
||
}
|
||
|
||
$quote = array_key_exists('package_amount', $params)
|
||
? max(0, $this->price->yuanToCents($params['package_amount']))
|
||
: $this->quote->applyOriginalDiff($oldOriginal, $newOriginal, $oldQuote);
|
||
|
||
DB::connection('business')->transaction(function () use ($packageId, $rows, $now, $newOriginal, $quote) {
|
||
PackageItemModel::where('package_id', $packageId)->where('deleted_at', 0)->update([
|
||
'deleted_at' => $now,
|
||
'updated_at' => $now,
|
||
]);
|
||
if (!empty($rows)) {
|
||
PackageItemModel::insert($rows);
|
||
}
|
||
PackageModel::where('id', $packageId)->update([
|
||
'original_amount' => $newOriginal,
|
||
'package_amount' => $quote,
|
||
'updated_at' => $now,
|
||
]);
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 补原价/套餐价/优惠的元文字段
|
||
*/
|
||
private function withAmountText(array $row): array
|
||
{
|
||
$original = (int) ($row['original_amount'] ?? 0);
|
||
$quote = (int) ($row['package_amount'] ?? 0);
|
||
$row['discount_amount'] = max(0, $original - $quote);
|
||
return $this->quote->withText($row);
|
||
}
|
||
}
|