153 lines
5.1 KiB
PHP
153 lines
5.1 KiB
PHP
<?php
|
||
|
||
namespace App\Service\business;
|
||
|
||
/**
|
||
* 价格可见性与倍率
|
||
*
|
||
* 报价单的 routine 列是一串用 @ 分隔的价格,每段要么是纯数字,要么是「名称:价格」,
|
||
* 乘倍率时只能乘价格段,把整段当数字乘会把名称吃掉。
|
||
* show_price 为假时整个价格数组换成 ['****'],并回 is_show_price=false 给前端。
|
||
*
|
||
* 这段逻辑原先只存在于 lgp-wx-api 的 ProductService,后台完全没有,
|
||
* 于是后台看到的是原价、小程序看到的是倍率价,对账时无从复现。收在这里两边共用。
|
||
*/
|
||
class PriceService
|
||
{
|
||
private static mixed $_instance;
|
||
|
||
public const MASK = '****';
|
||
|
||
public static function getInstance(): null|static
|
||
{
|
||
$name = get_called_class();
|
||
if (!isset(self::$_instance[$name])) {
|
||
self::$_instance[$name] = new static();
|
||
}
|
||
return self::$_instance[$name];
|
||
}
|
||
|
||
/**
|
||
* 拆分 routine 列
|
||
*/
|
||
public function split(?string $routine): array
|
||
{
|
||
$routine = trim((string) $routine);
|
||
if ($routine === '') {
|
||
return [];
|
||
}
|
||
return array_values(array_filter(array_map('trim', explode('@', $routine)), fn ($v) => $v !== ''));
|
||
}
|
||
|
||
/**
|
||
* 按用户可见性与倍率格式化 routine
|
||
*
|
||
* @param bool $showPrice 是否可见价格
|
||
* @param int|float|string $multiplier 价格倍率
|
||
* @return array<int, string>
|
||
*/
|
||
public function formatRoutine(?string $routine, bool $showPrice, mixed $multiplier = 1): array
|
||
{
|
||
if (!$showPrice) {
|
||
return [self::MASK];
|
||
}
|
||
$items = $this->split($routine);
|
||
foreach ($items as &$item) {
|
||
$item = $this->formatPrice($item, $multiplier);
|
||
}
|
||
unset($item);
|
||
return $items;
|
||
}
|
||
|
||
/**
|
||
* 单段价格乘倍率。「名称:价格」只乘价格部分,兼容半角冒号与空格
|
||
*/
|
||
public function formatPrice(string $value, mixed $multiplier = 1): string
|
||
{
|
||
$multiplier = $this->normalizeMultiplier($multiplier);
|
||
try {
|
||
if (preg_match('/^[0-9.]+$/', $value)) {
|
||
return bcmul($value, $multiplier, 0);
|
||
}
|
||
$normalized = str_replace([':', ' '], [':', ''], $value);
|
||
$parts = explode(':', $normalized);
|
||
if (array_key_exists(1, $parts) && preg_match('/^[0-9.]+$/', $parts[1])) {
|
||
$parts[1] = bcmul($parts[1], $multiplier, 0);
|
||
}
|
||
return implode(':', $parts);
|
||
} catch (\Throwable) {
|
||
return $value;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 给一组报价单行套上价格规则,返回值里 routine 变成数组
|
||
*
|
||
* @param array $rows price_sheet 行
|
||
* @return array{rows: array, is_show_price: bool}
|
||
*/
|
||
public function applyToRows(array $rows, bool $showPrice, mixed $multiplier = 1): array
|
||
{
|
||
foreach ($rows as &$row) {
|
||
$row['routine'] = $this->formatRoutine($row['routine'] ?? '', $showPrice, $multiplier);
|
||
}
|
||
unset($row);
|
||
return ['rows' => $rows, 'is_show_price' => $showPrice];
|
||
}
|
||
|
||
/**
|
||
* 取某个材质的单价,返回「分」
|
||
*
|
||
* 下单要的是一个确定的数字,而 routine 是给人看的字符串(可能是 "1200",
|
||
* 也可能是 "布艺:1200@皮艺:1800")。这里按 materialKey 找对应段,
|
||
* 找不到就退回第一个能解析出数字的段;一个都没有返回 0,由调用方决定报错还是放过。
|
||
*
|
||
* 金额一律整数分:老库价格是整数元,乘完倍率再 ×100,不引入浮点。
|
||
*/
|
||
public function resolveUnitPrice(?string $routine, string $materialKey = '', mixed $multiplier = 1): int
|
||
{
|
||
$items = $this->split($routine);
|
||
if (empty($items)) {
|
||
return 0;
|
||
}
|
||
$materialKey = trim($materialKey);
|
||
$fallback = 0;
|
||
foreach ($items as $item) {
|
||
$normalized = str_replace([':', ' '], [':', ''], $item);
|
||
$parts = explode(':', $normalized);
|
||
$name = count($parts) > 1 ? $parts[0] : '';
|
||
$value = count($parts) > 1 ? $parts[1] : $parts[0];
|
||
if (!preg_match('/^[0-9.]+$/', $value)) {
|
||
continue;
|
||
}
|
||
$yuan = (int) bcmul($value, $this->normalizeMultiplier($multiplier), 0);
|
||
if ($materialKey !== '' && $materialKey !== 'routine' && $name === $materialKey) {
|
||
return $yuan * 100;
|
||
}
|
||
if ($fallback === 0) {
|
||
$fallback = $yuan * 100;
|
||
}
|
||
}
|
||
return $fallback;
|
||
}
|
||
|
||
/**
|
||
* 分转元字符串,仅用于展示与导出
|
||
*/
|
||
public function centsToYuan(int $cents): string
|
||
{
|
||
return number_format($cents / 100, 2, '.', '');
|
||
}
|
||
|
||
/**
|
||
* 倍率兜底:库里可能是空串、0 或负数,直接拿去 bcmul 会把价格清零
|
||
*/
|
||
private function normalizeMultiplier(mixed $multiplier): string
|
||
{
|
||
if (!is_numeric($multiplier) || (float) $multiplier <= 0) {
|
||
return '1';
|
||
}
|
||
return (string) $multiplier;
|
||
}
|
||
}
|