Files
lgp-admin-plus-api/app/Service/business/PackageService.php
2026-08-19 08:16:49 +08:00

318 lines
11 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\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);
}
}