更新若干功能

This commit is contained in:
2026-08-19 08:16:49 +08:00
parent 4979bb83d2
commit 72bc6502eb
56 changed files with 3627 additions and 343 deletions

View File

@@ -89,6 +89,19 @@ class WxAppService
];
}
/**
* 当前品牌是否启用套餐。只读开关列,不解密密钥,主题和下发列表都能安全调用。
*/
public function packageEnabled(): bool
{
$code = $this->currentCode();
$query = WxAppModel::where('deleted_at', 0)->where('status', 0);
$app = $code !== ''
? (clone $query)->where('code', $code)->first(['package_enabled'])
: $query->first(['package_enabled']);
return (int) ($app['package_enabled'] ?? 0) === 1;
}
/**
* 当前请求声明的品牌标识
*

View File

@@ -16,7 +16,7 @@ class WxAuthService extends BaseWxService
{
protected bool $needLogin = false;
private const DEFAULT_AVATAR = 'http://qiniu.boerman.top/b_2010f38d40d7d4426787b9131020e2a3.png';
private const DEFAULT_AVATAR = 'https://qiniu.boerman.top/b_2010f38d40d7d4426787b9131020e2a3.png';
/**
* code 登录:老用户更新 session_key,新用户建档并送一份默认清单

View File

@@ -5,6 +5,8 @@ namespace App\Service\wx;
use App\BaseApp\BaseWxService;
use App\Models\business\ListItemModel;
use App\Models\business\ListModel;
use App\Models\business\PriceSheetModel;
use App\Service\business\PackageQuoteService;
use App\Service\business\PriceService;
use App\Service\business\SerialNoService;
@@ -78,6 +80,7 @@ class WxListService extends BaseWxService
$info = ListModel::with([
'items' => fn ($query) => $query->where('deleted_at', 0),
'items.catalogue',
'items.priceSheet',
'items.catalogue.priceSheet' => fn ($query) => $query->where('deleted_at', 0),
])->where('id', $id)
->where('user_id', $this->userId)
@@ -93,6 +96,23 @@ class WxListService extends BaseWxService
$multiplier = $this->priceMultiplier();
foreach ($info['items'] as &$item) {
$sheet = $item['catalogue']['price_sheet'] ?? [];
$rawRoutine = '';
foreach ($sheet as $line) {
if ((int) ($line['id'] ?? 0) === (int) ($item['price_sheet_id'] ?? 0)) {
$rawRoutine = (string) ($line['routine'] ?? '');
break;
}
}
if ($rawRoutine === '') {
$rawRoutine = (string) ($item['price_sheet']['routine'] ?? '');
}
if ((int) ($item['unit_price'] ?? 0) <= 0) {
$item['unit_price'] = $price->resolveUnitPrice(
$rawRoutine,
(string) ($item['material_key'] ?? 'routine'),
$multiplier
);
}
if (!empty($sheet)) {
$applied = $price->applyToRows($sheet, $showPrice, $multiplier);
$item['catalogue']['price_sheet'] = $applied['rows'];
@@ -102,6 +122,13 @@ class WxListService extends BaseWxService
}
unset($item);
$info['is_show_price'] = $showPrice;
$payable = PackageQuoteService::getInstance()->listPayable(
(int) ($info['package_id'] ?? 0),
(int) ($info['package_amount'] ?? 0),
(int) ($info['original_amount'] ?? 0),
$info['items']
);
$info = array_merge($info, PackageQuoteService::getInstance()->withText($payable, $showPrice));
return $info;
}
@@ -210,6 +237,22 @@ class WxListService extends BaseWxService
if (array_key_exists('remark', $params)) {
$update['remark'] = (string) $params['remark'];
}
$item = ListItemModel::where('id', $itemId)->whereIn('list_id', $listIds)->first();
if (empty($item)) {
$this->utils->errorThrow('明细不存在');
}
$sheetId = (int) ($update['price_sheet_id'] ?? $item['price_sheet_id']);
$material = (string) ($update['material_key'] ?? $item['material_key'] ?? 'routine');
if ($sheetId > 0) {
$sheet = PriceSheetModel::where('id', $sheetId)->where('deleted_at', 0)->first();
if (!empty($sheet)) {
$update['unit_price'] = PriceService::getInstance()->resolveUnitPrice(
(string) ($sheet['routine'] ?? ''),
$material,
$this->priceMultiplier()
);
}
}
return ListItemModel::where('id', $itemId)->whereIn('list_id', $listIds)->update($update);
}
}

View File

@@ -0,0 +1,274 @@
<?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);
}
}

View File

@@ -6,7 +6,6 @@ use App\BaseApp\BaseWxService;
use App\Models\business\CatalogueModel;
use App\Models\business\CategoryModel;
use App\Models\business\ImageModel;
use App\Models\business\WxUserModel;
use App\Service\business\PriceService;
/**
@@ -92,34 +91,4 @@ class WxProductService extends BaseWxService
$info['is_show_price'] = $applied['is_show_price'];
return $info;
}
/**
* 继承分享代理商的倍率
*
* 只有「还没有上级、自己也不是代理商」的用户会被绑定;已绑定同一个上级时同步最新倍率,
* 代理商改了倍率下级要跟着变。
*/
private function inheritAgentPrice(int $shareUserId): void
{
$isAgent = (int) ($this->userInfo['is_p'] ?? 0) === 1;
$pid = (int) ($this->userInfo['pid'] ?? 0);
if ($isAgent) {
return;
}
if ($pid !== 0 && $pid !== $shareUserId) {
return;
}
$agent = WxUserModel::where('id', $shareUserId)->where('deleted_at', 0)->first();
if (empty($agent) || (int) $agent['is_p'] !== 1) {
return;
}
$this->userInfo['show_price'] = 1;
$this->userInfo['price_number'] = $agent['price_number'];
WxUserModel::where('id', $this->userId)->update([
'pid' => $shareUserId,
'show_price' => 1,
'price_number' => $agent['price_number'],
'updated_at' => time(),
]);
}
}

View File

@@ -6,6 +6,7 @@ use App\BaseApp\BaseWxService;
use App\Models\business\WxTemplateModel;
use App\Models\business\WxUserModel;
use App\Models\WxAppModel;
use App\Service\business\WxCardSchemeService;
use App\Service\business\WxTemplatePresetService;
use App\Service\business\WxTemplateSchemaService;
@@ -68,32 +69,88 @@ class WxThemeService extends BaseWxService
$preset = WxTemplatePresetService::getInstance()->all()[0] ?? [];
$tokens = $preset['tokens'] ?? [];
$layout = $preset['layout'] ?? [];
return [
'code' => (string) ($preset['code'] ?? 'lux-champagne'),
'name' => (string) ($preset['name'] ?? '轻奢·香槟金'),
'style_tag' => (string) ($preset['style_tag'] ?? '轻奢'),
'version' => 0,
'tokens' => $tokens,
'layout' => $layout,
'css_vars' => $schema->toCssVariables($tokens),
'fallback' => true,
];
return $this->packTheme(
(string) ($preset['code'] ?? 'lux-champagne'),
(string) ($preset['name'] ?? '轻奢·香槟金'),
(string) ($preset['style_tag'] ?? '轻奢'),
0,
$tokens,
$layout,
$schema,
true,
);
}
$template = $template->toArray();
$tokens = is_array($template['tokens']) ? $template['tokens'] : [];
$layout = is_array($template['layout']) ? $template['layout'] : [];
return $this->packTheme(
(string) $template['code'],
(string) $template['name'],
(string) $template['style_tag'],
(int) $template['version'],
$tokens,
$layout,
$schema,
false,
);
}
/**
* 组装主题包:令牌 + 布局 + 本模板引用到的卡片方案
*/
private function packTheme(
string $code,
string $name,
string $styleTag,
int $version,
array $tokens,
array $layout,
WxTemplateSchemaService $schema,
bool $fallback,
): array {
return [
'code' => (string) $template['code'],
'name' => (string) $template['name'],
'style_tag' => (string) $template['style_tag'],
'version' => (int) $template['version'],
'code' => $code,
'name' => $name,
'style_tag' => $styleTag,
'version' => $version,
'tokens' => $tokens,
'layout' => is_array($template['layout']) ? $template['layout'] : [],
'layout' => $layout,
'css_vars' => $schema->toCssVariables($tokens),
'fallback' => false,
'card_schemes' => WxCardSchemeService::mapByCodes($this->collectSchemeCodes($layout)),
'features' => [
'package_enabled' => WxAppService::getInstance()->packageEnabled(),
],
'fallback' => $fallback,
];
}
/**
* 从根 card_scheme、页级 pages.*.card_scheme、区块 props.card_scheme 收集引用
*
* @return array<int, string>
*/
private function collectSchemeCodes(array $layout): array
{
$codes = [];
if (!empty($layout['card_scheme'])) {
$codes[] = (string) $layout['card_scheme'];
}
foreach ((array) ($layout['pages'] ?? []) as $page) {
$pageScheme = (string) ($page['card_scheme'] ?? '');
if ($pageScheme !== '') {
$codes[] = $pageScheme;
}
foreach ((array) ($page['sections'] ?? []) as $section) {
$code = (string) ($section['props']['card_scheme'] ?? '');
if ($code !== '') {
$codes[] = $code;
}
}
}
return $codes;
}
/**
* 可选风格列表,用于小程序里让用户自己换肤
*/