OwnerPriceOperationService.php 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397
  1. <?php
  2. namespace App\Services;
  3. use App\OwnerFeeDetail;
  4. use App\OwnerPriceOperation;
  5. use App\OwnerPriceOperationItem;
  6. use App\Services\common\QueryService;
  7. use App\Unit;
  8. use Illuminate\Database\Eloquent\Builder;
  9. use Illuminate\Database\Eloquent\Model;
  10. use Illuminate\Support\Facades\Cache;
  11. use Illuminate\Support\Facades\DB;
  12. use App\Traits\ServiceAppAop;
  13. class OwnerPriceOperationService
  14. {
  15. use ServiceAppAop;
  16. protected $modelClass=OwnerPriceOperation::class;
  17. /**
  18. * @param Builder $builder
  19. * @param array $params
  20. * @return Builder
  21. */
  22. private function query(Builder $builder, array $params)
  23. {
  24. if ($params["owner_id"] ?? false){
  25. $ids = $this->ownerGetIds($params["owner_id"]);
  26. if ($ids)$builder->whereIn("id",$ids);
  27. unset($params["owner_id"]);
  28. }
  29. $columnQueryRules = [
  30. "name" => ["like"=>""]
  31. ];
  32. return app(QueryService::class)->query($params, $builder, $columnQueryRules);
  33. }
  34. public function paginate(array $params, array $withs = [])
  35. {
  36. return $this->query(OwnerPriceOperation::query()->orderByDesc('id')->with($withs),$params)
  37. ->paginate($params["paginate"] ?? 50);
  38. }
  39. private function ownerGetIds(string $owner_id)
  40. {
  41. if (!$owner_id)return [];
  42. $arr = DB::select(DB::raw("SELECT owner_price_operation_id AS id FROM owner_price_operation_owner WHERE owner_id in (".$owner_id.")"));
  43. return array_column($arr,"id");
  44. }
  45. public function destroy($id)
  46. {
  47. OwnerPriceOperationItem::query()->where("owner_price_operation_id",$id)->delete();
  48. DB::table("owner_price_operation_owner")->where("owner_price_operation_id",$id)->delete();
  49. return OwnerPriceOperation::destroy($id);
  50. }
  51. /**
  52. * @param array $params
  53. * @return Model
  54. */
  55. public function create(array $params)
  56. {
  57. return OwnerPriceOperation::query()->create($params);
  58. }
  59. public function insertItem(array $params)
  60. {
  61. OwnerPriceOperationItem::query()->insert($params);
  62. }
  63. public function find($id, $withs = [])
  64. {
  65. $query = OwnerPriceOperation::query()->with($withs)->find($id);
  66. return $query;
  67. }
  68. public function destroyItem($id)
  69. {
  70. return OwnerPriceOperationItem::query()->where("owner_price_operation_id",$id)->delete();
  71. }
  72. public function findUpdate(OwnerPriceOperation $model, array $params)
  73. {
  74. return $model->update($params);
  75. }
  76. /** 参数顺序: 数量 匹配对象 列映射 货主ID 单位ID 类型 SKU .
  77. * 匹配顺序: 类型 货主 策略 单位 特征 ..多对多匹配规则废弃,1对1,设单位必定为件,对应规则必然只有一项存在
  78. * 单位匹配: 件,箱 由小到大,依次换算匹配 .
  79. *
  80. * 2:没有总数量存在,都为子项内数量
  81. *
  82. * @param array|object $matchObject key-val
  83. * @param array $columnMapping key-val
  84. * @param string $owner_id
  85. * @param string $type
  86. * @return double|array
  87. * 错误代码: -1:无匹配对象 -2:无计费模型 -3:未知单位 -4:sku为空 -5:货主未找到 -6:无箱规 -7:未匹配到计费模型
  88. *
  89. * 一. 2020-10-10 zzd
  90. * 二. 2021-01-08 zzd
  91. * 三. 2021-01-28 zzd
  92. * 增加满减策略:子策略匹配时不再考虑单,仅件箱换算,满减满足后标记模型修改历史对账单
  93. * 增加按订单计价策略:主匹配模型增加字段量价,该字段存在时视为按单计价,价格为该值
  94. * 四. 2021-02-18 zzd
  95. * 满减多阶段匹配 满减字段由单值改为字符串多值 匹配时转数组寻找最相近
  96. */
  97. public function matching($matchObject, $columnMapping, $owner_id, $type = '出库')
  98. {
  99. $unitModels = Unit::query()->whereIn("name",["件","箱"])->get();
  100. $units = [];
  101. foreach ($unitModels as $unitModel)$units[$unitModel->id] = $unitModel->name;
  102. $rules = OwnerPriceOperation::query()->with(["items"=>function($query){
  103. /** @var Builder $query */
  104. $query->orderByRaw("CASE strategy WHEN '起步' THEN 1 WHEN '默认' THEN 2 WHEN '特征' THEN 3 END DESC,priority");
  105. }])->where("operation_type",$type)->whereHas("ownerPriceOperationOwners",function ($query)use($owner_id){
  106. /** @var Builder $query */
  107. $query->where("id",$owner_id);
  108. })->orderByRaw("strategy desc,priority desc")->get(); //货主下的全部规则
  109. if (!$rules)return -2; //规则不存在跳出
  110. $total = Cache::get(date("Y-m")."|".$matchObject["owner_id"]); //获取该货主本月单量
  111. foreach ($rules as $rule){
  112. if (!$rule->items)continue; //不存在子规则跳出
  113. $isDiscount = false; //是否存在满减
  114. $discountIndex = 0;
  115. $targetValue = 0;
  116. if ($type=='出库' && $rule->discount_count){
  117. foreach (array_reverse(explode(",",$rule->discount_count),true) as $index=>$discount){
  118. if ($total >= $discount){
  119. $isDiscount = true; //第一个满足满减条件
  120. $discountIndex = $index;
  121. $targetValue = $discount;
  122. break;
  123. }
  124. }
  125. }
  126. //满减存在
  127. if ($isDiscount){
  128. $sign = false;
  129. //入口仅在此处存在 缓存1000s
  130. $key = "pivot_".$rule->id."_".$owner_id;
  131. $pivot = app(CacheService::class)->getOrExecute($key,function ()use($key,$targetValue,&$sign,$rule,$owner_id){
  132. try{
  133. DB::beginTransaction();
  134. //此处暂时未使用cache的互斥锁 使用sql行锁代替下 防止缓存击穿
  135. $pivot = DB::selectOne(DB::raw("SELECT * FROM owner_price_operation_owner WHERE owner_price_operation_id = ? AND owner_id = ? for update"),[$rule->id,$owner_id]);
  136. if ($pivot && (!$pivot->discount_date || substr($pivot->discount_date,0,7)!=date("Y-m") || $pivot->target_value < $targetValue)){
  137. //未被标记过处理时间或处理时间不为本月,或上次处理值过期,处理历史即时账单
  138. $sign = true;
  139. }
  140. if ($sign){
  141. //先标记成功 这将在后续推进历史单处理流程,防止程序在此堵塞
  142. DB::update(DB::raw("UPDATE owner_price_operation_owner SET discount_date = ?,target_value = ? WHERE owner_price_operation_id = ? AND owner_id = ?"),
  143. [date("Y-m-d"),$targetValue,$rule->id,$owner_id]);
  144. $pivot->discount_date = date("Y-m-d");
  145. $pivot->target_value = $targetValue;
  146. Cache::put($key,$pivot,1000);
  147. }
  148. DB::commit();
  149. }catch (\Exception $exception){
  150. DB::rollBack();
  151. LogService::log(__CLASS__,"即时账单满减处理失败",$exception->getMessage());
  152. }
  153. return $pivot ?? null;
  154. },1000);
  155. //进入历史单处理
  156. if ($pivot && $sign)$this->handlePastBill($rule,$owner_id,$units,$discountIndex,$pivot);
  157. }
  158. if ($rule->strategy == '特征'){//特征策略匹配
  159. $bool = app("FeatureService")->matchFeature($rule->feature,$columnMapping,$matchObject);
  160. if ($bool === true){
  161. if ($rule->total_price)return ["id"=>$rule->id,"money"=>$isDiscount ? explode(",",$rule->total_discount_price)[$discountIndex] : $rule->total_price]; //按单计价存在,直接返回单总价或减免总价
  162. $money = $this->matchItem($rule->items,$columnMapping,$matchObject,$units,$owner_id,$type=='入库' ? true : false,$isDiscount,$discountIndex);
  163. if ($money>0)return ["id"=>$rule->id,"money"=>$money];
  164. };
  165. }else{//默认策略匹配
  166. if ($rule->total_price)return ["id"=>$rule->id,"money"=>$isDiscount ? explode(",",$rule->total_discount_price)[$discountIndex] : $rule->total_price]; //按单计价存在,直接返回单总价或减免总价
  167. $money = $this->matchItem($rule->items,$columnMapping,$matchObject,$units,$owner_id,$type=='入库' ? true : false,$isDiscount,$discountIndex);
  168. if ($money>0)return ["id"=>$rule->id,"money"=>$money];
  169. };
  170. }
  171. return $money ?? -7;
  172. }
  173. /**
  174. * 根据货主 sku寻找箱规并将指定数量切换为箱
  175. * 不满一箱视为一箱
  176. *
  177. * @param integer $amount
  178. * @param integer $owner_id
  179. * @param string $sku
  180. *
  181. * @return int
  182. */
  183. private function changeUnit($amount,$owner_id,$sku)
  184. {
  185. if (!$sku)return -4;
  186. $pack = app("CommodityService")->getPack($owner_id,$sku);
  187. if (!$pack)return -6;
  188. return ceil($amount/$pack);
  189. }
  190. /**
  191. * 匹配子策略
  192. *
  193. * @param array $rules 策略对象组
  194. * @param array $columnMapping 映射对象
  195. * @param array $matchObject 被匹配对象
  196. * @param array $units 单位集
  197. * @param integer $owner_id 货主ID
  198. * @param bool $isIn 是否为入库单
  199. * @param bool $isDiscount 是否为满减单
  200. * @param int $discountIndex 阶梯满减所处下标 满减价格此处应为 1,2,3 解析为数组后根据此下标寻找对应值
  201. *
  202. * @return double
  203. */
  204. private function matchItem($rules, $columnMapping, $matchObject, $units, $owner_id, $isIn, $isDiscount, $discountIndex)
  205. {
  206. $amountColumn = $columnMapping["amount"] ?? "amount";
  207. $packageColumn = $columnMapping["packages"] ?? "packages";
  208. $packages = $matchObject[$packageColumn] ?? false;
  209. $commodityColumn = $columnMapping["商品名称"] ?? 'commodity';
  210. if (!$packages)return -1;
  211. $unitName = "";
  212. foreach ($rules as $rule){
  213. if ($isDiscount)$rule->unit_price = explode(",",$rule->discount_price)[$discountIndex]; //满足满减条件,单价调整为满减单价
  214. switch ($rule->strategy){
  215. case "特征":
  216. $inMoney = 0;
  217. foreach ($packages as &$package){
  218. if ($package["price"] ?? false)continue;
  219. if (!app("FeatureService")->matchFeature($rule->feature,["商品名称"=>$commodityColumn],$package)) continue;
  220. if (!$unitName)$unitName = $units[$rule->unit_id];
  221. else {
  222. if ($unitName != $units[$rule->unit_id]) return -3;
  223. }
  224. $package["price"] = $rule->unit_price;
  225. if (!isset($units[$rule->unit_id]))return -3;
  226. if ($units[$rule->unit_id] == '箱'){ //为箱时同步商品寻找箱规
  227. $sumTemp = 0;
  228. $packageColumn = $columnMapping["packages"] ?? "packages";
  229. foreach ($matchObject[$packageColumn] as $commodity){
  230. $sumTemp += $this->changeUnit($package[$amountColumn],$owner_id,$commodity["sku"]);
  231. }
  232. $amount = $sumTemp;
  233. if ($amount<0)return $amount;
  234. $package[$amountColumn] = $amount;
  235. }
  236. $inMoney += $package[$amountColumn] * $package["price"];
  237. }
  238. if ($isIn && $inMoney !== 0){
  239. return $inMoney;
  240. }
  241. break;
  242. case "默认":
  243. $inMoney = 0;
  244. foreach ($packages as &$package){
  245. if ($package["price"] ?? false)continue; //校验是否已匹配到
  246. if (!$unitName)$unitName = $units[$rule->unit_id]; //校验单位是否一致
  247. else {
  248. if ($unitName != $units[$rule->unit_id])
  249. return -3;
  250. }
  251. $package["price"] = $rule->unit_price;
  252. if (!isset($units[$rule->unit_id]))return -3;
  253. if ($units[$rule->unit_id] == '箱'){ //为箱时同步商品寻找箱规
  254. $sumTemp = 0;
  255. $packageColumn = $columnMapping["packages"] ?? "packages";
  256. foreach ($matchObject[$packageColumn] as $commodity){
  257. $sumTemp += $this->changeUnit($package[$amountColumn],$owner_id,$commodity["sku"]);
  258. }
  259. $amount = $sumTemp;
  260. if ($amount<0)return $amount;
  261. $package[$amountColumn] = $amount;
  262. }
  263. $inMoney += $package[$amountColumn] * $package["price"];
  264. }
  265. if ($isIn && $inMoney !== 0){
  266. return $inMoney;
  267. }
  268. break;
  269. default:
  270. if ($isIn)break; //入库不计算起步
  271. if ($rule->amount){ //起步数+起步费
  272. if ($unitName && $unitName != $units[$rule->unit_id])return -3; //校验单位是否一致
  273. $money = $rule->unit_price;
  274. $startNumber = $rule->amount;
  275. $packages = $this->settingCount($packages,$amountColumn,$startNumber);
  276. if ($packages){
  277. foreach ($packages as $package){
  278. $money += $package[$amountColumn] * $package["price"];
  279. }
  280. }
  281. }else{//单起步费
  282. $money = 0;
  283. if ($packages){
  284. foreach ($packages as $package){
  285. $money += $package[$amountColumn] * $package["price"];
  286. }
  287. }
  288. if ($money<$rule->unit_price)$money = $rule->unit_price;
  289. }
  290. return $money;
  291. }
  292. }
  293. return -7;
  294. }
  295. //递归式重新设置数量
  296. private function settingCount($packages,$amountColumn,$startNumber)
  297. {
  298. if (!$packages) return null;
  299. $maxPrice = 0;
  300. $index = null;
  301. foreach ($packages as $i => $package){
  302. if ($package[$amountColumn] <= 0){
  303. unset($packages[$i]);continue;
  304. }
  305. if ($package["price"] > $maxPrice || ($package["price"]==0 && $maxPrice==0)){
  306. $maxPrice = $package["price"];
  307. $index = $i;
  308. }
  309. }
  310. if ($packages[$index][$amountColumn] >= $startNumber){
  311. $packages[$index][$amountColumn] -= $startNumber;
  312. return $packages;
  313. }else{
  314. $startNumber -= $packages[$index][$amountColumn];
  315. unset($packages[$index]);
  316. $this->settingCount($packages,$amountColumn,$startNumber);
  317. }
  318. }
  319. /**
  320. * 处理历史账单
  321. *
  322. * @param object $rule
  323. * @param int $owner_id
  324. * @param array $units
  325. * @param int $discountIndex
  326. * @param object $pivot
  327. */
  328. public function handlePastBill($rule, $owner_id, $units, $discountIndex, $pivot)
  329. {
  330. try{
  331. DB::beginTransaction();
  332. $month = date("Y-m");
  333. $day = date("t",strtotime($month));
  334. foreach (OwnerFeeDetail::query()->with(["order.logistic","order.shop","order.packages.commodities.commodity","order.batch"])
  335. ->where("owner_id",$owner_id)
  336. ->whereBetween("worked_at",[$month."-01",$month."-".$day])->get() as $detail){
  337. $order = $detail->order;
  338. $logistic_fee = 0;
  339. $commodities = [];
  340. foreach ($order->packages as &$package){
  341. // 四维转二维
  342. foreach($package->commodities as &$commodity){
  343. $commodity["commodity_name"] = $commodity->commodity ? $commodity->commodity->name : '';
  344. $commodity["sku"] = $commodity->commodity ? $commodity->commodity->sku : '';
  345. }
  346. $commodities = array_merge($commodities,$package->commodities->toArray());
  347. }
  348. if ($logistic_fee!==null && $logistic_fee<0)$logistic_fee = null;
  349. $object = ["commodities"=>$commodities,
  350. "logistic_name"=>($order->logistic ? $order->logistic->name : ''),
  351. "shop_name"=>($order->shop ? $order->shop->name : ''),
  352. "order_type"=>$order->order_type,
  353. "batch_type" => $order->batch ? $order->batch->wms_type : '',
  354. "owner_id"=>$order->owner_id];
  355. $mapping = ["packages"=>"commodities","商品名称"=>"commodity_name",
  356. "承运商"=>"logistic_name", "店铺类型"=>"shop_name",
  357. "订单类型"=>"order_type","波次类型"=>"batch_type"];
  358. $money = $this->matchItem($rule->items,$mapping,$object,$units,$owner_id,false,true,$discountIndex);
  359. if ($money>0)$detail->update(["work_fee"=>$money]);
  360. else LogService::log(__CLASS__,"处理历史即时账单时发生匹配错误","账单主键:".$detail->id."; 错误代码".$money);
  361. };
  362. DB::commit();
  363. }catch (\Exception $e){
  364. DB::rollBack();
  365. //处理失败回退标记
  366. DB::update(DB::raw("UPDATE owner_price_operation_owner SET discount_date = ?,target_value = ? WHERE owner_price_operation_id = ? AND owner_id = ?"),
  367. [$pivot->discount_date,$pivot->target_value,$rule->id,$owner_id]);
  368. LogService::log(__CLASS__,"处理历史即时账单时发生系统错误","计费模型主键:".$rule->id."; 错误信息".$e->getMessage());
  369. }
  370. }
  371. }