2025-12-05 09:22:53 +00:00
|
|
|
|
<?php
|
|
|
|
|
|
|
|
|
|
|
|
namespace app\common\service;
|
|
|
|
|
|
|
2026-01-05 10:42:08 +08:00
|
|
|
|
use app\common\logic\InvItemLogic;
|
2026-02-24 13:55:48 +08:00
|
|
|
|
use app\common\model\User;
|
2025-12-05 09:22:53 +00:00
|
|
|
|
use app\common\validate\InvItemValidate;
|
|
|
|
|
|
use app\common\traits\crud\CrudServiceTrait;
|
|
|
|
|
|
use app\common\model\InvItem;
|
2026-02-24 13:55:48 +08:00
|
|
|
|
use support\Redis;
|
2025-12-05 09:22:53 +00:00
|
|
|
|
use support\Request;
|
2025-12-09 07:33:04 +00:00
|
|
|
|
use support\exception\BusinessException;
|
2025-12-05 09:22:53 +00:00
|
|
|
|
|
|
|
|
|
|
class InvItemService
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
|
|
|
|
use CrudServiceTrait;
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 指定当前要操作的模型
|
|
|
|
|
|
*/
|
|
|
|
|
|
protected string $modelClass = InvItem::class;
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 验证器
|
|
|
|
|
|
*/
|
|
|
|
|
|
protected $validator = InvItemValidate::class;
|
|
|
|
|
|
|
2026-01-05 10:42:08 +08:00
|
|
|
|
protected $logic;
|
|
|
|
|
|
|
|
|
|
|
|
public function __construct()
|
|
|
|
|
|
{
|
|
|
|
|
|
$this->logic = (new InvItemLogic());
|
|
|
|
|
|
}
|
2025-12-05 09:22:53 +00:00
|
|
|
|
/**
|
|
|
|
|
|
* 获取验证器
|
|
|
|
|
|
*/
|
|
|
|
|
|
public function getValidator()
|
|
|
|
|
|
{
|
|
|
|
|
|
return new $this->validator;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-05 10:42:08 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 构造 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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-05 09:22:53 +00:00
|
|
|
|
/**
|
|
|
|
|
|
* 生成 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;
|
2026-01-07 09:42:08 +08:00
|
|
|
|
$data['stock_qty'] = $data['stock_qty']??1;
|
2025-12-05 09:22:53 +00:00
|
|
|
|
$data['location'] = '';
|
2026-02-24 13:55:48 +08:00
|
|
|
|
|
|
|
|
|
|
Redis::set('inv_store_u_stock', $request->user->uid, 'EX', 30);
|
|
|
|
|
|
|
2025-12-05 09:22:53 +00:00
|
|
|
|
return $data;
|
|
|
|
|
|
}
|
2026-02-24 13:55:48 +08:00
|
|
|
|
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']);
|
|
|
|
|
|
}
|
2025-12-09 07:33:04 +00:00
|
|
|
|
|
2026-02-24 16:15:49 +08:00
|
|
|
|
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);
|
|
|
|
|
|
}
|
2025-12-09 07:33:04 +00:00
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 变动物品库存数量
|
|
|
|
|
|
*
|
|
|
|
|
|
* @param int $itemId 物品ID
|
|
|
|
|
|
* @param int $changeQty 变动数量:>0 入库,<0 出库
|
|
|
|
|
|
* @param bool $allowNegative 是否允许库存变成负数,默认不允许
|
2026-02-24 11:28:12 +08:00
|
|
|
|
* @param array $options 额外选项(用于记录日志)
|
|
|
|
|
|
* - biz_type: 业务类型
|
|
|
|
|
|
* - biz_no: 单据号
|
|
|
|
|
|
* - operator_id: 操作人ID
|
|
|
|
|
|
* - operator_name: 操作人姓名
|
|
|
|
|
|
* - remark: 备注
|
2025-12-09 07:33:04 +00:00
|
|
|
|
* @return InvItem 最新的物品记录
|
|
|
|
|
|
* @throws BusinessException
|
|
|
|
|
|
*/
|
2026-02-24 11:28:12 +08:00
|
|
|
|
public function changeStock(int $itemId, int $changeQty, bool $allowNegative = false, array $options = []): InvItem
|
2025-12-09 07:33:04 +00:00
|
|
|
|
{
|
|
|
|
|
|
if ($changeQty === 0) {
|
|
|
|
|
|
throw new BusinessException('库存变动数量不能为 0');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-24 11:28:12 +08:00
|
|
|
|
// 获取变动前的库存(用于日志)
|
|
|
|
|
|
$itemBefore = InvItem::query()->find($itemId);
|
|
|
|
|
|
if (!$itemBefore) {
|
|
|
|
|
|
throw new BusinessException('物品不存在');
|
|
|
|
|
|
}
|
|
|
|
|
|
$beforeQty = $itemBefore->stock_qty;
|
|
|
|
|
|
|
2025-12-09 07:33:04 +00:00
|
|
|
|
$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);
|
2026-02-24 11:28:12 +08:00
|
|
|
|
|
|
|
|
|
|
// 记录库存变化日志
|
|
|
|
|
|
$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);
|
|
|
|
|
|
|
2025-12-09 07:33:04 +00:00
|
|
|
|
return $item;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 出库(减库存,默认不允许负数)
|
2026-02-24 11:28:12 +08:00
|
|
|
|
*
|
|
|
|
|
|
* @param int $itemId 物品ID
|
|
|
|
|
|
* @param int $qty 出库数量
|
|
|
|
|
|
* @param bool $allowNegative 是否允许负数
|
|
|
|
|
|
* @param array $options 日志选项(同 changeStock)
|
|
|
|
|
|
* @return InvItem
|
|
|
|
|
|
* @throws BusinessException
|
2025-12-09 07:33:04 +00:00
|
|
|
|
*/
|
2026-02-24 11:28:12 +08:00
|
|
|
|
public function stockOut(int $itemId, int $qty, bool $allowNegative = false, array $options = []): InvItem
|
2025-12-09 07:33:04 +00:00
|
|
|
|
{
|
|
|
|
|
|
if ($qty <= 0) {
|
|
|
|
|
|
throw new BusinessException('出库数量必须大于 0');
|
|
|
|
|
|
}
|
2026-02-24 11:28:12 +08:00
|
|
|
|
// 确定变动类型: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'] ?? '领用出库',
|
|
|
|
|
|
]));
|
2025-12-09 07:33:04 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 入库(加库存)
|
2026-02-24 11:28:12 +08:00
|
|
|
|
*
|
|
|
|
|
|
* @param int $itemId 物品ID
|
|
|
|
|
|
* @param int $qty 入库数量
|
|
|
|
|
|
* @param array $options 日志选项(同 changeStock)
|
|
|
|
|
|
* @return InvItem
|
|
|
|
|
|
* @throws BusinessException
|
2025-12-09 07:33:04 +00:00
|
|
|
|
*/
|
2026-02-24 11:28:12 +08:00
|
|
|
|
public function stockIn(int $itemId, int $qty, array $options = []): InvItem
|
2025-12-09 07:33:04 +00:00
|
|
|
|
{
|
|
|
|
|
|
if ($qty <= 0) {
|
|
|
|
|
|
throw new BusinessException('入库数量必须大于 0');
|
|
|
|
|
|
}
|
2026-02-24 11:28:12 +08:00
|
|
|
|
// 确定变动类型: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());
|
|
|
|
|
|
}
|
2025-12-09 07:33:04 +00:00
|
|
|
|
}
|
2026-01-05 10:42:08 +08:00
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 获取车型list
|
|
|
|
|
|
*/
|
|
|
|
|
|
public function getList()
|
|
|
|
|
|
{
|
|
|
|
|
|
return $this->logic->getList();
|
|
|
|
|
|
}
|
2025-12-05 09:22:53 +00:00
|
|
|
|
}
|