From 08b6d7cda941a11bc8a72faf1793ae2be16b3fc6 Mon Sep 17 00:00:00 2001 From: ctexthuang Date: Tue, 11 Mar 2025 17:52:13 +0800 Subject: [PATCH] feat : catering --- app/Cache/Redis/Admin/AdminRedisKey.php | 45 ++- app/Constants/Admin/CateringCode.php | 23 ++ app/Constants/ConfigCode.php | 4 + app/Controller/Admin/CateringController.php | 3 +- .../Admin/Catering/CateringBaseService.php | 20 + .../Admin/Catering/Option/CateringService.php | 366 ++++++++++++++++++ .../Admin/Catering/PrintTrait.php | 158 ++++++++ app/Service/ServiceTrait/CateringTrait.php | 13 +- 8 files changed, 624 insertions(+), 8 deletions(-) create mode 100644 app/Constants/Admin/CateringCode.php create mode 100644 app/Service/Admin/Catering/Option/CateringService.php create mode 100644 app/Service/ServiceTrait/Admin/Catering/PrintTrait.php diff --git a/app/Cache/Redis/Admin/AdminRedisKey.php b/app/Cache/Redis/Admin/AdminRedisKey.php index 0a6b3ff..e092387 100644 --- a/app/Cache/Redis/Admin/AdminRedisKey.php +++ b/app/Cache/Redis/Admin/AdminRedisKey.php @@ -47,8 +47,51 @@ class AdminRedisKey * 部门集合 * @return string */ - public static function adminSectionList() + public static function adminSectionList(): string { return '__system:admin:section:list'; } + + // +-------------------------------------------------------------------------------------------------------------------------------------------- + // | catering class + // +-------------------------------------------------------------------------------------------------------------------------------------------- + /** + * 打印 (id) + * @param int $cycleId + * @return string + */ + public static function optionCateringIsPrint(int $cycleId): string + { + return 'catering:option:print:cycle_id:'.$cycleId; + } + + /** + * 截单 (site_id) + * @param int $cycleId + * @return string + */ + public static function optionCateringStopOrder(int $cycleId): string + { + return 'catering:option:stop_order:cycle_id:'.$cycleId; + } + + /** + * 生成取餐码 (id) + * @param int $cycleId + * @return string + */ + public static function optionCateringBuildPickupCode(int $cycleId): string + { + return 'catering:option:stop_order:cycle_id:'.$cycleId; + } + + /** + * 配餐 (id) + * @param int $cycleId + * @return string + */ + public static function optionIsCatering(int $cycleId): string + { + return 'catering:option:is:cycle_id:'.$cycleId; + } } \ No newline at end of file diff --git a/app/Constants/Admin/CateringCode.php b/app/Constants/Admin/CateringCode.php new file mode 100644 index 0000000..978ff6b --- /dev/null +++ b/app/Constants/Admin/CateringCode.php @@ -0,0 +1,23 @@ + '3', self::SUNDRY_PRICE_COMPUTE_TYPE => 1, self::NUMBER_OF_USER_ADDRESS_POOLS => 10, + self::MAXIMUM_VALUE_IN_FULL_BOX => 32, + self::MAXIMUM_WHOLE_CASE_SPLIT => 4, //CouponConfiguration self::COUPONS_FOR_NEWCOMERS => '0', self::NEWBIE_COUPON_VALIDITY => '0', diff --git a/app/Controller/Admin/CateringController.php b/app/Controller/Admin/CateringController.php index 237ca42..5c1be4e 100644 --- a/app/Controller/Admin/CateringController.php +++ b/app/Controller/Admin/CateringController.php @@ -7,6 +7,7 @@ namespace App\Controller\Admin; use App\Middleware\Admin\JwtAuthMiddleware; use App\Service\Admin\Catering\Meal\CycleListService as MealCycleListService; use App\Service\Admin\Catering\Option\CycleListService as OptionCycleListService; +use App\Service\Admin\Catering\Option\CateringService as OptionCateringService; use Hyperf\HttpServer\Annotation\Controller; use Hyperf\HttpServer\Annotation\Middlewares; use Hyperf\HttpServer\Annotation\RequestMapping; @@ -58,6 +59,6 @@ class CateringController public function optionCatering() { - + return (new OptionCateringService)->handle(); } } diff --git a/app/Service/Admin/Catering/CateringBaseService.php b/app/Service/Admin/Catering/CateringBaseService.php index d78a054..754e8f3 100644 --- a/app/Service/Admin/Catering/CateringBaseService.php +++ b/app/Service/Admin/Catering/CateringBaseService.php @@ -11,11 +11,13 @@ declare(strict_types=1); namespace App\Service\Admin\Catering; use App\Cache\Redis\Api\SiteCache; +use App\Cache\Redis\Common\ConfigCache; use App\Constants\Common\RoleCode; use App\Exception\ErrException; use App\Model\AdminUser; use App\Model\DriverSequence; use App\Model\Site; +use App\Model\Sku; use App\Service\Admin\BaseService; use App\Service\ServiceTrait\Admin\GetUserInfoTrait; use App\Service\ServiceTrait\Common\CycleTrait; @@ -50,12 +52,30 @@ abstract class CateringBaseService extends BaseService #[Inject] protected SiteCache $siteCache; + /** + * @var ConfigCache + */ + #[Inject] + protected ConfigCache $configCache; + + /** + * @var Sku + */ + #[Inject] + protected Sku $skuModel; + /** * @var DriverSequence */ #[Inject] protected DriverSequence $driverSequenceModel; + /** + * @var AdminUser + */ + #[Inject] + protected AdminUser $adminUserModel; + /** * @throws ContainerExceptionInterface * @throws NotFoundExceptionInterface diff --git a/app/Service/Admin/Catering/Option/CateringService.php b/app/Service/Admin/Catering/Option/CateringService.php new file mode 100644 index 0000000..24652a3 --- /dev/null +++ b/app/Service/Admin/Catering/Option/CateringService.php @@ -0,0 +1,366 @@ +printBoxMaxNum = (int)$this->configCache->getConfigValueByKey(ConfigCode::MAXIMUM_VALUE_IN_FULL_BOX); + $this->partitionCount = (int)$this->configCache->getConfigValueByKey(ConfigCode::MAXIMUM_WHOLE_CASE_SPLIT); + } + + public function handle() + { + $id = (int)$this->request->input('id'); + + $this->logInfo = $this->orderOptionCateringLogModel->find($id); + $this->siteInfo = $this->siteModel->find($this->logInfo->site_id); + + $this->check(); + + $this->closePrintAndPlaceOrder(); + + $this->getOrderData(); + + $this->buildPickupCode(); + + $res = match ($this->request->input('type')) { + CateringCode::OPTION_PRINT_YLY => $this->printOrderByYly(), + CateringCode::CATERING_STATUS_FINISH => $this->printOrderByCoding(), + }; + + return $this->return->success('success', $res); + } + + + + private function buildPickupCode(): int + { +// if ($this->isBuildPickupCode()) { +//// return $this->getPickupCode(); +// } + + $currentCode = 0; + $prefix = strtoupper(StringUtil::randStr(3)); + $codeRanges = []; + $takeFoodCodes = []; + foreach ($this->orderList as &$order) { + foreach ($order['good_list'] as &$goodItem) { + if ($currentCode % $this->printBoxMaxNum === 0) { + $startCode = $currentCode + 1; + $endCode = min($currentCode + $this->printBoxMaxNum, $this->totalCopies); + $codeGroups = $this->splitArrayIntoPartitions(range($startCode, $endCode)); + $codeRanges = $this->generateCodeRanges($codeGroups, $prefix); + } + $currentCode++; + $paddedCode = str_pad((string)$currentCode, 3, '0', STR_PAD_LEFT); + $fullCode = "$prefix-$paddedCode"; + $goodItem['take_food_code'] = $fullCode; + $goodItem['heapsort'] = $codeRanges[$currentCode] ?? ''; + $takeFoodCodes[] = [ + 'order_id' => $order['id'], + 'box_num' => $goodItem['box_num'], + 'take_food_code' => $fullCode, + 'heapsort' => $goodItem['heapsort'] + ]; + } + } + +// $this->insertTakeFoodCode($takeFoodCodes); + + return count($takeFoodCodes); + } + +// private function insertTakeFoodCode(array $codeRanges) +// { +// try { +// Db::beginTransaction(); +// +// /** +// * @var TakeFoodCode $takeFoodCodeModel +// */ +// $takeFoodCodeModel = app(TakeFoodCode::class); +// $takeFoodCodeModel->whereIn('order_id',$this->orderId)->delete(); +// if (!$takeFoodCodeModel->insertAll($takeFoodCodeArr)) throw new Exception('取餐码生成失败'); +// +// if (!$this->isBuildTakeFoodToday($this->todayDate,$this->cacheLockValue)) throw new Exception('取餐码生成失败-缓存已添加'); +// +// Db::commit(); +// return count($takeFoodCodeArr); +// }catch (Exception $e) { +// Db::rollback(); +// throw new ErrException($e->getMessage()); +// } +// } + + /** + * 将数组尽可能均匀地分割为指定份数 + * @param array $array 输入数组 + * @return array + * @throws ContainerExceptionInterface + * @throws NotFoundExceptionInterface + */ + private function splitArrayIntoPartitions(array $array): array + { + $partitionCount = $this->partitionCount; + $totalElements = count($array); + $baseChunkSize = intval($totalElements / $partitionCount); + $remainder = $totalElements % $partitionCount; + $result = []; + $currentIndex = 0; + + for ($partition = 0; $partition < $partitionCount; $partition++) { + $chunkSize = $baseChunkSize + ($partition < $remainder ? 1 : 0); + if ($chunkSize === 0) { + $result[] = []; + continue; + } + $result[] = array_slice($array, $currentIndex, $chunkSize); + $currentIndex += $chunkSize; + } + return $result; + } + /** + * 生成编码范围映射表 + * @param array $codeGroups 编码分组数组 + * @param string $prefix 前缀字符串 + * @return array + */ + private function generateCodeRanges(array $codeGroups, string $prefix): array { + $rangeMap = []; + + foreach ($codeGroups as $group) { + if (empty($group)) continue; + $minCode = min($group); + $maxCode = max($group); + $rangeValue = sprintf("%03d~%03d", $minCode, $maxCode); + + foreach ($group as $code) { + $rangeMap[$code] = $rangeValue; + } + } + return $rangeMap; + } + + /** + * @return void + */ + private function getOrderData(): void + { + $orderList = $this->orderModel + ->where('site_id', $this->siteInfo->id) + ->where('cycle_id', $this->cycleId) + ->where('type',OrderCode::ORDER_TYPE_OPTIONAL) + ->where('status',OrderCode::PAYED) + ->orderBy('id') + ->select(['id','cycle_id','order_sno','user_id','copies']) + ->get(); + + if ($orderList->isEmpty()) throw new ErrException('该点暂无自选订单'); + $orderList = $orderList->toArray(); + + $orderIds = array_column($orderList,'id'); + $orderGoodArr = $this->orderGoodModel->whereIn('order_id',$orderIds)->select(['order_id','sku_id','quantity','copies'])->get(); + if ($orderGoodArr->isEmpty()) throw new ErrException('商品数据丢失,请联系管理员'); + $orderGoodArr = $orderGoodArr->toArray(); + + $userIds = array_column($orderList,'user_id'); + $userInfos = $this->userModel->whereIn('id',$userIds)->select(['username', 'nickname', 'mobile', 'id'])->get(); + if ($userInfos->isEmpty()) throw new ErrException('用户数据丢失,请联系管理员'); + $userInfos = array_column($userInfos->toArray(),null,'id'); + + $skuIds = array_unique(array_column($orderGoodArr,'sku_id')); //todo code_number 还没写 + $skuCodeNumberArr = $this->skuModel->where('id','in',$skuIds)->pluck('code_number','id')->toArray(); + $orderGoodArr = $this->buildOrderCopiesData($orderGoodArr,$skuCodeNumberArr); + +// $adminInfo = $this->adminUserModel->find($this->siteInfo->delivered_id); + $driverInfo = $this->driverSequenceModel->find($this->siteInfo->delivered_id); + + foreach ($orderList as &$one) { + $one['dates'] = date('Y-m-d'); + $one['site_text'] = $this->siteInfo->name; + $one['nickname'] = $userInfos[$one['user_id']]['nickname'] ?? ''; + $one['mobile'] = $userInfos[$one['user_id']]['mobile'] ?? ''; + $one['driver_num'] = $driverInfo->driver_num; + $one['site_order'] = $this->siteInfo->sequence; + $one['real_name'] = $driverInfo->driver_name; + $one['good_list'] = array_values($orderGoodArr[$one['id']]); + } + + $this->orderList = $orderList; + $this->orderIds = $orderIds; + $this->totalCopies = array_sum(array_column($orderList,'copies')); + + //节省内存 优化协程内存占用 防止内存泄露 + unset($orderGoodArr,$skuCodeNumberArr,$skuIds,$userInfos,$userIds,$orderIds,$orderList); + } + + /** + * @param array $data + * @param array $skuInfoArr + * @return array + */ + private function buildOrderCopiesData(array $data, array $skuInfoArr): array + { + $result = []; + foreach ($data as $item) { + // 提取关键字段 + $orderId = $item["order_id"]; + $copies = $item["copies"]; + $skuId = $item["sku_id"]; + $quantity= $item["quantity"]; + + // 按层级构建数组结构 + if (!isset($result[$orderId])) { + $result[$orderId] = []; + } + + if (!isset($result[$orderId][$copies])) { + $result[$orderId][$copies] = [ + 'copies' => $copies, + 'sku' => [] + ]; + } + + $result[$orderId][$copies]['sku'][] = [ + 'id' => $skuId, + 'num' => $quantity, + 'code_num' => $skuInfoArr[$skuId]['code_number'] == '' ? 0 : (int)$skuInfoArr[$skuId]['code_number'], + ]; + } + + return $result; + } + + /** + * 打印置灰和截单 + * @return void + * @throws ContainerExceptionInterface + * @throws NotFoundExceptionInterface + */ + private function closePrintAndPlaceOrder(): void + { + //生成 key + $this->__initRedisKey(); + + //打印一次后置灰 + $this->closePrint(); + + //点位截单 + $this->closeSite(); + + //如果一条线的所有点位截单 则整线截单 + $this->closeWholeLine(); + } + + /** + * 检测数据 + * @return void + */ + private function check(): void + { + if (empty($this->logInfo)) throw new ErrException('配餐数据不存在'); + + if ($this->logInfo->quantity <= 0) throw new ErrException('该配餐数量为0,不可配餐'); + } + + private function printOrderByYly(): true + { + return true; + } + + private function printOrderByCoding() + { + return []; + } +} \ No newline at end of file diff --git a/app/Service/ServiceTrait/Admin/Catering/PrintTrait.php b/app/Service/ServiceTrait/Admin/Catering/PrintTrait.php new file mode 100644 index 0000000..aa171b4 --- /dev/null +++ b/app/Service/ServiceTrait/Admin/Catering/PrintTrait.php @@ -0,0 +1,158 @@ +printKey = AdminRedisKey::optionCateringIsPrint($this->cycleId); + $this->stopOrderKey = AdminRedisKey::optionCateringStopOrder($this->cycleId); + $this->pickupCodeKey = AdminRedisKey::optionCateringBuildPickupCode($this->cycleId); + $this->isCateringKey = AdminRedisKey::optionIsCatering($this->cycleId); + + $script = <<redis->eval($script, [$this->printKey, $this->stopOrderKey, $this->pickupCodeKey, $this->isCateringKey, CateringCode::REDIS_FINISH_VALUE, DateUtil::DAY], 4); + } + + /** + * @return void + * @throws ContainerExceptionInterface + * @throws NotFoundExceptionInterface + */ + protected function closePrint(): void + { + if (CateringCode::REDIS_FINISH_VALUE == $this->redis->hGet($this->printKey, $this->logInfo->id)) return; + + $this->redis->hSet($this->printKey, $this->logInfo->id, CateringCode::REDIS_FINISH_VALUE); + } + + /** + * @return void + * @throws ContainerExceptionInterface + * @throws NotFoundExceptionInterface + */ + protected function closeSite(): void + { + if (CateringCode::REDIS_FINISH_VALUE == $this->redis->hGet($this->stopOrderKey, $this->logInfo->site_id)) return; + + $this->redis->hSet($this->stopOrderKey, $this->logInfo->site_id, CateringCode::REDIS_FINISH_VALUE); + } + + /** + * @return void + * @throws ContainerExceptionInterface + * @throws NotFoundExceptionInterface + */ + protected function closeWholeLine(): void + { + $siteArr = $this->siteModel->where('delivered_id',$this->siteInfo->delivered_id)->pluck('id'); + + //没有日志数据 跳出 + $logArr = $this + ->orderOptionCateringLogModel + ->where('cycle_id',$this->cycleId) + ->whereIn('site_id',$siteArr) + ->pluck('site_id') + ->toArray(); + if (empty($logArr)) return; + + //没有缓存数据 跳出 + $cacheData = $this->redis->hGetAll($this->stopOrderKey); + if (empty($cacheData)) return; + $cacheData = array_diff(array_keys($cacheData),[0]); //需要排除默认写的值 + + //数量不对等 跳出 + if (count($logArr) !== count($cacheData)) return; + + //如果相反交集有一个不为空就是不对的 跳出 + if (!empty(array_diff($logArr,$cacheData)) || !empty(array_diff($cacheData,$logArr))) return; + + //批量写入 hashmap + $insertCacheData = []; + foreach ($siteArr as $one) { + $insertCacheData[$one] = CateringCode::REDIS_FINISH_VALUE; + } + + $this->redis->hMset($this->stopOrderKey, $insertCacheData); + } + + /** + * @return bool + * @throws ContainerExceptionInterface + * @throws NotFoundExceptionInterface + */ + protected function isBuildPickupCode(): bool + { + if (CateringCode::REDIS_FINISH_VALUE == $this->redis->hGet($this->pickupCodeKey, $this->logInfo->id)) return true; + + return false; + } + +} \ No newline at end of file diff --git a/app/Service/ServiceTrait/CateringTrait.php b/app/Service/ServiceTrait/CateringTrait.php index f63aa2d..5f865fd 100644 --- a/app/Service/ServiceTrait/CateringTrait.php +++ b/app/Service/ServiceTrait/CateringTrait.php @@ -3,6 +3,7 @@ namespace App\Service\ServiceTrait; use App\Cache\Redis\Api\SiteCache; +use App\Constants\Admin\CateringCode; use App\Constants\Common\OrderCode; use App\Model\OrderMealCateringLog; use App\Model\OrderOptionCateringLog; @@ -77,7 +78,7 @@ trait CateringTrait return; } - if ($logInfo->status == 2) { + if ($logInfo->status == CateringCode::CATERING_STATUS_FINISH) { $this->log->error(__CLASS__.':Function:refundCallBackHandle:manageSubOptionCateringLog:已经截单不需要修改,订单信息:'.json_encode($this->orderInfo->toArray())); return; } @@ -107,7 +108,7 @@ trait CateringTrait continue; } - if ($logInfo->status == 2) { + if ($logInfo->status == CateringCode::CATERING_STATUS_FINISH) { $this->log->error(__CLASS__.':Function:refundCallBackHandle:manageSubMealCateringLog:已经截单不需要修改,订单信息:'.json_encode($this->orderInfo->toArray()).':订单商品信息:'.json_encode($orderGoods)); continue; } @@ -143,10 +144,10 @@ trait CateringTrait $logInfo->kitchen_id = (int)$siteInfo['kitchen_id']; $logInfo->quantity = 0; $logInfo->add_staple_food_num = 0; - $logInfo->status = 1; + $logInfo->status = CateringCode::CATERING_STATUS_UNDERWAY; } - if ($logInfo->status == 2) return false; + if ($logInfo->status == CateringCode::CATERING_STATUS_FINISH) return false; $logInfo->quantity = $logInfo->quantity + $this->orderInfo->copies; $logInfo->add_staple_food_num = $this->orderInfo->add_staple_food_num + $addStapleFoodNum; @@ -174,10 +175,10 @@ trait CateringTrait $logInfo->cycle_id = $this->orderInfo->cycle_id; $logInfo->sku_id = (int)$key; $logInfo->quantity = 0; - $logInfo->status = 1; + $logInfo->status = CateringCode::CATERING_STATUS_UNDERWAY; } - if ($logInfo->status == 2) return false; + if ($logInfo->status == CateringCode::CATERING_STATUS_FINISH) return false; $logInfo->quantity = $logInfo->quantity + $orderGood;