Files
OMS/app/ome/lib/branch/product.php
2026-01-04 19:08:31 +08:00

1537 lines
55 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
/**
* Copyright 2012-2026 ShopeX (https://www.shopex.cn)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* 物料仓库处理Lib
*
* @version 1.0
*/
class ome_branch_product extends ome_redis
{
var $export_name = '仓库库存';
private $db;
function __construct()
{
$this->db = kernel::database();
}
// redis需要回滚的《仓冻结》
static private $_redisFreezeRollbackList = [];
// redis需要回滚的《仓库存》
static private $_redisStoreRollbackList = [];
// redis的《冻结/库存流水》
static private $_redisFlow = [];
// 初始化redis需要回滚的《仓冻结》和《仓库存》
static public function initRedisBranch()
{
self::_initRedisFreezeRollbackList();
self::_initRedisStoreRollbackList();
}
// 初始化redis需要回滚的《仓冻结》
static private function _initRedisFreezeRollbackList()
{
self::$_redisFreezeRollbackList = [];
return true;
}
// 初始化redis需要回滚的《仓库存》
static private function _initRedisStoreRollbackList()
{
self::$_redisStoreRollbackList = [];
return true;
}
// 追加redis需要回滚的《仓冻结》
static private function _setRedisFreezeRollbackList($freezeRollbackReq=[])
{
if ($freezeRollbackReq) {
self::$_redisFreezeRollbackList[] = $freezeRollbackReq;
}
return true;
}
// 追加redis需要回滚的《仓库存》
static private function _setRedisStoreRollbackList($storeRollbackReq=[])
{
if ($storeRollbackReq) {
self::$_redisStoreRollbackList[] = $storeRollbackReq;
}
return true;
}
// 获取redis需要回滚的《仓冻结》
static private function _getRedisFreezeRollbackList()
{
return self::$_redisFreezeRollbackList;
}
// 获取redis需要回滚的《仓库存》
static private function _getRedisStoreRollbackList()
{
return self::$_redisStoreRollbackList;
}
// 删除《冻结/库存流水》并且 回滚redis的《仓冻结》和《仓库存》
static public function rollbackRedisBranch()
{
self::delRedisBranchFlow();
self::freezeRollbackInRedis();
self::storeRollbackInRedis();
}
// 获取redis《冻结/库存流水》的hash表名
public function getFlowHash($type = '')
{
if ($type == 'freeze') {
return sprintf('%s#branchFreezeFlow', base_shopnode::node_id('ome'));
} elseif ($type == 'store') {
return sprintf('%s#branchStoreFlow', base_shopnode::node_id('ome'));
} else {
return '';
}
}
// 临时保存redis《冻结流水》或者《库存流水》
static private function _setRedisFlow($_key, $_field, $_value)
{
return true;
return true;
return true;
$isRedis = parent::_connectRedis();
if (!$isRedis) {
return true;
}
// 只有在事务里才去保存临时流水,否则静态变量里的数据不会删除
if (!kernel::database()->isInTransaction()) {
return true;
}
$info = parent::$stockRedis->hget($_key, $_field);
if ($info) {
$_value = $info . ';' . $_value;
} else {
$key = $_key . '-' . $_field;
self::$_redisFlow[$key] = [
'flowHash' => $_key,
'flowKey' => $_field,
];
}
parent::$stockRedis->hset($_key, $_field, $_value);
}
// 删除redis《冻结流水》和redis《库存流水》
static public function delRedisBranchFlow()
{
$isRedis = parent::_connectRedis();
if (!$isRedis) {
return [true, 'isRedis is false'];
}
if (!self::$_redisFlow) {
return [true, 'branchProduct redisFlow is []'];
}
foreach (self::$_redisFlow as $ro_k => $ro_v) {
if ($ro_v['flowHash'] && $ro_v['flowKey']) {
parent::$stockRedis->hdel($ro_v['flowHash'], $ro_v['flowKey']);
unset(self::$_redisFlow[$ro_k]);
}
}
return [true, 'succ'];
}
/*
* 增加冻结库存
*/
function freez($branch_id,$product_id,$nums)
{
//暂时没有在branch_product上使用冻结库存
$this->chg_product_store_freeze($branch_id, $product_id, $nums, "+");
return true;
}
/*
* 释放冻结库存
*/
function unfreez($branch_id,$product_id,$nums)
{
//暂时没有在branch_product上使用冻结库存
return $this->chg_product_store_freeze($branch_id, $product_id, $nums, "-");
}
/*
* 修改冻结库存
*/
function chg_product_store_freeze($branch_id, $product_id, $num, $operator='=', $log_type='delivery')
{
// 改用freezeInRedis方法
return false;
return false;
return false;
$branchObj = app::get('ome')->model('branch_product');
if ($log_type == 'negative_stock' && !$branchObj->count(array('branch_id' => $branch_id, 'product_id' => $product_id))) {
$branch_arr = array();
$branchLib = kernel::single('ome_store_manage_branch');
$stores = $branchLib->getStoreByBranchId($branch_id);
$branch_arr['branch_id'] = $branch_id;
$branch_arr['product_id'] = $product_id;
$branch_arr['store_id'] = $stores['store_id'];
$branch_arr['store_bn'] = $stores['store_bn'];
$branch_arr['store'] = 0;
$branch_arr['last_modified'] = time();
$branchObj->insert($branch_arr);
}
$filter = array("branch_id=$branch_id","product_id=$product_id");
$UpdateValues = array('last_modified='.time());
switch($operator)
{
case "+":
$UpdateValues[] = "store_freeze=IFNULL(store_freeze,0)+$num";
if ($log_type != 'negative_stock') {
$filter[] = "store>=store_freeze+$num";
}
break;
case "-":
$UpdateValues[] = " store_freeze=IF((CAST(store_freeze AS SIGNED)-$num)>0,store_freeze-$num,0)";
break;
case "=":
default:
$UpdateValues[] = "store_freeze=$num";
break;
}
$sql = 'UPDATE `' . DB_PREFIX . 'ome_branch_product` SET '.implode(',',$UpdateValues).' WHERE '.implode(' AND ', $filter);
if($this->db->exec($sql)){
$affect_row = $this->db->affect_row();
// 先针对冻结库存处理影响行数。发货时有库存减再减冻结可能时间一样影响行数为0发货不做影响行数限制
if ($operator == '+') {
return $affect_row === 1 ? true : false;
}
return true;
}else{
return false;
}
}
/*
* 更新仓库库存
* redis高并发以后已弃用改用storeInRedis方法
*/
function change_store($branch_id, $product_id, $num, $operator='=', $update_material=true,$negative_stock=false)
{
return false;
return false;
return false;
$now = time();
$store = '';
$where = '';
switch($operator){
case "+":
$store = "store=IFNULL(store,0)+".$num;
break;
case "-":
if ($negative_stock === true) {
$store = "store=store-$num ";
} else {
$store = " store=IF((CAST(store AS SIGNED)-$num)>0,store-$num,0) ";
$where .= ' AND store>=' . $num;
}
break;
case "=":
default:
$store = "store=".$num;
break;
}
$sql = "UPDATE ".DB_PREFIX."ome_branch_product SET ".$store.",last_modified=".$now."
WHERE product_id=".$product_id." AND branch_id=".$branch_id.$where;
$result = $this->db->exec($sql);
if($result)
{
$rs = $this->db->affect_row();
if(is_numeric($rs) && $rs > 0){
if($update_material == false)
{
return true;#不统计基础物料库存
}
return $this->count_store($product_id);
}
}
return false;
}
/*
* 统计所有此基础物料库存
* redis库存高可用废弃掉直接修改db库存、冻结的方法
*/
function count_store($product_id, $branch_id=0)
{
return false;
return false;
return false;
$mStock = app::get('material')->model('basic_material_stock');
$time = time();
$sql = "SELECT product_id, SUM(store) AS 'store' FROM ".DB_PREFIX."ome_branch_product WHERE product_id='".$product_id."' GROUP BY product_id";
$row = $this->db->selectrow($sql);
if(!$row)
{
$data = array(
'bm_id' => $product_id,
'store' => 0,
);
}
else
{
$data = array(
'bm_id' => $row['product_id'],
'store' => $row['store'],
);
}
$data['last_modified'] = $time;
$data['real_store_lastmodify'] = $time;
$data['max_store_lastmodify'] = $time;
# [更新]基础物料库存
$mStock->save($data);
return true;
}
#获取基础物料对应仓库库存
function getStoreByBranch($product_id, $branch_id)
{
$sql = 'select store from '. DB_PREFIX .'ome_branch_product where product_id='.$product_id.' and branch_id='.$branch_id;
$row = $this->db->selectRow($sql);
if($row)
{
return $row['store'];
}
else
{
return false;
}
}
#获取单仓库-多个基础物料仓库
function getStoreListByBranch($branch_id, $product_ids)
{
if(empty($product_ids) || empty($branch_id) || !is_array($product_ids)) {
return [];
}
$sql = 'select product_id,store from '. DB_PREFIX .'ome_branch_product where product_id in('.implode(',', $product_ids).') and branch_id='.$branch_id;
$rows = $this->db->select($sql);
if($rows)
{
$products = array();
foreach($rows as $row)
{
$products[$row['product_id']] = $row['store'];
}
return $products;
}
else
{
return [];
}
}
#获取单仓库-多个基础物料的可用库存
function getAvailableStore($branch_id, $product_ids)
{
$basicMStockFreezeLib = kernel::single('material_basic_material_stock_freeze');
$sql = 'select product_id, branch_id, store,store_freeze from '. DB_PREFIX .'ome_branch_product where product_id in('.implode(',', $product_ids).') and branch_id='.$branch_id;
$rows = $this->db->select($sql);
if($rows)
{
$products = array();
foreach($rows as $row)
{
//根据仓库ID、基础物料ID获取该物料仓库级的预占
$row['store_freeze'] = $basicMStockFreezeLib->getBranchFreeze($row['product_id'], $row['branch_id']);
$products[$row['product_id']] = $row['store'] - $row['store_freeze'];
}
return $products;
}
else
{
return false;
}
}
#获取单仓库-单个基础物料中的可用库存
function get_available_store($branch_id, $product_id)
{
$basicMStockFreezeLib = kernel::single('material_basic_material_stock_freeze');
$bpModel = app::get('ome')->model('branch_product');
$branch = $bpModel->getList('store, store_freeze',array('product_id'=>$product_id,'branch_id'=>$branch_id), 0, 1);
//根据仓库ID、基础物料ID获取该物料仓库级的预占
$branch[0]['store_freeze'] = $basicMStockFreezeLib->getBranchFreeze($product_id, $branch_id);
return $branch[0]['store'] - $branch[0]['store_freeze'];
}
#查询对应库存总数
function countBranchProduct($product_id, $column='safe_store')
{
$sql = "SELECT SUM($column) AS 'total' FROM ". DB_PREFIX ."ome_branch_product WHERE product_id = $product_id ";
$count = $this->db->selectrow($sql);
return $count['total'];
}
/**
* 初始化库存数量为NULL的货品
*/
public function initNullStore($product_id, $branch_id)
{
if($product_id)
{
if($branch_id) {
$sql = "UPDATE ". DB_PREFIX ."ome_branch_product SET store=0 WHERE branch_id='" . $branch_id . "' AND product_id='" . $product_id ."' AND ISNULL(store) LIMIT 1";
}else{
$sql = "UPDATE ". DB_PREFIX ."ome_branch_product SET store=0 WHERE product_id='" . $product_id ."' AND ISNULL(store) LIMIT 1";
}
return $this->db->exec($sql);
}
else
{
return false;
}
}
/*------------------------------------------------------ */
//-- 以下是不常用方法
/*------------------------------------------------------ */
/* 减仓库表库存
* 备注:货位库存相关方法,可以删除此方法
* \app\purchase\model\returned\purchase.php 中使用
*/
function Cut_store($adata)
{
$libBranchProductPos = kernel::single('ome_branch_product_pos');
foreach($adata['items'] as $k=>$v)
{
$libBranchProductPos->change_store($adata['branch_id'],$v['product_id'],$v['pos_id'],$v['num'],'-');
}
}
/*
* 备注:货位库存相关方法,可以删除此方法
* \app\purchase\model\appropriation.php 中使用
*/
function operate_store($adata,$operate)
{
$libBranchProductPos = kernel::single('ome_branch_product_pos');
if($operate=='add')
{
$libBranchProductPos->change_store($adata['branch_id'],$adata['product_id'],$adata['pos_id'],$adata['num'],'+');
}
else if($operate=='lower')
{
$libBranchProductPos->change_store($adata['branch_id'],$adata['product_id'],$adata['pos_id'],$adata['num'],'-');
}
}
/*
* 更改在途库存
*/
function change_arrive_store($branch_id, $product_id, $num, $type='+')
{
$now = time();
$store = "";
switch($type)
{
case "+":
$store = "arrive_store=IFNULL(arrive_store,0)+".$num;
break;
case "-":
$store = " arrive_store=IF((CAST(arrive_store AS SIGNED)-$num)>0,arrive_store-$num,0) ";
break;
case "=":
default:
$store = "arrive_store=".$num;
break;
}
$branch_pro = $this->db->selectrow("SELECT arrive_store FROM sdb_ome_branch_product WHERE product_id=".$product_id." AND branch_id=".$branch_id."");
if($branch_pro){
$sql = "UPDATE ".DB_PREFIX."ome_branch_product SET ".$store." WHERE product_id=".$product_id." AND branch_id=".$branch_id;
$rs = $this->db->exec($sql);
}else{
if($type == '-' || $num < 0) {
return false;
}
$branchLib = kernel::single('ome_store_manage_branch');
$stores = $branchLib->getStoreByBranchId($branch_id);
$bp = array(
'branch_id' => $branch_id,
'product_id' => $product_id,
'store' => 0,
'store_freeze' => 0,
'last_modified' => time(),
'arrive_store' => $num,
'safe_store' => 0,
'store_id' => $stores['store_id'],
'store_bn' => $stores['store_bn'],
);
$obranch_product = app::get('ome')->model('branch_product');
$rs = $obranch_product->insert($bp);
}
return $rs;
}
/*
* 备注:货位相关方法,可以删除此方法
* \app\purchase\controller\admin\stock.php
*/
function Get_pos_id($branch_id,$store_position)
{
$obranch_pos = app::get('ome')->model('branch_pos');
$pos = $obranch_pos->dump(array('branch_id'=>$branch_id,'store_position'=>$store_position),'pos_id');
return $pos['pos_id'];
}
/*
* 获取货品在对应仓库中的库存
*
* @param int $product_id 货品id
*
* @return array
*/
function get_branch_store($product_id)
{
$basicMStockFreezeLib = kernel::single('material_basic_material_stock_freeze');
$ret = array();
$branch_product = $this->db->select("SELECT * FROM sdb_ome_branch_product WHERE product_id=".intval($product_id));
if($branch_product)
{
foreach($branch_product as $v){
//根据仓库ID、基础物料ID获取该物料仓库级的预占
$v['store_freeze'] = $basicMStockFreezeLib->getBranchFreeze($v['product_id'], $v['branch_id']);
//将订单确认拆分的仓库货品数量由store改为store_freeze //因冻结存在负数情况会出现a(b)=a+b的情况
$store = max(0,$v['store']-abs($v['store_freeze']));
$ret[$v['branch_id']] = $store;
}
}
return $ret;
}
/**
* 分仓查询可售库存
* @Author: xueding
* @Vsersion: 2022/7/11 下午9:16
* @param $product_ids
* @param $branch_ids
* @return array
*/
function getBranchStoreList($product_ids,$branch_ids)
{
$ret = array();
$branch_product = app::get('ome')->model('branch_product')->getList('*',array('product_id'=>$product_ids,'branch_id'=>$branch_ids));
$sql = "SELECT sum(num) as total,branch_id,bm_id FROM sdb_material_basic_material_stock_freeze WHERE bm_id IN (".implode(',',$product_ids).") AND obj_type='1' AND branch_id IN (".implode(',',$branch_ids).") GROUP BY branch_id,bm_id";
$branchStoreFree = app::get('material')->model('basic_material_stock_freeze')->db->select($sql);
$branchStoreFreeS = ome_func::filter_by_value($branchStoreFree,'bm_id');
if($branch_product)
{
foreach($branch_product as $v){
//根据仓库ID、基础物料ID获取该物料仓库级的预占
$branchStoreFreeNum = array_column($branchStoreFreeS[$v['product_id']],null,'branch_id');
$v['store_freeze'] = $branchStoreFreeNum[$v['branch_id']]['total'];
//将订单确认拆分的仓库货品数量由store改为store_freeze
$store = max(0,$v['store']-$v['store_freeze']);
$ret[$v['product_id']][$v['branch_id']] = $store;
}
}
return $ret;
}
/*
* 根据仓库ID和货品ID 获取相应的库存数量
*
* @param int $product_id 货品id
*
* @return array
*/
function get_product_store($branch_id, $product_id)
{
$basicMStockFreezeLib = kernel::single('material_basic_material_stock_freeze');
$sql = "SELECT * FROM sdb_ome_branch_product WHERE product_id=".intval($product_id)." AND branch_id=".intval($branch_id);
$branch_product = $this->db->selectrow($sql);
//根据仓库ID、基础物料ID获取该物料仓库级的预占
$branch_product['store_freeze'] = $basicMStockFreezeLib->getBranchFreeze($product_id, $branch_id);
$sale_store = $branch_product['store'] - $branch_product['store_freeze'];
return $sale_store;
}
/**
* [人工预占库存]订单创建
* @todo场景平台推送的订单明细中指定仓库编码进行发货,需要提前进行仓库级库存冻结
*
* @param array $orderItems
* @param int $branch_id
* @param string $error_msg
* @return bool
*/
public function order_artificial_freeze($orderItems, $branch_id, &$error_msg='')
{
$artFreezeObj = app::get('material')->model('basic_material_stock_artificial_freeze');
$freeGroupObj = app::get('material')->model('basic_material_stock_artificial_freeze_group');
$operLogObj = app::get('ome')->model('operation_log');
$storeManageLib = kernel::single('ome_store_manage');
//check
if(empty($orderItems) || empty($branch_id)){
$error_msg = '没有可操作的数据';
return false;
}
//system账号信息
$opInfo = kernel::single('ome_func')->get_system();
//人工库存预占组
$group_name = '指定仓发货';
$groupInfo = $freeGroupObj->dump(array('group_name'=>$group_name), 'group_id');
$group_id = intval($groupInfo['group_id']);
if(empty($group_id)){
$sdf = array(
'group_name' => '订单创建',
);
$freeGroupObj->insert($sdf);
$group_id = $freeGroupObj->db->lastInsertId();
}
//params
$params = array();
$params['node_type'] = 'artificialFreeze';
$storeManageLib->loadBranch(array('branch_id'=>$branch_id));
//order_items
foreach ($orderItems as $key => $val)
{
$sdf = array(
'branch_id' => $branch_id,
'bm_id' => $val['product_id'],
'freeze_num' => $val['nums'],
'freeze_reason' => '订单指定仓库发货',
'freeze_time' => time(),
'update_modified' => time(),
'op_id' => $opInfo['op_id'],
'original_bn' => $val['order_bn'],
'original_type' => 'create_order',
'group_id' => $group_id,
'shop_id' => $val['shop_id'], //货品预占流水记录上的shop_id
'bn' => $val['bn'],
);
$result = $artFreezeObj->insert($sdf);
if(!$result){
$error_msg = sprintf('插入人工预占流水失败,货号:%s', $val['bn']);
return false;
}
$bmsaf_id = $sdf['bmsaf_id'];
//log
$operLogObj->write_log('add_artificial_freeze@ome', $bmsaf_id, '订单指定仓库发货,新增人工库存预占记录');
//货品预占信息
$sdf['obj_id'] = $bmsaf_id;
$params['params'][] = $sdf;
}
//人工库存预占
$result = $storeManageLib->processBranchStore($params, $error_msg);
if(!$result){
$error_msg = '人工库存预占失败';
return false;
}
return true;
}
/**
* [释放人工预占库存]订单取消
*
* @param array $orderItems
* @param int $branch_id
* @param string $error_msg
* @return bool
*/
public function order_artificial_unfreeze($orderInfo, $branch_id, &$error_msg='')
{
$artFreezeObj = app::get('material')->model('basic_material_stock_artificial_freeze');
$operLogObj = app::get('ome')->model('operation_log');
$storeManageLib = kernel::single('ome_store_manage');
$original_bn = $orderInfo['order_bn'];
//check
if(empty($original_bn)){
$error_msg = '没有可释放的数据';
return false;
}
if(empty($branch_id)){
$error_msg = '没有指定仓库';
return false;
}
//system账号信息
$opInfo = kernel::single('ome_func')->get_system();
//filter
$filter = array (
'original_bn' => $original_bn, //订单号
'original_type' => 'create_order', //预占类型
);
$artFreezeList = $artFreezeObj->getList('*', $filter);
if(empty($artFreezeList)){
$error_msg = '没有可释放的预占记录';
return false;
}
//params
$params = array();
$params['node_type'] = 'artificialUnfreeze';
$storeManageLib->loadBranch(array('branch_id'=>$branch_id));
$trans = kernel::database()->beginTransaction();
//unfreeze
foreach ($artFreezeList as $key => $val)
{
$bmsaf_id = $val['bmsaf_id'];
//check
if($branch_id && $val['branch_id'] != $branch_id){
continue; //指定仓库释放
}
if($val['status'] != '1'){
continue; //已释放的则跳过
}
//update
$updateData = array('status'=>'2', 'op_id'=>$opInfo['op_id'], 'update_modified'=>time());
$affect_rows = $artFreezeObj->update($updateData, array('bmsaf_id'=>$bmsaf_id));
if ($affect_rows !== 1) {
$error_msg = '货号ID'. $val['bm_id'] .' 释放预占失败';
kernel::database()->rollBack();
return false;
}
//log
$operLogObj->write_log('add_artificial_freeze@ome', $bmsaf_id, '取消订单,释放人工库存预占记录');
//货品预占信息
$val['obj_id'] = $bmsaf_id;
//不用释放冻结库存,否则会返回“货品冻结释放失败!”
$val['not_unfreeze'] = true;
$params['params'][] = $val;
}
//释放人工库存预占
$result = $storeManageLib->processBranchStore($params, $error_msg);
if(!$result){
$error_msg = '释放人工库存预占失败';
kernel::database()->rollBack();
return false;
}
kernel::database()->commit($trans);
return true;
}
/**
* 扣减冻结
*
* @params array $items,
* 示例:[['branch_id'=>1,'product_id'=>1,'quantity'=>1,'bn'=>'test001','obj_type'=>'对象类型', 'bill_type'=>'业务类型','obj_id'=>'对象ID','obj_bn'=>'业务单号','log_type'=>'允许冻结大于库存传negative_stock'],['branch_id'=>1,'product_id'=>2,'quantity'=>1,'bn'=>'test002','obj_type'=>'对象类型', 'bill_type'=>'业务类型','obj_id'=>'对象ID','obj_bn'=>'业务单号','log_type'=>'']]
* @params string $opt可选值: +、-
* @return array
*/
static public function freezeInRedis($items, $opt, $source='')
{
$node_id = base_shopnode::node_id('ome');
if (!in_array($opt, ['+', '-'])){
return [false, '仓冻结操作符错误:'.$opt];
}
if ($opt == '+') {
$ratio = 1;
$lua = file_get_contents(app::get('ome')->app_dir.'/lua/branch_store_freeze_incr.lua');
$title = '增加仓冻结';
} else {
$ratio = -1;
$lua = file_get_contents(app::get('ome')->app_dir.'/lua/branch_store_freeze_decr.lua');
$title = '扣减仓冻结';
}
$flowHash = kernel::single('ome_branch_product')->getFlowHash('freeze');
// todo 需要加参数验证
$fItems = [];
foreach ($items as $item) {
if (!$item['branch_id']) {
return [false, '参数错误-branch_id空'.json_encode($item)];
} elseif (!$item['product_id']) {
return [false, '参数错误-bm_id空'.json_encode($item)];
} elseif (!$item['quantity']) {
$bm_bn = self::getbmBn($item['product_id']);
return [false, '参数错误-quantity空[bm_bn=>'.$bm_bn.']'.json_encode($item)];
}
$key = sprintf('%s#stock:%s:%s', $node_id, $item['branch_id'], $item['product_id']);
// 扣减冻结为负数
$item['quantity'] = abs($item['quantity']) * $ratio;
$flowKey = sprintf('%s#%s#%s#%s#%s', $item['obj_type'], $item['bill_type'], $item['obj_id'], $opt, time());
if (isset($fItems[$key])) {
$fItems[$key]['quantity'] += $item['quantity'];
if (isset($item['log_type']) && $item['log_type'] == 'negative_stock') {
$fItems[$key]['log_type'] = 'negative_stock';
}
} else {
if (!isset($item['log_type'])) {
$item['log_type'] = '';
}
$fItems[$key] = $item;
}
// 查库存数,用于 增加仓冻结 的时候冻结数如果超过库存数redis返回报错
$fItems[$key]['store_quantity'] = 0;
if ($opt == '+') {
$branchProductStore = kernel::database()->selectrow("SELECT store FROM sdb_ome_branch_product WHERE branch_id = " . $item['branch_id'] . " AND product_id = ".$item['product_id']);
$fItems[$key]['store_quantity'] = $branchProductStore ? $branchProductStore['store'] : 0;
}
}
if (!$fItems) {
return [true, '数据为空'];
}
// 根据product_id对数组进行升序排序
uasort($fItems, function($a, $b) {
return $a['product_id'] - $b['product_id'];
});
$flowValue = [];
foreach ($fItems as $_k => $_v) {
$tmp = [
0 => $_v['branch_id'],
1 => $_v['product_id'],
2 => abs($_v['quantity']),
// 3 => time(),
];
$flowValue[] = implode(':', $tmp);
}
$flowValue = implode(';', $flowValue);
$isRedis = parent::_connectRedis();
$code = null;
// 1. 更新Redis
if ($isRedis) {
$args = array_merge(array_keys($fItems), array_column($fItems, 'quantity'), array_column($fItems, 'store_quantity'), array_column($fItems, 'log_type'));
list($code, $msg, $sku_id) = $rr = parent::$stockRedis->eval($lua, $args, count($fItems));
if ($code === false) {
return [false, $title.'失败:脚本异常'];
}
if ($code > 100) {
$bm_bn = $fItems[$sku_id]['bn'] ? $fItems[$sku_id]['bn'] : self::getbmBn($fItems[$sku_id]['product_id']);
return [false, '【'.$bm_bn.'】'.$title.'失败:'.$msg];
}
// 存redis流水
self::_setRedisFlow($flowHash, $flowKey, $flowValue);
}
// 2. 如果Redis不存在更新数据库
if (!$isRedis || $code == 100){
$db = kernel::database();
foreach ($fItems as $key => $item) {
/*
$sql = "UPDATE `" . DB_PREFIX . "ome_branch_product`
SET `last_modified`=".time().",`store_freeze`=IF((CAST(`store_freeze` AS SIGNED)+{$item['quantity']})>0,`store_freeze`+{$item['quantity']},0)
WHERE `branch_id`={$item['branch_id']} AND `product_id`={$item['product_id']}";
*/
$branchObj = app::get('ome')->model('branch_product');
if ($item['log_type'] == 'negative_stock' && !$branchObj->count(array('branch_id' => $item['branch_id'], 'product_id' => $item['product_id']))) {
$branch_arr = array();
$branchLib = kernel::single('ome_store_manage_branch');
$stores = $branchLib->getStoreByBranchId($item['branch_id']);
$branch_arr['branch_id'] = $item['branch_id'];
$branch_arr['product_id'] = $item['product_id'];
$branch_arr['store_id'] = $stores['store_id'];
$branch_arr['store_bn'] = $stores['store_bn'];
$branch_arr['store'] = 0;
$branch_arr['last_modified'] = time();
$branchObj->insert($branch_arr);
}
// 修改成允许store_freeze减成负数
$sql = "UPDATE `" . DB_PREFIX . "ome_branch_product`
SET `last_modified`=".time().",`store_freeze`=`store_freeze`+{$item['quantity']}
WHERE `branch_id`={$item['branch_id']} AND `product_id`={$item['product_id']}";
// 如果是增加冻结需要判断库存是否足够
if ($item['quantity'] > 0 && $item['log_type'] != 'negative_stock'){
$sql .= " AND `store` >= `store_freeze` + {$item['quantity']}" ;
}
if (!$db->exec($sql)){
$bm_bn = $item['bn'] ? $item['bn'] : self::getbmBn($item['product_id']);
return [false, '【'.$bm_bn.'】'.$title.'失败:'.$db->errorinfo()];
}
if (1 !== $db->affect_row()){
$bm_bn = $item['bn'] ? $item['bn'] : self::getbmBn($item['product_id']);
return [false, '【'.$bm_bn.'】'.$title.'失败:仓冻结不足'];
}
}
}
// 3. 数据库更新成功做SET
if ($isRedis && $code == 100) {
$args = [];
foreach ($fItems as $key => $item) {
$row = $db->selectrow("SELECT `store`,`store_freeze` FROM `" . DB_PREFIX . "ome_branch_product` WHERE `branch_id`={$item['branch_id']} AND `product_id`={$item['product_id']}");
if (!$row) {
continue;
}
$args[] = $row['store'].','.$row['store_freeze'];
}
if ($args) {
$lua = file_get_contents(app::get('ome')->app_dir.'/lua/branch_store_set.lua');
$args = array_merge(array_keys($fItems), $args);
parent::$stockRedis->eval($lua, $args, count($fItems));
}
}
// 4. 如果走队列并没有更新数据库生成冻结流水,方便后台任务读取并更新
if ($isRedis && $code != 100){
$freezeQueueMdl = app::get('ome')->model('branch_freeze_queue');
foreach ($fItems as $key => $item) {
$queue = [
'branch_id' => $item['branch_id'],
'product_id' => $item['product_id'],
'quantity' => $item['quantity'],
'obj_type' => $item['obj_type'],
'bill_type' => $item['bill_type'],
'obj_id' => $item['obj_id'],
'obj_bn' => $item['obj_bn'],
'source' => $source,
// 'obj_item_id'=> $item['obj_item_id'],
];
$freezeQueueMdl->insert($queue);
}
}
// 业务roback以后需要对redis回滚freezeRollbackReq是需要回滚的参数。
if ($isRedis) {
$freezeRollbackReq = [
'items' => $items,
'opt' => $opt == '+' ? '-' : '+', // 回滚需要逆向
'source' => $source, // __CLASS__.'::'.__FUNCTION__
];
self::_setRedisFreezeRollbackList($freezeRollbackReq);
}
return [true, $title.'成功'];
}
/**
* 扣减库存
*
* @params array $items,
* 示例:[['branch_id'=>1,'product_id'=>1,'quantity'=>1,'bn'=>'test001','iostock_bn'=>'出入库单号','negative_stock'=>true/false],['branch_id'=>1,'product_id'=>2,'quantity'=>1,'bn'=>'test002','iostock_bn'=>'出入库单号']]
* negative_stock 是否允许负库存true/false
* @params string $opt可选值: +、-
* @return void
*/
static public function storeInRedis($items, $opt, $source='')
{
// redis库存高可用迭代掉本类的change_store方法
$node_id = base_shopnode::node_id('ome');
if (!in_array($opt, ['+', '-'])){
return [false, '仓库存操作符错误:'.$opt];
}
if ($opt == '+') {
$ratio = 1;
$lua = file_get_contents(app::get('ome')->app_dir.'/lua/branch_store_incr.lua');
$title = '增加仓库存';
} else {
$ratio = -1;
$lua = file_get_contents(app::get('ome')->app_dir.'/lua/branch_store_decr.lua');
$title = '扣减仓库存';
}
$flowHash = kernel::single('ome_branch_product')->getFlowHash('store');
// todo 需要加参数验证
$fItems = [];
foreach ($items as $item) {
if (!$item['branch_id']) {
return [false, '参数错误-branch_id空'.json_encode($item)];
} elseif (!$item['product_id']) {
return [false, '参数错误-bm_id空'.json_encode($item)];
} elseif (!$item['quantity']) {
$bm_bn = self::getbmBn($item['product_id']);
return [false, '参数错误-quantity空[bm_bn=>'.$bm_bn.']'.json_encode($item)];
}
$key = sprintf('%s#stock:%s:%s', $node_id, $item['branch_id'], $item['product_id']);
$flowKey = sprintf('%s#%s#%s', $item['iostock_bn'], $opt, time());
// 扣减冻结为负数
$item['quantity'] = abs($item['quantity']) * $ratio;
if (isset($fItems[$key])) {
$fItems[$key]['quantity'] += $item['quantity'];
if ($item['negative_stock'] === true){
$fItems[$key]['negative_stock'] = 'true';
}
} else {
$fItems[$key] = $item;
$fItems[$key]['negative_stock'] = ($item['negative_stock'] && $item['negative_stock'] === true) ? 'true' : 'false';
}
}
if (!$fItems) {
return [true, '数据为空'];
}
// 根据product_id对数组进行升序排序
uasort($fItems, function($a, $b) {
return $a['product_id'] - $b['product_id'];
});
$flowValue = [];
foreach ($fItems as $_k => $_v) {
$tmp = [
0 => $_v['branch_id'],
1 => $_v['product_id'],
2 => abs($_v['quantity']),
// 3 => time(),
];
$flowValue[] = implode(':', $tmp);
}
$flowValue = implode(';', $flowValue);
// 1. 更新数据库
$db = kernel::database();
foreach ($fItems as $key => $item) {
$where = "`branch_id`={$item['branch_id']} AND `product_id`={$item['product_id']}";
if ($opt == '+') {
$sql_set = "`store`=IFNULL(store,0)+{$item['quantity']}";
} else {
if ($item['negative_stock'] == 'true') {
$sql_set = "`store`=`store`+{$item['quantity']}";
} else {
$sql_set = "`store`=IF((CAST(`store` AS SIGNED)+{$item['quantity']})>0,`store`+{$item['quantity']},0)";
$where .= " AND `store` + {$item['quantity']} >=0";
}
}
$sql_set .= ",`last_modified`=".time();
/*
// 扣减库存的时候需要把冻结一起扣减掉
if ($item['quantity'] < 0){
$sql_set .= ", `store_freeze`=IF((CAST(`store_freeze` AS SIGNED)+{$item['quantity']})>0,`store_freeze`+{$item['quantity']},0)";
}
*/
$sql = "UPDATE `" . DB_PREFIX . "ome_branch_product` SET " . $sql_set . " WHERE " . $where;
if (!$db->exec($sql)){
$bm_bn = $item['bn'] ? $item['bn'] : self::getbmBn($item['product_id']);
return [false, '【'.$bm_bn.'】'.$title.'失败:'.$db->errorinfo()];
}
if (1 !== $db->affect_row()){
$bm_bn = $item['bn'] ? $item['bn'] : self::getbmBn($item['product_id']);
return [false, '【'.$bm_bn.'】'.$title.'失败:仓库存不足'];
}
}
$isRedis = parent::_connectRedis();
$code = null;
// 1. 更新Redis
if ($isRedis) {
$args = array_merge(array_keys($fItems), array_column($fItems, 'quantity'), array_column($fItems, 'negative_stock'));
list($code, $msg, $sku_id) = $rr = parent::$stockRedis->eval($lua, $args, count($fItems));
if ($code === false) {
return [false, $title.'失败:脚本异常'];
}
if ($code > 100) {
$bm_bn = $fItems[$sku_id]['bn'] ? $fItems[$sku_id]['bn'] : self::getbmBn($fItems[$sku_id]['product_id']);
return [false, '【'.$bm_bn.'】'.$title.'失败:'.$msg];
}
// 存redis流水
self::_setRedisFlow($flowHash, $flowKey, $flowValue);
}
// // 2. 如果Redis不存在更新数据库
// if (!$isRedis || $code == 100){
// $db = kernel::database();
// foreach ($fItems as $key => $item) {
// $sql_set = "`last_modified`=".time().",`store`=IF((CAST(`store` AS SIGNED)+{$item['quantity']})>0,`store`+{$item['quantity']},0)";
// /*
// // 扣减库存的时候需要把冻结一起扣减掉
// if ($item['quantity'] < 0){
// $sql_set .= ", `store_freeze`=IF((CAST(`store_freeze` AS SIGNED)+{$item['quantity']})>0,`store_freeze`+{$item['quantity']},0)";
// }
// */
// $sql = "UPDATE `" . DB_PREFIX . "ome_branch_product`
// SET " . $sql_set . "
// WHERE `branch_id`={$item['branch_id']} AND `product_id`={$item['product_id']}";
// // 如果是扣减库存需要判断库存是否足够
// if ($item['quantity'] < 0){
// $sql .= " AND `store` + {$item['quantity']} >=0" ;
// }
// if (!$db->exec($sql)){
// return [false, '【'.$item['bn'].'】'.$title.'失败:'.$db->errorinfo()];
// }
// if (1 !== $db->affect_row()){
// return [false, '【'.$item['bn'].'】'.$title.'失败:仓库存不足'];
// }
// }
// }
// 3. Redis不存在做SET
if ($isRedis && $code == 100) {
$args = [];
foreach ($fItems as $key => $item) {
$row = $db->selectrow("SELECT `store`,`store_freeze` FROM `" . DB_PREFIX . "ome_branch_product` WHERE `branch_id`={$item['branch_id']} AND `product_id`={$item['product_id']}");
$args[] = $row['store'].','.$row['store_freeze'];
}
$lua = file_get_contents(app::get('ome')->app_dir.'/lua/branch_store_set.lua');
$args = array_merge(array_keys($fItems), $args);
parent::$stockRedis->eval($lua, $args, count($fItems));
}
// // 4. 如果走队列并没有更新数据库生成库存变更流水,方便后台任务读取并更新
// if ($isRedis && $code != 100){
// $stockQueueMdl = app::get('ome')->model('branch_stock_queue');
// foreach ($fItems as $key => $item) {
// $queue = [
// 'branch_id' => $item['branch_id'],
// 'product_id' => $item['product_id'],
// 'quantity' => $item['quantity'],
// 'iostock_bn' => $item['iostock_bn'],
// ];
// $stockQueueMdl->insert($queue);
// }
// }
// 业务rollback以后需要对redis回滚storeRollbackReq是需要回滚的参数。
if ($isRedis) {
$storeRollbackReq = [
'items' => $items,
'opt' => $opt == '+' ? '-' : '+', // 回滚需要逆向
'source' => $source, // __CLASS__.'::'.__FUNCTION__
];
self::_setRedisStoreRollbackList($storeRollbackReq);
}
return [true, $title.'成功'];
}
/**
* redis冻结回滚
*/
static public function freezeRollbackInRedis()
{
if (!self::_getRedisFreezeRollbackList()) {
return [true, 'branchProduct redisFreezeRollbackList is []'];
}
foreach (self::_getRedisFreezeRollbackList() as $ro_value) {
$items = $ro_value['items'];
$opt = $ro_value['opt'];
self::_freezeRollbackInRedis($items, $opt);
}
self::_initRedisFreezeRollbackList();
return [true, 'redis仓库存回滚成功'];
}
/**
* redis冻结回滚
*
* @params array $items,
* 示例:[['branch_id'=>1,'product_id'=>1,'quantity'=>1,'bn'=>'test001'],['branch_id'=>1,'product_id'=>2,'quantity'=>1,'bn'=>'test002']]
* @params string $opt可选值: +、-
* @return void
*/
static private function _freezeRollbackInRedis($items, $opt)
{
$node_id = base_shopnode::node_id('ome');
if (!in_array($opt, ['+', '-'])){
return [false, '(回滚)仓冻结操作符错误:'.$opt];
}
if ($opt == '+') {
$ratio = 1;
$lua = file_get_contents(app::get('ome')->app_dir.'/lua/branch_store_freeze_incr.lua');
$title = '(回滚)增加仓冻结';
} else {
$ratio = -1;
$lua = file_get_contents(app::get('ome')->app_dir.'/lua/branch_store_freeze_decr.lua');
$title = '(回滚)扣减仓冻结';
}
// todo 需要加参数验证
$fItems = [];
foreach ($items as $item) {
if (!$item['branch_id']) {
return [false, '(回滚)参数错误-branch_id空'.json_encode($item)];
} elseif (!$item['product_id']) {
return [false, '(回滚)参数错误-bm_id空'.json_encode($item)];
} elseif (!$item['quantity']) {
$bm_bn = self::getbmBn($item['product_id']);
return [false, '(回滚)参数错误-quantity空[bm_bn=>'.$bm_bn.']'.json_encode($item)];
}
$key = sprintf('%s#stock:%s:%s', $node_id, $item['branch_id'], $item['product_id']);
// 扣减冻结为负数
$item['quantity'] = abs($item['quantity']) * $ratio;
if (isset($fItems[$key])) {
$fItems[$key]['quantity'] += $item['quantity'];
} else {
$fItems[$key] = $item;
}
}
$isRedis = parent::_connectRedis();
$code = null;
if (!$isRedis) {
return [true, '冻结回滚连接redis失败'];
}
// 1. 更新Redis
if ($isRedis) {
$args = array_merge(array_keys($fItems), array_column($fItems, 'quantity'));
list($code, $msg, $sku_id) = $rr = parent::$stockRedis->eval($lua, $args, count($fItems));
if ($code === false) {
return [false, $title.'失败:脚本异常'];
}
if ($code > 100) {
$bm_bn = $fItems[$sku_id]['bn'] ? $fItems[$sku_id]['bn'] : self::getbmBn($fItems[$sku_id]['product_id']);
return [false, '(回滚)'.$bm_bn.'】'.$title.'失败:'.$msg];
}
}
/* // 不能做set处理因为调用redis的rollback的地方有的在db->rollback之前有的在之后
// 3. 数据库更新成功做SET
if ($isRedis && $code == 100) {
$args = [];
foreach ($fItems as $key => $item) {
$row = $db->selectrow("SELECT `store`,`store_freeze` FROM `" . DB_PREFIX . "ome_branch_product` WHERE `branch_id`={$item['branch_id']} AND `product_id`={$item['product_id']}");
$args[] = $row['store'].','.$row['store_freeze'];
}
$lua = file_get_contents(app::get('ome')->app_dir.'/lua/branch_store_set.lua');
$args = array_merge(array_keys($fItems), $args);
parent::$stockRedis->eval($lua, $args, count($fItems));
}
*/
return [true, $title.'成功'];
}
/**
* redis库存回滚
*/
static public function storeRollbackInRedis()
{
if (!self::_getRedisStoreRollbackList()) {
return [true, 'branchProduct redisStoreRollbackList is []'];
}
foreach (self::_getRedisStoreRollbackList() as $ro_value) {
$items = $ro_value['items'];
$opt = $ro_value['opt'];
self::_storeRollbackInRedis($items, $opt);
}
self::_initRedisStoreRollbackList();
return [true, 'redis仓冻结回滚成功'];
}
/**
* redis库存回滚
*
* @params array $items,
* 示例:[['branch_id'=>1,'product_id'=>1,'quantity'=>1,'bn'=>'test001'],['branch_id'=>1,'product_id'=>2,'quantity'=>1,'bn'=>'test002']]
* @params string $opt可选值: +、-
* @return void
*/
static private function _storeRollbackInRedis($items, $opt)
{
$node_id = base_shopnode::node_id('ome');
if (!in_array($opt, ['+', '-'])){
return [false, '(回滚)仓库存操作符错误:'.$opt];
}
if ($opt == '+') {
$ratio = 1;
$lua = file_get_contents(app::get('ome')->app_dir.'/lua/branch_store_incr.lua');
$title = '(回滚)增加仓库存';
} else {
$ratio = -1;
$lua = file_get_contents(app::get('ome')->app_dir.'/lua/branch_store_decr.lua');
$title = '(回滚)扣减仓库存';
}
// todo 需要加参数验证
$fItems = [];
foreach ($items as $item) {
if (!$item['branch_id']) {
return [false, '(回滚)参数错误-branch_id空'.json_encode($item)];
} elseif (!$item['product_id']) {
return [false, '(回滚)参数错误-bm_id空'.json_encode($item)];
} elseif (!$item['quantity']) {
$bm_bn = self::getbmBn($item['product_id']);
return [false, '(回滚)参数错误-quantity空[bm_bn=>'.$bm_bn.']'.json_encode($item)];
}
$key = sprintf('%s#stock:%s:%s', $node_id, $item['branch_id'], $item['product_id']);
// 扣减冻结为负数
$item['quantity'] = abs($item['quantity']) * $ratio;
if (isset($fItems[$key])) {
$fItems[$key]['quantity'] += $item['quantity'];
} else {
$fItems[$key] = $item;
}
}
$isRedis = parent::_connectRedis();
$code = null;
if (!$isRedis) {
return [true, '库存回滚连接redis失败'];
}
// 1. 更新Redis
if ($isRedis) {
$args = array_merge(array_keys($fItems), array_column($fItems, 'quantity'));
list($code, $msg, $sku_id) = $rr = parent::$stockRedis->eval($lua, $args, count($fItems));
if ($code === false) {
return [false, $title.'失败:脚本异常'];
}
if ($code > 100) {
$bm_bn = $fItems[$sku_id]['bn'] ? $fItems[$sku_id]['bn'] : self::getbmBn($fItems[$sku_id]['product_id']);
return [false, '(回滚)【'.$bm_bn.'】'.$title.'失败:'.$msg];
}
}
// 3. 数据库更新成功做SET
if ($isRedis && $code == 100) {
$db = kernel::database();
$args = [];
foreach ($fItems as $key => $item) {
$row = $db->selectrow("SELECT `store`,`store_freeze` FROM `" . DB_PREFIX . "ome_branch_product` WHERE `branch_id`={$item['branch_id']} AND `product_id`={$item['product_id']}");
$args[] = $row['store'].','.$row['store_freeze'];
}
$lua = file_get_contents(app::get('ome')->app_dir.'/lua/branch_store_set.lua');
$args = array_merge(array_keys($fItems), $args);
parent::$stockRedis->eval($lua, $args, count($fItems));
}
return [true, $title.'成功'];
}
/**
* 在redis中获取冻库存和结数
*
* @params array $branch_product_info,
* 示例:['branch_id' => 222, 'product_id' => 666]
*
* @return void
*/
static public function storeFromRedis($branch_product_info = [])
{
if (!$branch_product_info['branch_id'] || !$branch_product_info['product_id']) {
return [false, 'branch_id or product_id is null', []];
}
$isRedis = parent::_connectRedis();
$code = null;
// 1. 从Redis获取
if ($isRedis) {
$node_id = base_shopnode::node_id('ome');
$key = sprintf('%s#stock:%s:%s', $node_id, $branch_product_info['branch_id'], $branch_product_info['product_id']);
$redis_quantity = parent::$stockRedis->hgetall($key);
if ($redis_quantity) {
$return = [
'store' => $redis_quantity['store'],
'store_freeze' => $redis_quantity['store_freeze'],
];
return [true, 'succ', $return];
} else {
$isRedis = false;
}
}
// 2. 如果Redis不存在或者redis中数据不存在查mysql
if (!$isRedis){
$db = kernel::database();
$row = $db->selectrow("SELECT `store`,`store_freeze` FROM `" . DB_PREFIX . "ome_branch_product` WHERE `branch_id`={$branch_product_info['branch_id']} AND `product_id`={$branch_product_info['product_id']}");
if ($row) {
return [true, 'succ', $row];
} else {
return [false, 'get dbstore is fail', []];
}
}
return [false, 'storeFromRedis unknown error', []];
}
static private function getbmBn($bm_id='')
{
if (!$bm_id) {
return '未知商品';
}
$mdl = app::get('material')->model('basic_material');
$info = $mdl->db_dump(['bm_id'=>$bm_id], 'material_bn');
!$info && $info = ['material_bn'=>$bm_id];
return $info['material_bn'];
}
}