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

275 lines
10 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\wx;
use App\BaseApp\BaseWxService;
use App\Models\business\ListItemModel;
use App\Models\business\ListModel;
use App\Models\business\PackageItemModel;
use App\Models\business\PackageModel;
use App\Service\business\PackageQuoteService;
use App\Service\business\PriceService;
use App\Service\business\SerialNoService;
use App\Service\common\MediaUrlService;
use Illuminate\Support\Facades\DB;
/**
* 小程序套餐:热门/列表/详情免登录,一件加入清单要登录
*
* 应用没开套餐时列表直接空,避免关了开关首页还露出卡片。
*/
class WxPackageService extends BaseWxService
{
protected bool $needLogin = false;
private MediaUrlService $media;
private PackageQuoteService $quote;
private PriceService $price;
public function __construct()
{
parent::__construct();
$this->media = MediaUrlService::getInstance();
$this->quote = PackageQuoteService::getInstance();
$this->price = PriceService::getInstance();
}
/**
* 首页热门
*/
public function hot(): array
{
if (!$this->featureEnabled()) {
return [];
}
$rows = PackageModel::where('deleted_at', 0)
->where('status', 0)
->where('is_hot', 1)
->orderBy('sort')
->orderBy('id', 'desc')
->get();
return $this->mapCards($rows->toArray());
}
/**
* 套餐 tab 列表keyword / name 按名称、副标题模糊查
*/
public function list(): array
{
if (!$this->featureEnabled()) {
return [];
}
$page = max(1, (int) request()->get('page', 1));
$pageSize = max(1, (int) request()->get('pageSize', 20));
$keyword = trim((string) request()->get('keyword', request()->get('name', '')));
$query = PackageModel::where('deleted_at', 0)->where('status', 0)->orderBy('sort')->orderBy('id', 'desc');
if ($keyword !== '') {
$query->where(function ($inner) use ($keyword) {
$inner->where('name', 'like', '%' . $keyword . '%')
->orWhere('subtitle', 'like', '%' . $keyword . '%');
});
}
$total = (clone $query)->count();
$rows = $query->forPage($page, $pageSize)->get()->toArray();
return [
'items' => $this->mapCards($rows),
'total' => $total,
'page' => $page,
'pageSize' => $pageSize,
'has_more' => ($page * $pageSize) < $total,
];
}
/**
* 套餐详情(含搭配与价格)
*
* 带 p_user_id 时与商品详情同一套:代理商分享进来的普通用户继承倍率。
*/
public function detail(int $id, int $shareUserId = 0): array
{
if ($shareUserId > 0) {
$this->inheritAgentPrice($shareUserId);
}
if (!$this->featureEnabled()) {
$this->utils->errorThrow('套餐功能未启用');
}
$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)->where('status', 0)->first();
if (empty($info)) {
$this->utils->errorThrow('套餐不存在或已下架');
}
$info = $info->toArray();
$show = $this->canSeePrice();
$multiplier = $this->priceMultiplier();
$info['cover'] = $this->media->toPublic($info['cover'] ?? '');
$info['item_count'] = count($info['items'] ?? []);
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'] ?? '';
$unit = $this->quote->applyMultiplier((int) ($item['unit_price'] ?? 0), $multiplier);
$item['unit_price'] = $unit;
$item['unit_price_text'] = $show ? $this->price->centsToYuan($unit) : PriceService::MASK;
$item['product'] = $item['catalogue'] ?? null;
}
unset($item);
return $this->withWxAmounts($info, $show, $multiplier);
}
/**
* 一件加入清单:记下套餐价/原价和每行快照,供以后补差价
*/
public function toList(array $params): array
{
// 写操作必须登录,构造时 needLogin=false 是为了列表免鉴权
if ($this->userId <= 0) {
$this->utils->errorThrow('请先登录');
}
if (!$this->featureEnabled()) {
$this->utils->errorThrow('套餐功能未启用');
}
$packageId = (int) ($params['package_id'] ?? 0);
$detail = $this->detail($packageId);
$items = $detail['items'] ?? [];
if (empty($items)) {
$this->utils->errorThrow('套餐还没有搭配商品');
}
$listId = (int) ($params['list_id'] ?? 0);
$list = null;
if ($listId > 0) {
$list = ListModel::where('id', $listId)
->where('user_id', $this->userId)
->where('deleted_at', 0)
->first();
if (empty($list)) {
$this->utils->errorThrow('清单不存在');
}
$bound = (int) ($list['package_id'] ?? 0);
if ($bound > 0 && $bound !== $packageId) {
$this->utils->errorThrow('该清单已绑定其他套餐,请新建清单');
}
}
// detail() 已经乘过用户倍率,这里直接快照,不能再乘一次
$packageAmount = (int) ($detail['package_amount'] ?? 0);
$originalAmount = (int) ($detail['original_amount'] ?? 0);
$newListId = 0;
DB::connection('business')->transaction(function () use (
&$newListId, $list, $packageId, $packageAmount, $originalAmount, $items, $detail
) {
$now = time();
if (empty($list)) {
$newListId = ListModel::insertGetId([
'list_no' => SerialNoService::getInstance()->generate(SerialNoService::PREFIX_LIST, 'list', 'list_no'),
'user_id' => $this->userId,
'name' => (string) ($detail['name'] ?? '套餐'),
'remark' => '',
'package_id' => $packageId,
'package_amount' => $packageAmount,
'original_amount' => $originalAmount,
'created_at' => $now,
]);
} else {
$newListId = (int) $list['id'];
$update = ['updated_at' => $now];
if ((int) ($list['package_id'] ?? 0) === 0) {
$update['package_id'] = $packageId;
$update['package_amount'] = $packageAmount;
$update['original_amount'] = $originalAmount;
}
ListModel::where('id', $newListId)->update($update);
}
foreach ($items as $item) {
$catalogueId = (int) ($item['catalogue_id'] ?? 0);
$sheetId = (int) ($item['price_sheet_id'] ?? 0);
$qty = max(1, (int) ($item['quantity'] ?? 1));
$unit = (int) ($item['unit_price'] ?? 0);
$exists = ListItemModel::where('list_id', $newListId)
->where('catalogue_id', $catalogueId)
->where('price_sheet_id', $sheetId)
->where('deleted_at', 0)
->first();
if (!empty($exists)) {
ListItemModel::where('id', $exists['id'])->update([
'quantity' => (int) $exists['quantity'] + $qty,
'unit_price' => $unit,
'package_item_id' => (int) ($item['id'] ?? 0),
'updated_at' => $now,
]);
continue;
}
ListItemModel::insert([
'list_id' => $newListId,
'catalogue_id' => $catalogueId,
'price_sheet_id' => $sheetId,
'material_key' => (string) ($item['material_key'] ?? 'routine'),
'quantity' => $qty,
'unit_price' => $unit,
'package_item_id' => (int) ($item['id'] ?? 0),
'snapshot_unit_price' => $unit,
'snapshot_quantity' => $qty,
'remark' => (string) ($item['remark'] ?? ''),
'created_at' => $now,
]);
}
});
return ['list_id' => $newListId];
}
/**
* 当前品牌是否打开套餐
*/
public function featureEnabled(): bool
{
return WxAppService::getInstance()->packageEnabled();
}
/**
* 卡片列表:封面、件数、有价权才带价格
*/
private function mapCards(array $rows): array
{
$ids = array_column($rows, 'id');
$counts = PackageItemModel::whereIn('package_id', $ids)
->where('deleted_at', 0)
->selectRaw('package_id, count(*) as total')
->groupBy('package_id')
->get()
->keyBy('package_id');
$show = $this->canSeePrice();
$multiplier = $this->priceMultiplier();
foreach ($rows as &$row) {
$row['cover'] = $this->media->toPublic($row['cover'] ?? '');
$row['item_count'] = (int) ($counts[$row['id']]['total'] ?? 0);
$row = $this->withWxAmounts($row, $show, $multiplier);
unset($row['remark']);
}
unset($row);
return $rows;
}
/**
* 把基准价乘倍率,并按价权决定是否打码
*/
private function withWxAmounts(array $row, bool $show, mixed $multiplier): array
{
$original = $this->quote->applyMultiplier((int) ($row['original_amount'] ?? 0), $multiplier);
$package = $this->quote->applyMultiplier((int) ($row['package_amount'] ?? 0), $multiplier);
$row['original_amount'] = $original;
$row['package_amount'] = $package;
$row['discount_amount'] = max(0, $original - $package);
$row['is_show_price'] = $show;
return $this->quote->withText($row, $show);
}
}