317 lines
10 KiB
PHP
317 lines
10 KiB
PHP
<?php
|
||
|
||
namespace app\common\service;
|
||
|
||
use app\common\logic\InvItemLogic;
|
||
use app\common\model\User;
|
||
use app\common\validate\InvItemValidate;
|
||
use app\common\traits\crud\CrudServiceTrait;
|
||
use app\common\model\InvItem;
|
||
use support\Redis;
|
||
use support\Request;
|
||
use support\exception\BusinessException;
|
||
|
||
class InvItemService
|
||
{
|
||
|
||
use CrudServiceTrait;
|
||
|
||
/**
|
||
* 指定当前要操作的模型
|
||
*/
|
||
protected string $modelClass = InvItem::class;
|
||
|
||
/**
|
||
* 验证器
|
||
*/
|
||
protected $validator = InvItemValidate::class;
|
||
|
||
protected $logic;
|
||
|
||
public function __construct()
|
||
{
|
||
$this->logic = (new InvItemLogic());
|
||
}
|
||
/**
|
||
* 获取验证器
|
||
*/
|
||
public function getValidator()
|
||
{
|
||
return new $this->validator;
|
||
}
|
||
|
||
/**
|
||
* 构造 CrudUtil 查询配置
|
||
*/
|
||
protected function buildQueryConfig(Request $request, string $scene = 'default'): array
|
||
{
|
||
$p = $request->all();
|
||
|
||
$config = [];
|
||
$config['model'] = $this->modelClass;
|
||
switch ($scene) {
|
||
case 'default':
|
||
$config['where']['status'] = 1;
|
||
$config['order']['sort'] = 'desc';
|
||
$config['order']['id'] = 'desc';
|
||
break;
|
||
case 'index':
|
||
$config['where']['status'] = 1;
|
||
|
||
// 关键字
|
||
if (isset($p['keyword']) && $p['keyword']) {
|
||
$k = '%' . $p['keyword'] . '%';
|
||
foreach (['item_name'] as $col) {
|
||
$config['orWhere'][] = [$col, 'like', $k];
|
||
}
|
||
}
|
||
|
||
$config['order']['sort'] = 'desc';
|
||
$config['order']['id'] = 'desc';
|
||
break;
|
||
case 'show':
|
||
$config['where']['id'] = $p['id'];
|
||
$config['where']['status'] = 1;
|
||
break;
|
||
case 'store':
|
||
break;
|
||
case 'update':
|
||
$config['where']['id'] = $p['id'];
|
||
break;
|
||
case 'destroy':
|
||
$config['where']['id'] = $p['id'];
|
||
break;
|
||
}
|
||
|
||
return $config;
|
||
}
|
||
|
||
/**
|
||
* 生成 8-12 位的物品编码(默认10位)
|
||
*
|
||
* @param int $length 编码长度,默认为10(范围 8-12)
|
||
* @return string 返回大写的字母数字组合编码
|
||
*/
|
||
private function generateItemCode($length = 10)
|
||
{
|
||
// 排除易混淆字符:0, O, o, 1, I, l
|
||
$chars = '23456789ABCDEFGHJKLMNPQRSTUVWXYZ';
|
||
$code = '';
|
||
$maxIndex = strlen($chars) - 1;
|
||
|
||
for ($i = 0; $i < $length; $i++) {
|
||
$code .= $chars[mt_rand(0, $maxIndex)];
|
||
}
|
||
|
||
return $code;
|
||
}
|
||
|
||
|
||
public function beforeServiceStore($data, Request $request): array
|
||
{
|
||
$data['item_code'] = $this->generateItemCode();
|
||
$data['category_id'] = 0;
|
||
$data['spec_model'] = '';
|
||
$data['unit'] = '件';
|
||
$data['is_consumable'] = 0;
|
||
$data['stock_qty'] = $data['stock_qty']??1;
|
||
$data['location'] = '';
|
||
|
||
Redis::set('inv_store_u_stock', $request->user->uid, 'EX', 30);
|
||
|
||
return $data;
|
||
}
|
||
public function afterServiceStore($res)
|
||
{
|
||
$uid = redis::get('inv_store_u_stock');
|
||
$name = $uid ? User::where('id', $uid)->value('name') : '';
|
||
Redis::del('inv_store_u_stock');
|
||
// 记录库存变化日志
|
||
$this->recordStockLog([
|
||
'item_id' => $res['id'],
|
||
'item_name_snap' => $res['item_name'],
|
||
'change_qty' => $res['stock_qty'],
|
||
'before_qty' => 0,
|
||
'after_qty' => $res['stock_qty'],
|
||
'biz_type' => 1,
|
||
'biz_no' => '',
|
||
'operator_id' => $uid,
|
||
'operator_name' => $name,
|
||
'remark' => '初始数量',
|
||
], $res['stock_qty']);
|
||
}
|
||
|
||
public function beforeServiceUpdate($data, Request $request)
|
||
{
|
||
$uid = $request->user->uid;
|
||
$name = $uid ? User::where('id', $uid)->value('name') : '';
|
||
$item = InvItem::where('id', $data['id'])->first();
|
||
$change_qty = $data['change_qty'] - $item['stock_qty'];
|
||
// 记录库存变化日志
|
||
$this->recordStockLog([
|
||
'item_id' => $data['id'],
|
||
'item_name_snap' => $data['item_name'],
|
||
'change_qty' => $change_qty,
|
||
'before_qty' => $item['stock_qty'],
|
||
'after_qty' => $data['stock_qty'],
|
||
'biz_type' => 1,
|
||
'biz_no' => '',
|
||
'operator_id' => $uid,
|
||
'operator_name' => $name,
|
||
'remark' => '管理员修改数量',
|
||
], $change_qty);
|
||
}
|
||
|
||
/**
|
||
* 变动物品库存数量
|
||
*
|
||
* @param int $itemId 物品ID
|
||
* @param int $changeQty 变动数量:>0 入库,<0 出库
|
||
* @param bool $allowNegative 是否允许库存变成负数,默认不允许
|
||
* @param array $options 额外选项(用于记录日志)
|
||
* - biz_type: 业务类型
|
||
* - biz_no: 单据号
|
||
* - operator_id: 操作人ID
|
||
* - operator_name: 操作人姓名
|
||
* - remark: 备注
|
||
* @return InvItem 最新的物品记录
|
||
* @throws BusinessException
|
||
*/
|
||
public function changeStock(int $itemId, int $changeQty, bool $allowNegative = false, array $options = []): InvItem
|
||
{
|
||
if ($changeQty === 0) {
|
||
throw new BusinessException('库存变动数量不能为 0');
|
||
}
|
||
|
||
// 获取变动前的库存(用于日志)
|
||
$itemBefore = InvItem::query()->find($itemId);
|
||
if (!$itemBefore) {
|
||
throw new BusinessException('物品不存在');
|
||
}
|
||
$beforeQty = $itemBefore->stock_qty;
|
||
|
||
$now = time();
|
||
|
||
// 构建查询
|
||
$query = InvItem::query()->where('id', $itemId);
|
||
|
||
// 如果不允许负数,并且是出库场景(变动为负)
|
||
if (! $allowNegative && $changeQty < 0) {
|
||
// 要保证:当前 stock_qty >= 出库数量
|
||
$query->where('stock_qty', '>=', -$changeQty);
|
||
}
|
||
|
||
// 使用 increment 实现:UPDATE inv_item SET stock_qty = stock_qty + ?, updated_at = ? WHERE ...
|
||
$affected = $query->increment('stock_qty', $changeQty, [
|
||
'updated_at' => $now,
|
||
]);
|
||
|
||
if ($affected === 0) {
|
||
// 更新不到行:
|
||
// 1) 物品不存在
|
||
// 2) 或者库存不足(不允许负数时)
|
||
throw new BusinessException('库存不足,无法完成库存变动');
|
||
}
|
||
|
||
// 再查一次最新数据返回(在外层事务中也会保持一致)
|
||
/** @var InvItem $item */
|
||
$item = InvItem::query()->find($itemId);
|
||
|
||
// 记录库存变化日志
|
||
$this->recordStockLog([
|
||
'item_id' => $itemId,
|
||
'item_name_snap' => $item->item_name,
|
||
'change_qty' => $changeQty,
|
||
'before_qty' => $beforeQty,
|
||
'after_qty' => $item->stock_qty,
|
||
'biz_type' => $options['biz_type'] ?? null,
|
||
'biz_no' => $options['biz_no'] ?? null,
|
||
'operator_id' => $options['operator_id'] ?? null,
|
||
'operator_name' => $options['operator_name'] ?? null,
|
||
'remark' => $options['remark'] ?? null,
|
||
], $changeQty);
|
||
|
||
return $item;
|
||
}
|
||
|
||
/**
|
||
* 出库(减库存,默认不允许负数)
|
||
*
|
||
* @param int $itemId 物品ID
|
||
* @param int $qty 出库数量
|
||
* @param bool $allowNegative 是否允许负数
|
||
* @param array $options 日志选项(同 changeStock)
|
||
* @return InvItem
|
||
* @throws BusinessException
|
||
*/
|
||
public function stockOut(int $itemId, int $qty, bool $allowNegative = false, array $options = []): InvItem
|
||
{
|
||
if ($qty <= 0) {
|
||
throw new BusinessException('出库数量必须大于 0');
|
||
}
|
||
// 确定变动类型:2-出库(领用)或 3-出库(损耗)
|
||
$changeType = $options['change_type'] ?? \app\common\model\InvStockLog::CHANGE_TYPE_OUT_CLAIM;
|
||
|
||
return $this->changeStock($itemId, -$qty, $allowNegative, array_merge($options, [
|
||
'change_type' => $changeType,
|
||
'remark' => $options['remark'] ?? '领用出库',
|
||
]));
|
||
}
|
||
|
||
/**
|
||
* 入库(加库存)
|
||
*
|
||
* @param int $itemId 物品ID
|
||
* @param int $qty 入库数量
|
||
* @param array $options 日志选项(同 changeStock)
|
||
* @return InvItem
|
||
* @throws BusinessException
|
||
*/
|
||
public function stockIn(int $itemId, int $qty, array $options = []): InvItem
|
||
{
|
||
if ($qty <= 0) {
|
||
throw new BusinessException('入库数量必须大于 0');
|
||
}
|
||
// 确定变动类型:1-入库 或 4-入库(归还)
|
||
$changeType = $options['change_type'] ?? \app\common\model\InvStockLog::CHANGE_TYPE_IN;
|
||
|
||
return $this->changeStock($itemId, $qty, true, array_merge($options, [
|
||
'change_type' => $changeType,
|
||
'remark' => $options['remark'] ?? '入库',
|
||
]));
|
||
}
|
||
|
||
/**
|
||
* 记录库存变化日志
|
||
*
|
||
* @param array $logData
|
||
* @param int $changeQty 变动数量
|
||
* @return void
|
||
*/
|
||
private function recordStockLog(array $logData, int $changeQty): void
|
||
{
|
||
try {
|
||
// 根据变动数量确定变动类型(如果未指定)
|
||
if (!isset($logData['change_type'])) {
|
||
if ($changeQty > 0) {
|
||
$logData['change_type'] = \app\common\model\InvStockLog::CHANGE_TYPE_IN;
|
||
} else {
|
||
$logData['change_type'] = \app\common\model\InvStockLog::CHANGE_TYPE_OUT_CLAIM;
|
||
}
|
||
}
|
||
|
||
(new InvStockLogService())->record($logData);
|
||
} catch (\Throwable $e) {
|
||
// 日志记录失败不影响主业务,仅记录错误
|
||
error_log('库存日志记录失败: ' . $e->getMessage());
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 获取车型list
|
||
*/
|
||
public function getList()
|
||
{
|
||
return $this->logic->getList();
|
||
}
|
||
} |