OwnerPriceOperationService.php 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358
  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. //未被标记过处理时间或处理时间不为本月,或上次处理值过期,处理历史即时账单
  129. $pivot = DB::selectOne(DB::raw("SELECT * FROM owner_price_operation_owner WHERE owner_price_operation_id = ? AND owner_id = ?"),[$rule->id,$owner_id]);
  130. if ($pivot && (!$pivot->discount_date || substr($pivot->discount_date,0,7)!=date("Y-m") || $pivot->target_value < $targetValue)){
  131. try{
  132. DB::beginTransaction();
  133. $month = date("Y-m");
  134. $day = date("t",strtotime($month));
  135. foreach (OwnerFeeDetail::query()->with(["order.logistic","order.shop","order.packages.commodities.commodity","order.batch"])
  136. ->where("owner_id",$owner_id)
  137. ->whereBetween("worked_at",[$month."-01",$month."-".$day])->get() as $detail){
  138. $order = $detail->order;
  139. $logistic_fee = 0;
  140. $commodities = [];
  141. foreach ($order->packages as &$package){
  142. // 四维转二维
  143. foreach($package->commodities as &$commodity){
  144. $commodity["commodity_name"] = $commodity->commodity ? $commodity->commodity->name : '';
  145. $commodity["sku"] = $commodity->commodity ? $commodity->commodity->sku : '';
  146. }
  147. $commodities = array_merge($commodities,$package->commodities->toArray());
  148. }
  149. if ($logistic_fee!==null && $logistic_fee<0)$logistic_fee = null;
  150. $object = ["commodities"=>$commodities,
  151. "logistic_name"=>($order->logistic ? $order->logistic->name : ''),
  152. "shop_name"=>($order->shop ? $order->shop->name : ''),
  153. "order_type"=>$order->order_type,
  154. "batch_type" => $order->batch ? $order->batch->wms_type : '',
  155. "owner_id"=>$order->owner_id];
  156. $mapping = ["packages"=>"commodities","商品名称"=>"commodity_name",
  157. "承运商"=>"logistic_name", "店铺类型"=>"shop_name",
  158. "订单类型"=>"order_type","波次类型"=>"batch_type"];
  159. $money = $this->matchItem($rule->items,$mapping,$object,$units,$owner_id,false,$isDiscount,$discountIndex);
  160. if ($money>0)$detail->update(["work_fee"=>$money]);
  161. else LogService::log(__CLASS__,"处理历史即时账单时发生匹配错误","账单主键:".$detail->id."; 错误代码".$money);
  162. };
  163. DB::update(DB::raw("UPDATE owner_price_operation_owner SET discount_date = ?,target_value = ? WHERE owner_price_operation_id = ? AND owner_id = ?"),
  164. [date("Y-m-d"),$targetValue,$rule->id,$owner_id]);
  165. DB::commit();
  166. }catch (\Exception $e){
  167. DB::rollBack();
  168. LogService::log(__CLASS__,"处理历史即时账单时发生系统错误","计费模型主键:".$rule->id."; 错误信息".$e->getMessage());
  169. }
  170. }
  171. }
  172. if ($rule->strategy == '特征'){//特征策略匹配
  173. $bool = app("FeatureService")->matchFeature($rule->feature,$columnMapping,$matchObject);
  174. if ($bool === true){
  175. if ($rule->total_price)return ["id"=>$rule->id,"money"=>$isDiscount ? explode(",",$rule->total_discount_price)[$discountIndex] : $rule->total_price]; //按单计价存在,直接返回单总价或减免总价
  176. $money = $this->matchItem($rule->items,$columnMapping,$matchObject,$units,$owner_id,$type=='入库' ? true : false,$isDiscount,$discountIndex);
  177. if ($money>0)return ["id"=>$rule->id,"money"=>$money];
  178. };
  179. }else{//默认策略匹配
  180. if ($rule->total_price)return ["id"=>$rule->id,"money"=>$isDiscount ? explode(",",$rule->total_discount_price)[$discountIndex] : $rule->total_price]; //按单计价存在,直接返回单总价或减免总价
  181. $money = $this->matchItem($rule->items,$columnMapping,$matchObject,$units,$owner_id,$type=='入库' ? true : false,$isDiscount,$discountIndex);
  182. if ($money>0)return ["id"=>$rule->id,"money"=>$money];
  183. };
  184. }
  185. return $money ?? -7;
  186. }
  187. /**
  188. * 根据货主 sku寻找箱规并将指定数量切换为箱
  189. * 不满一箱视为一箱
  190. *
  191. * @param integer $amount
  192. * @param integer $owner_id
  193. * @param string $sku
  194. *
  195. * @return int
  196. */
  197. private function changeUnit($amount,$owner_id,$sku)
  198. {
  199. if (!$sku)return -4;
  200. $pack = app("CommodityService")->getPack($owner_id,$sku);
  201. if (!$pack)return -6;
  202. return ceil($amount/$pack);
  203. }
  204. /**
  205. * 匹配子策略
  206. *
  207. * @param array $rules 策略对象组
  208. * @param array $columnMapping 映射对象
  209. * @param array $matchObject 被匹配对象
  210. * @param array $units 单位集
  211. * @param integer $owner_id 货主ID
  212. * @param bool $isIn 是否为入库单
  213. * @param bool $isDiscount 是否为满减单
  214. * @param int $discountIndex 阶梯满减所处下标 满减价格此处应为 1,2,3 解析为数组后根据此下标寻找对应值
  215. *
  216. * @return double
  217. */
  218. private function matchItem($rules, $columnMapping, $matchObject, $units, $owner_id, $isIn, $isDiscount, $discountIndex)
  219. {
  220. $amountColumn = $columnMapping["amount"] ?? "amount";
  221. $packageColumn = $columnMapping["packages"] ?? "packages";
  222. $packages = $matchObject[$packageColumn] ?? false;
  223. $commodityColumn = $columnMapping["商品名称"] ?? 'commodity';
  224. if (!$packages)return -1;
  225. $unitName = "";
  226. foreach ($rules as $rule){
  227. if ($isDiscount)$rule->unit_price = explode(",",$rule->discount_price)[$discountIndex]; //满足满减条件,单价调整为满减单价
  228. switch ($rule->strategy){
  229. case "特征":
  230. $inMoney = 0;
  231. foreach ($packages as &$package){
  232. if ($package["price"] ?? false)continue;
  233. if (!app("FeatureService")->matchFeature($rule->feature,["商品名称"=>$commodityColumn],$package)) continue;
  234. if (!$unitName)$unitName = $units[$rule->unit_id];
  235. else {
  236. if ($unitName != $units[$rule->unit_id]) return -3;
  237. }
  238. $package["price"] = $rule->unit_price;
  239. if (!isset($units[$rule->unit_id]))return -3;
  240. if ($units[$rule->unit_id] == '箱'){ //为箱时同步商品寻找箱规
  241. $sumTemp = 0;
  242. $packageColumn = $columnMapping["packages"] ?? "packages";
  243. foreach ($matchObject[$packageColumn] as $commodity){
  244. $sumTemp += $this->changeUnit($package[$amountColumn],$owner_id,$commodity["sku"]);
  245. }
  246. $amount = $sumTemp;
  247. if ($amount<0)return $amount;
  248. $package[$amountColumn] = $amount;
  249. }
  250. $inMoney += $package[$amountColumn] * $package["price"];
  251. }
  252. if ($isIn && $inMoney !== 0){
  253. return $inMoney;
  254. }
  255. break;
  256. case "默认":
  257. $inMoney = 0;
  258. foreach ($packages as &$package){
  259. if ($package["price"] ?? false)continue; //校验是否已匹配到
  260. if (!$unitName)$unitName = $units[$rule->unit_id]; //校验单位是否一致
  261. else {
  262. if ($unitName != $units[$rule->unit_id])
  263. return -3;
  264. }
  265. $package["price"] = $rule->unit_price;
  266. if (!isset($units[$rule->unit_id]))return -3;
  267. if ($units[$rule->unit_id] == '箱'){ //为箱时同步商品寻找箱规
  268. $sumTemp = 0;
  269. $packageColumn = $columnMapping["packages"] ?? "packages";
  270. foreach ($matchObject[$packageColumn] as $commodity){
  271. $sumTemp += $this->changeUnit($package[$amountColumn],$owner_id,$commodity["sku"]);
  272. }
  273. $amount = $sumTemp;
  274. if ($amount<0)return $amount;
  275. $package[$amountColumn] = $amount;
  276. }
  277. $inMoney += $package[$amountColumn] * $package["price"];
  278. }
  279. if ($isIn && $inMoney !== 0){
  280. return $inMoney;
  281. }
  282. break;
  283. default:
  284. if ($isIn)break; //入库不计算起步
  285. if ($rule->amount){ //起步数+起步费
  286. if ($unitName && $unitName != $units[$rule->unit_id])return -3; //校验单位是否一致
  287. $money = $rule->unit_price;
  288. $startNumber = $rule->amount;
  289. $packages = $this->settingCount($packages,$amountColumn,$startNumber);
  290. if ($packages){
  291. foreach ($packages as $package){
  292. $money += $package[$amountColumn] * $package["price"];
  293. }
  294. }
  295. }else{//单起步费
  296. $money = 0;
  297. if ($packages){
  298. foreach ($packages as $package){
  299. $money += $package[$amountColumn] * $package["price"];
  300. }
  301. }
  302. if ($money<$rule->unit_price)$money = $rule->unit_price;
  303. }
  304. return $money;
  305. }
  306. }
  307. return -7;
  308. }
  309. //递归式重新设置数量
  310. private function settingCount($packages,$amountColumn,$startNumber)
  311. {
  312. if (!$packages) return null;
  313. $maxPrice = 0;
  314. $index = null;
  315. foreach ($packages as $i => $package){
  316. if ($package[$amountColumn] <= 0){
  317. unset($packages[$i]);continue;
  318. }
  319. if ($package["price"] > $maxPrice || ($package["price"]==0 && $maxPrice==0)){
  320. $maxPrice = $package["price"];
  321. $index = $i;
  322. }
  323. }
  324. if ($packages[$index][$amountColumn] >= $startNumber){
  325. $packages[$index][$amountColumn] -= $startNumber;
  326. return $packages;
  327. }else{
  328. $startNumber -= $packages[$index][$amountColumn];
  329. unset($packages[$index]);
  330. $this->settingCount($packages,$amountColumn,$startNumber);
  331. }
  332. }
  333. }