ownerGetIds($params["owner_id"]); if ($ids)$builder->whereIn("id",$ids); unset($params["owner_id"]); } $columnQueryRules = [ "name" => ["like"=>""] ]; return app(QueryService::class)->query($params, $builder, $columnQueryRules); } public function paginate(array $params, array $withs = []) { return $this->query(OwnerPriceOperation::query()->orderByDesc('id')->with($withs),$params) ->paginate($params["paginate"] ?? 50); } private function ownerGetIds(string $ownerId) { if (!$ownerId)return []; $arr = DB::select(DB::raw("SELECT owner_price_operation_id AS id FROM owner_price_operation_owner WHERE owner_id in (".$ownerId.")")); return array_column($arr,"id"); } /** * 拷贝目标数据 * * @param object|int $model * @param array $values * @param array $items * @param array|null $owners * @param bool $isLoadItem * * @return object|null */ public function copy($model, $values = [], $owners = null, $items = [], $isLoadItem = true) { if (is_integer($model))$model = OwnerPriceOperation::query()->find($model); if (!$model)return null; $values["operation"] = "U"; $values["target_id"] = $model->id; foreach ($model->getFillable() as $column){ if (!array_key_exists($column,$values))$values[$column] = $model[$column]; } if ($owners === null){ $query = DB::raw("SELECT * FROM owner_price_operation_owner WHERE owner_price_operation_id = {$model->id}"); $owners = array_column(DB::select($query),"owner_id"); } /** @var OwnerPriceOperation $copyModel */ $copyModel = OwnerPriceOperation::query()->create($values); $copyModel->owners()->sync($owners); $insert = []; if ($isLoadItem){ $model->load("items"); /** @var \stdClass $model */ foreach ($model->items as $item){ $columns = ["strategy","amount","unit_id","unit_price","feature","priority","discount_price","odd_price"]; if ($items[$item->id] ?? false){ foreach ($columns as $column){ if (!array_key_exists($column,$items[$item->id]))$items[$item->id][$column] = $item[$column]; } $obj = $items[$item->id]; unset($items[$item->id]); }else{ /** @var OwnerPriceOperationItem $item */ $obj = $item->toArray(); unset($obj["id"]); } $obj["owner_price_operation_id"] = $copyModel->id; $insert[] = $obj; } }else{ foreach ($items as $item){ $arr = []; $arr["owner_price_operation_id"] = $copyModel->id; $arr["strategy"] = $item["strategy"]; $arr["amount"] = $item["amount"] ?? null; $arr["unit_id"] = $item["unit_id"]; $arr["unit_price"] = $item["unit_price"]; $arr["feature"] = $item["feature"] ?? null; $arr["odd_price"] = $item["odd_price"] ?? null; $arr["priority"] = $item["priority"] ?? 0; $arr["discount_price"] = isset($item["discount_price"]) ? (is_array($item["discount_price"]) ? implode(",",$item["discount_price"]) : $item["discount_price"]) : null; $insert[] = $arr; } } if ($insert)OwnerPriceOperationItem::query()->insert($insert); return $copyModel; } /** * 审核或恢复目标集 * * @param bool $isAudit * @param integer|null|array $ownerId * @param integer|null|array $ids */ public function auditOrRecover($isAudit = true, $ownerId = null, $ids = null) { if (!$ownerId && !$ids)return; $result = app(QueryService::class)->priceModelAuditOrRecoverQuery($isAudit,OwnerPriceOperation::query(),$ownerId,$ids); if ($result["delete"])$this->destroy($result["delete"]); if ($result["update"])OwnerPriceOperation::query()->whereIn("id",$result["update"])->update(["operation"=>null,"target_id"=>null]); if (!is_array($ownerId))$ownerId = [$ownerId]; foreach ($ownerId as $ow)Cache::tags("operationFeeOwner:".$ow)->flush(); } public function destroy($id) { if (!is_array($id))$id = [$id]; OwnerPriceOperationItem::query()->whereIn("owner_price_operation_id",$id)->delete(); $query = "IN ("; for ($i=0;$iwhereIn("owner_price_operation_id",$id)->delete(); app("OwnerService")->refreshRelevance($owners,1,true); return OwnerPriceOperation::destroy($id); } /** * @param array $params * @return Model */ public function create(array $params) { $params["operation"] = "C"; return OwnerPriceOperation::query()->create($params); } public function insertItem(array $params) { OwnerPriceOperationItem::query()->insert($params); } public function find($id, $withs = []) { $query = OwnerPriceOperation::query()->with($withs)->find($id); return $query; } public function update(array $params, array $values) { $query = OwnerPriceOperation::query(); foreach ($params as $column => $value){ $query->where($column,$value); } return $query->update($values); } public function destroyItem($id) { return OwnerPriceOperationItem::query()->where("owner_price_operation_id",$id)->delete(); } public function findUpdate(OwnerPriceOperation $model, array $params) { return $model->update($params); } /** * 获取计费模型缓存 * * @param integer $owner * @param string $type * @param int|null $typeMark * * @return array|Collection * */ public function getOwnerPriceOperation($owner, $type, $typeMark) { return Cache::tags("operationFeeOwner:".$owner)->remember("operationFee:".$owner.$type.$typeMark,config("cache.expirations.rarelyChange"), function ()use($owner,$type,$typeMark){ $query = OwnerPriceOperation::query()->with(["items"=>function($query){ /** @var Builder $query */ $query->orderByRaw("CASE strategy WHEN '起步' THEN 1 WHEN '默认' THEN 2 WHEN '特征' THEN 3 END DESC,priority"); }])->where("operation_type",$type)->whereHas("owners",function ($query)use($owner){ /** @var Builder $query */ $query->where("id",$owner); })->where(function(Builder $query){ $query->whereNull("operation")->orWhere("operation",""); })->orderByRaw("strategy desc,priority desc"); if ($typeMark!==null)$query->where("type_mark",$typeMark); else $query->whereNull("type_mark"); return $query->get(); }); } /** * 获取满减信息 * * @param null|string $discount * @param integer $total * * @return bool|array */ public function getDiscount($discount, $total) { if ($discount){ foreach (array_reverse(explode(",",$discount),true) as $index=>$discount){ if ($total >= $discount)return [$index=>$discount]; } } return false; } /** * 处理折扣单 * * @param object $rule * @param integer $owner * @param bool|array $result */ public function handleDiscount($rule, $owner, $result) { $sign = false; //入口仅在此处存在 缓存1000s $key = "owner_price_operation_owner_".$rule->id."_".$owner; $discountIndex = key($result); $targetValue = $result[$discountIndex]; $pivot = null; try{ DB::beginTransaction(); //此处暂时未使用cache的互斥锁 使用sql行锁代替下 防止缓存击穿 $pivot = app(CacheService::class)->getOrExecute($key,function ()use($key,$targetValue,&$sign,$rule,$owner){ return DB::selectOne(DB::raw("SELECT * FROM owner_price_operation_owner WHERE owner_price_operation_id = ? AND owner_id = ? for update"),[$rule->id,$owner]); },1000); if ($pivot && (!$pivot->discount_date || substr($pivot->discount_date,0,7)!=date("Y-m") || $pivot->target_value < $targetValue)){ //未被标记过处理时间或处理时间不为本月,或上次处理值过期,处理历史即时账单 $sign = true; } if ($sign){ //先标记成功 这将在后续推进历史单处理流程,防止程序在此堵塞 DB::update(DB::raw("UPDATE owner_price_operation_owner SET discount_date = ?,target_value = ? WHERE owner_price_operation_id = ? AND owner_id = ?"), [date("Y-m-d"),$targetValue,$rule->id,$owner]); $pivot->discount_date = date("Y-m-d"); $pivot->target_value = $targetValue; Cache::put($key,$pivot,1000); } DB::commit(); }catch (\Exception $exception){ DB::rollBack(); LogService::log(__CLASS__,"即时账单满减处理失败",$exception->getMessage()); } //进入历史单处理 if ($pivot && $sign)dispatch(new HandlePastBill(array($rule,$owner,$discountIndex,$pivot))); } /** 参数顺序: 数量 匹配对象 列映射 货主ID 单位ID 类型 SKU . * 匹配顺序: 类型 货主 策略 单位 特征 ..多对多匹配规则废弃,1对1,设单位必定为件,对应规则必然只有一项存在 * 单位匹配: 件,箱 由小到大,依次换算匹配 . * * 2:没有总数量存在,都为子项内数量 * * @param array|object|Model $matchObject key-val * @param array $columnMapping key-val * @param string $ownerId * @param string $type * @param int|null $typeMark * * @return double|array * 错误代码: -1:无匹配对象 -2:无计费模型 -3:未知单位 -4:sku为空 -5:货主未找到 -6:无箱规 -7:未匹配到计费模型 * * 一. 2020-10-10 zzd * 二. 2021-01-08 zzd * 三. 2021-01-28 zzd * 增加满减策略:子策略匹配时不再考虑单,仅件箱换算,满减满足后标记模型修改历史对账单 * 增加按订单计价策略:主匹配模型增加字段量价,该字段存在时视为按单计价,价格为该值 * 四. 2021-02-18 zzd * 满减多阶段匹配 满减字段由单值改为字符串多值 匹配时转数组寻找最相近 * 五. 2021-03-18 zzd * 区分单据类型,增加字段 * 六. 2021-03-23 zzd * 不严格区分入库出库差异 统一模型 * 七. 2021-03-30 zzd * 增加一级二级特征,零头价,满减按总件,附加费用等 * 八. 2021-04-19 zzd * 增加税率计算,改变返回值数据结构,增加封顶费 * 九. 2021-04-21 zzd * 排除掉order不存在包裹情况,预设输出值为null防止返回0,历史账单处理推进队列防止超时 */ public function matching($matchObject, $columnMapping, $ownerId, $type = '出库', $typeMark = null) { $units = app("UnitService")->getUnitMapping(["件","箱"]); //获取单位映射集 $rules = $this->getOwnerPriceOperation($ownerId,$type,$typeMark);//货主下的全部规则 if (!$rules)return -2; //规则不存在跳出 //建立一组返回变量 $id = null; $money = null; $taxFee = null; if ($type == '出库'){ $total = app("OrderService")->getOrderQuantity($ownerId)+1;//获取该货主本月C端单量 $matchObject->packages->each(function ($package)use(&$orderTotal){ $package->commodities->each(function ($commodity)use(&$orderTotal){ $orderTotal += (int)$commodity->amount; }); }); $matchObject[$columnMapping[12]] = $orderTotal; }else { $total = 0; if ($matchObject->storeItems)foreach ($matchObject->storeItems as $item)$total += $item->amount; $matchObject[$columnMapping[12]] = $total; $total += app("StoreService")->getStoreAmount($ownerId);//获取该货主本月入库件数 } foreach ($rules as $rule){ if (!$rule->items)continue; //不存在子规则跳出 $result = $this->getDiscount($rule->discount_count,$total); //满减信息 if ($result)$this->handleDiscount($rule,$ownerId,$result);//满减存在 if ($rule->strategy == '特征'){ if (app("FeatureService")->matchFeature($rule->feature,$columnMapping,$matchObject)){ if (!$rule->total_price)$money = $this->matchItem($rule,$columnMapping,$matchObject,$units,$ownerId,$result); $id = $rule->id; }; }else{ if (!$rule->total_price)$money = $this->matchItem($rule,$columnMapping,$matchObject,$units,$ownerId,$result); $id = $rule->id; }; if ($id){ if ($rule->total_price)$money = $result ? explode(",",$rule->total_discount_price)[key($result)] : $rule->total_price;//按单计价存在,直接返回单总价或减免总价 $money = $rule->max_fee&&$money>$rule->max_fee ? $rule->max_fee : $money;//封顶费 if ($money<=0)$money=null;//计算失误 if ($rule->tax_rate_id && $rule->taxRate)$taxFee = $money*($rule->taxRate->value/100); else{ /** @var Owner|\stdClass $owner */ $owner = Owner::query()->with("taxRate") ->whereHas("taxRate")->find($ownerId); if ($owner)$taxFee = $money*($owner->taxRate->value/100); } break; } } return array($id,$money,$taxFee); } /** * 根据货主 sku寻找箱规并将指定数量切换为箱 返回箱规 * * @param integer $ownerId * @param null|object $commodity * * @return int */ private function changeUnit($ownerId,$commodity) { if (!$commodity)return -4; if ($commodity["pack_spec"])return $commodity["pack_spec"]; $pack = app("CommodityService")->getPack($ownerId,$commodity["sku"]); if (!$pack)return -6; return $pack; } /** * 重置子节点的映射对象 将无限极数组扁平化为二维 以Feature中定义的8:商品数量 key为基准 * * @param object|array $matchObject * @param array $columnMapping * * @return array */ private function resetChildNodeMapping($matchObject,&$columnMapping) { $need = ""; foreach (Feature::TYPE_NODE as $index){ if (!$need)$need=strstr($columnMapping[$index],".",true); $columnMapping[$index] = ltrim(strstr($columnMapping[$index],"."),"."); } $nextObj = strstr($columnMapping[8],".",true); $first = $matchObject[$need] ?? false; if ($first===false)return $matchObject; if (!$first)return $first; if (is_array($first))$first = reset($first); if ($nextObj && is_array($first[$nextObj])){ $result = []; foreach ($matchObject[$need] as $arr)$result = array_merge($result,$arr[$nextObj]); return $this->resetChildNodeMapping($result,$columnMapping); } return $matchObject[$need]; } /** * 匹配子策略 * * @param Model|\stdClass $obj 策略对象 * @param array $columnMapping 映射对象 * @param Model $matchObject 被匹配对象 * @param array $units 单位集 * @param integer $ownerId 货主ID * @param bool|array $result 满减信息 * * @return double */ private function matchItem($obj, $columnMapping, $matchObject, $units, $ownerId, $result) { $matchObject = $this->resetChildNodeMapping($matchObject->toArray(),$columnMapping); if (!$matchObject)return -1; $total = 0; //商品总数 foreach ($matchObject as $commodity)$total += $commodity[$columnMapping[8]]; //取对象内商品数量总数将其当作子属性插入原对象 $surcharge = 0; $unitName = ""; if ($obj->surcharge_unit_id && $obj->surcharge && isset($units[$rule->surcharge_unit_id])){ if ($units[$obj->surcharge_unit_id] == '件')$surcharge += $obj->surcharge*$total; else $surcharge += $obj->surcharge; }//耗材附加费 foreach ($obj->items as $rule){ if ($result)$rule->unit_price = explode(",",$rule->discount_price)[key($result)]; //满足满减条件,单价调整为满减单价 if ($rule->strategy=='起步'){ $startNumber = $rule->amount; $money = 0; if ($unitName && $startNumber && $unitName != $units[$rule->unit_id])return -3; //校验单位是否一致 if ($startNumber){ $money = $rule->unit_price; $matchObject=$this->settingCount($matchObject,$columnMapping[8],$startNumber); } if ($matchObject)foreach ($matchObject as $package)if ($package["price"] ?? false)$money += $package[$columnMapping[8]] * $package["price"]; if (!$startNumber && $money<$rule->unit_price)$money = $rule->unit_price; return $money+$surcharge; } foreach ($matchObject as &$package){ if ($package["price"] ?? false)continue; if (!isset($units[$rule->unit_id]))return -3; if (!$unitName)$unitName = $units[$rule->unit_id]; else if ($unitName != $units[$rule->unit_id]) return -3; if ($rule->strategy=='特征'){ $package[$columnMapping[10]] = $total; //设置一个不存在的总数进入原对象 if (!app("FeatureService")->matchFeature($rule->feature,$columnMapping,$package)) continue; } $package["price"] = $rule->unit_price; } if ($units[$rule->unit_id] == '箱'){ //为箱时同步商品寻找箱规 $amount = 0; foreach ($matchObject as $commodity){ $pack = $this->changeUnit($ownerId,$commodity[$columnMapping[9]]); if ($rule->odd_price){ $amount += floor($amount/$pack); $surcharge += $rule->odd_price * ($amount%$pack); //零头附加费 }else$amount += ceil($amount/$pack); } if ($amount<0)return $amount; $package[$columnMapping[8]] = $amount; } } if ($matchObject){ $money = $surcharge; foreach ($matchObject as $package)if ($package["price"] ?? false)$money += $package[$columnMapping[8]] * $package["price"]; } return $money ?? -7; } //递归式重新设置数量 private function settingCount($packages,$amountColumn,$startNumber) { if (!$packages) return null; $maxPrice = 0; $index = null; foreach ($packages as $i => $package){ if ($package[$amountColumn] <= 0){ unset($packages[$i]);continue; } if (!($package["price"] ?? false)){ $package["price"] = 0; $packages[$i]["price"] = 0; } if ($package["price"] > $maxPrice || ($package["price"]==0 && $maxPrice==0)){ $maxPrice = $package["price"]; $index = $i; } } if ($packages[$index][$amountColumn] >= $startNumber){ $packages[$index][$amountColumn] -= $startNumber; return $packages; }else{ $startNumber -= $packages[$index][$amountColumn]; unset($packages[$index]); $this->settingCount($packages,$amountColumn,$startNumber); } } /** * 处理历史账单 * * @param object $rule * @param int $owner * @param int $discountIndex * @param object $pivot */ public function handlePastBill($rule, $owner, $discountIndex, $pivot) { try{ DB::beginTransaction(); $month = date("Y-m"); $day = date("t",strtotime($month)); $units = app("UnitService")->getUnitMapping(["件","箱"]); //获取单位映射集 foreach (OwnerFeeDetail::query()->with(["order.logistic","order.shop","order.packages.commodities.commodity","order.batch"]) ->where("owner_id",$owner) ->whereBetween("worked_at",[$month."-01",$month."-".$day])->get() as $detail){ $order = $detail->order; $logistic_fee = 0; if ($logistic_fee!==null && $logistic_fee<0)$logistic_fee = null; $money = $this->matchItem($rule,Feature::MAPPING["order"],$order,$units,$owner,[$discountIndex=>true]); if ($money>0)$detail->update(["work_fee"=>$money]); else LogService::log(__CLASS__,"处理历史即时账单时发生匹配错误","账单主键:".$detail->id."; 错误代码".$money); }; DB::commit(); }catch (\Exception $e){ DB::rollBack(); //处理失败回退标记 DB::update(DB::raw("UPDATE owner_price_operation_owner SET discount_date = ?,target_value = ? WHERE owner_price_operation_id = ? AND owner_id = ?"), [$pivot->discount_date,$pivot->target_value,$rule->id,$owner]); LogService::log(__CLASS__,"处理历史即时账单时发生系统错误","计费模型主键:".$rule->id."; 错误信息".$e->getMessage()); } } }