Files
lgp-admin-plus-api/app/Service/business/PackageService.php

318 lines
11 KiB
PHP
Raw Normal View History

2026-08-19 08:16:49 +08:00
<?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);
}
}