客户等级设置

This commit is contained in:
liu
2026-08-17 16:27:15 +08:00
parent 2eb27127c9
commit a06c45dc49
31 changed files with 548 additions and 1142 deletions
@@ -8,7 +8,6 @@ use App\Http\Requests\Product\ProductFormRequest;
use App\Models\CustomerLevelModel;
use App\Models\NoticeModel;
use App\Models\ProductModel;
use App\Models\ProductPriceModel;
use App\Models\StoreModel;
use App\Models\UserModel;
use Illuminate\Http\JsonResponse;
@@ -24,7 +23,7 @@ use Modules\Common\Http\Controllers\BaseController;
use Modules\SystemTool\Services\SysFileService;
/**
* 商品档案管理
* 商品档案管理(售价 = 成本价 × (100 + 客户等级上浮比例) / 100,不再维护等级价格行)
*/
#[RequestAttribute('/product/goods', 'product.goods')]
class ProductController extends BaseController
@@ -38,7 +37,7 @@ class ProductController extends BaseController
protected array $quickSearchField = ['name', 'spec'];
/** A1 商品列表(含分类/供应商/各等级价格cost_price 在 $hidden 中,后台列表需显式恢复) */
/** A1 商品列表(含分类/供应商;prices 为按启用等级上浮比例换算的展示价cost_price 在 $hidden 中,后台列表需显式恢复) */
#[GetRoute(authorize: 'query')]
public function query(Request $request): JsonResponse
{
@@ -46,12 +45,26 @@ class ProductController extends BaseController
$pageSize = $params['pageSize'] ?? 10;
$data = $this->buildSearch(
$params,
ProductModel::query()->with(['category:id,name', 'supplier:id,name', 'prices.level:id,name'])
ProductModel::query()->with(['category:id,name', 'supplier:id,name'])
)
->orderBy('sort')
->orderBy('id', 'desc')
->paginate($pageSize);
$data->getCollection()->makeVisible('cost_price');
// 按启用等级直接换算展示价(无等级价格表,价格由成本价 × 等级上浮比例得出)
$levels = $this->enabledLevels();
$data->getCollection()->transform(static function (ProductModel $product) use ($levels) {
$row = $product->toArray();
$row['prices'] = $levels->map(static fn (CustomerLevelModel $level) => [
'level_id' => $level->id,
'level' => ['id' => $level->id, 'name' => $level->name],
'percent' => (float) $level->percent,
'price' => CustomerLevelModel::calcLevelPrice($product->cost_price, $level->percent),
])->values()->all();
return $row;
});
return $this->success($data->toArray());
}
@@ -70,32 +83,15 @@ class ProductController extends BaseController
}
/** 创建商品(事务内建商品 + 同步等级价格) */
/** 创建商品 */
#[PostRoute(authorize: 'create')]
public function create(ProductFormRequest $request): JsonResponse
{
$validated = $request->validated();
$prices = $validated['prices'] ?? [];
unset($validated['prices']);
$product = DB::transaction(function () use ($validated, $prices) {
$product = ProductModel::create($validated);
foreach ($prices as $row) {
ProductPriceModel::create([
'product_id' => $product->id,
'level_id' => (int) $row['level_id'],
'price' => $row['price'],
'price_type' => (int) ($row['price_type'] ?? ProductPriceModel::PRICE_TYPE_FIXED),
'percent' => $row['percent'] ?? 0,
]);
}
return $product;
});
$product = ProductModel::create($request->validated());
return $this->success(['id' => $product->id]);
}
/** 编辑商品prices 按 level_id upsert,删除已移除的等级行;未提交 prices 键时保持原价) */
/** 编辑商品 */
#[PutRoute(route: '/{id}', authorize: 'update', where: ['id' => '[0-9]+'])]
public function update(int $id, ProductFormRequest $request): JsonResponse
{
@@ -103,34 +99,11 @@ class ProductController extends BaseController
if (empty($product)) {
throw new RepositoryException('商品不存在');
}
$validated = $request->validated();
$prices = $validated['prices'] ?? [];
unset($validated['prices']);
DB::transaction(function () use ($product, $validated, $prices, $request) {
$product->update($validated);
if ($request->has('prices')) {
$levelIds = [];
foreach ($prices as $row) {
$levelId = (int) $row['level_id'];
$levelIds[] = $levelId;
ProductPriceModel::updateOrCreate(
['product_id' => $product->id, 'level_id' => $levelId],
[
'price' => $row['price'],
'price_type' => (int) ($row['price_type'] ?? ProductPriceModel::PRICE_TYPE_FIXED),
'percent' => $row['percent'] ?? 0,
],
);
}
$product->prices()->whereNotIn('level_id', $levelIds)->delete();
}
});
$product->update($request->validated());
return $this->success();
}
/** 删除商品(软删除,连带价格行一并删除 */
/** 删除商品(软删除) */
#[DeleteRoute(route: '/{id}', authorize: 'delete', where: ['id' => '[0-9]+'])]
public function delete(int $id): JsonResponse
{
@@ -138,22 +111,18 @@ class ProductController extends BaseController
if (empty($product)) {
throw new RepositoryException('商品不存在');
}
DB::transaction(function () use ($product) {
$product->prices()->delete();
$product->delete();
});
$product->delete();
return $this->success();
}
/**
* A2 价格矩阵:行=商品(支持 category_id / keyword 过滤 + page/pageSize 服务端分页),
* 列=全部启用等级,值=实际销售价(缺失为 null);行内含 cost_price 与每等级
* price_type_{levelId} / percent_{levelId},供前端判断计价类型与联动重算
* 列=全部启用等级,值=按等级上浮比例换算的售价(成本价未设置为 null);仅成本价可编辑
*/
#[GetRoute('/priceMatrix', 'query')]
public function priceMatrix(Request $request): JsonResponse
{
$query = ProductModel::query()->with('prices:id,product_id,level_id,price,price_type,percent');
$query = ProductModel::query();
if (($categoryId = (int) $request->input('category_id', 0)) > 0) {
$query->where('category_id', $categoryId);
}
@@ -168,13 +137,9 @@ class ProductController extends BaseController
$page = max(1, (int) $request->input('page', 1));
$products = $query->orderBy('sort')->orderBy('id')->paginate($pageSize, ['*'], 'page', $page);
$levels = CustomerLevelModel::query()
->where('status', CustomerLevelModel::STATUS_NORMAL)
->orderBy('sort')
->get(['id', 'name']);
$levels = $this->enabledLevels();
$rows = $products->getCollection()->map(function (ProductModel $product) use ($levels) {
$priceMap = $product->prices->keyBy('level_id');
$rows = $products->getCollection()->map(static function (ProductModel $product) use ($levels) {
$row = [
'id' => $product->id,
'name' => $product->name,
@@ -183,31 +148,27 @@ class ProductController extends BaseController
'cost_price' => (float) $product->cost_price,
];
foreach ($levels as $level) {
$price = $priceMap[$level->id] ?? null;
$row['price_' . $level->id] = $price
? (float) ProductPriceModel::calcActualPrice(
(int) $price->price_type,
$price->price,
$price->percent,
$product->cost_price,
)
$row['price_' . $level->id] = $product->cost_price > 0
? (float) CustomerLevelModel::calcLevelPrice($product->cost_price, $level->percent)
: null;
$row['price_type_' . $level->id] = $price ? (int) $price->price_type : ProductPriceModel::PRICE_TYPE_FIXED;
$row['percent_' . $level->id] = $price ? (float) $price->percent : 0;
}
return $row;
});
return $this->success([
'levels' => $levels->toArray(),
'levels' => $levels->map(static fn (CustomerLevelModel $level) => [
'id' => $level->id,
'name' => $level->name,
'percent' => (float) $level->percent,
])->values()->all(),
'rows' => $rows->values()->toArray(),
'total' => $products->total(),
]);
}
/**
* A2 批量调价:三类更新行(成本价 / 固定价 / 成本百分比,可混合同一行),事务写入,
* 写完后给受影响门店生成 Noticetype=price
* A2 批量调价:批量调整成本价(等级售价随之按上浮比例联动),事务写入,
* 写完后给全部正常门店的用户生成 Noticetype=price
*/
#[PutRoute('/batchPrice', 'batchPrice')]
public function batchPrice(BatchPriceRequest $request): JsonResponse
@@ -216,53 +177,24 @@ class ProductController extends BaseController
DB::transaction(function () use ($updates) {
$productIds = [];
$levelIds = [];
foreach ($updates as $row) {
$productId = (int) $row['product_id'];
$productIds[$productId] = true;
// 分支1:成本价更新(百分比计价的基数,可与等级价行同在一行)
if (array_key_exists('cost_price', $row) && $row['cost_price'] !== null) {
ProductModel::whereKey($productId)->update(['cost_price' => $row['cost_price']]);
}
// 分支2/3:等级价格行(固定价或成本百分比,按 price_type 区分)
if (isset($row['level_id'])) {
$levelId = (int) $row['level_id'];
$levelIds[$levelId] = true;
ProductPriceModel::updateOrCreate(
['product_id' => $productId, 'level_id' => $levelId],
[
'price' => $row['price'] ?? 0,
'price_type' => (int) ($row['price_type'] ?? ProductPriceModel::PRICE_TYPE_FIXED),
'percent' => $row['percent'] ?? 0,
],
);
}
ProductModel::whereKey($productId)->update(['cost_price' => $row['cost_price']]);
}
// 成本价变更只影响「该商品下百分比计价」的等级;受影响门店等级 = 等级更新行 ∪ 百分比行等级
$percentLevelIds = ProductPriceModel::query()
->whereIn('product_id', array_keys($productIds))
->where('price_type', ProductPriceModel::PRICE_TYPE_PERCENT)
->pluck('level_id')
->merge($levelIds)
->unique()
->all();
$productNames = ProductModel::whereIn('id', array_keys($productIds))
->pluck('name')
->implode('、');
$content = mb_substr('以下商品价格已调整:' . $productNames . ',下次登录小程序后按新价格显示', 0, 500);
// 受影响门店:客户等级在受影响等级范围内的正常门店,通知其绑定的正常用户
// 成本价变更影响所有等级的售价:通知全部正常门店绑定的正常用户
$userIds = UserModel::query()
->where('status', UserModel::STATUS_NORMAL)
->whereIn('store_id', function ($q) use ($percentLevelIds) {
->whereIn('store_id', function ($q) {
$q->select('id')
->from('store')
->where('status', StoreModel::STATUS_NORMAL)
->whereIn('level_id', $percentLevelIds);
->where('status', StoreModel::STATUS_NORMAL);
})
->pluck('id');
@@ -274,7 +206,6 @@ class ProductController extends BaseController
'content' => $content,
'data' => [
'product_ids' => array_keys($productIds),
'level_ids' => array_keys($levelIds),
],
'is_read' => NoticeModel::UNREAD,
]);
@@ -298,4 +229,17 @@ class ProductController extends BaseController
->toArray();
return $this->success($data);
}
/**
* 启用中的客户等级(列表/矩阵共用的等级列来源)
*
* @return \Illuminate\Database\Eloquent\Collection<int, CustomerLevelModel>
*/
private function enabledLevels(): \Illuminate\Database\Eloquent\Collection
{
return CustomerLevelModel::query()
->where('status', CustomerLevelModel::STATUS_NORMAL)
->orderBy('sort')
->get(['id', 'name', 'percent']);
}
}