diff --git a/.gitignore b/.gitignore index 496f0a0..fa572a1 100644 --- a/.gitignore +++ b/.gitignore @@ -6,4 +6,5 @@ .env /tests/tmp /tests/.phpunit.result.cache -/public/uploads \ No newline at end of file +/public/uploads +/webman \ No newline at end of file diff --git a/app/admin/controller/ApprovalFlowsController.php b/app/admin/controller/ApprovalFlowsController.php new file mode 100644 index 0000000..ec9bb60 --- /dev/null +++ b/app/admin/controller/ApprovalFlowsController.php @@ -0,0 +1,44 @@ +service = new ApprovalFlowsService(); + } + + /** + * 列表页数据格式化 + * + * @param array $data + * @return array + */ + // protected function indexFormat($data) + // { + // if (isset($data['created_at'])) + // $data['created_at'] = date('Y-m-d H:i:s', $data['created_at']); + // if (isset($data['updated_at'])) + // $data['updated_at'] = date('Y-m-d H:i:s', $data['updated_at']); + // return $data; + // } + + + public function desc() + { + + + // type sequential(顺序会签), parallel(并行会签), any(或签) + + + + } +} \ No newline at end of file diff --git a/app/admin/controller/ApprovalInstancesController.php b/app/admin/controller/ApprovalInstancesController.php new file mode 100644 index 0000000..c7bf7fb --- /dev/null +++ b/app/admin/controller/ApprovalInstancesController.php @@ -0,0 +1,33 @@ +service = new ApprovalInstancesService(); + } + + /** + * 列表页数据格式化 + * + * @param array $data + * @return array + */ + // protected function indexFormat($data) + // { + // if (isset($data['created_at'])) + // $data['created_at'] = date('Y-m-d H:i:s', $data['created_at']); + // if (isset($data['updated_at'])) + // $data['updated_at'] = date('Y-m-d H:i:s', $data['updated_at']); + // return $data; + // } +} \ No newline at end of file diff --git a/app/admin/controller/ApprovalRecordsController.php b/app/admin/controller/ApprovalRecordsController.php new file mode 100644 index 0000000..2506845 --- /dev/null +++ b/app/admin/controller/ApprovalRecordsController.php @@ -0,0 +1,142 @@ +service = new ApprovalRecordsService(); + } + + /** + * 列表页数据格式化 + * + * @param array $data + * @return array + */ + protected function indexFormat($data) + { + if (isset($data['records']) && count($data['records']) > 0) { + foreach ($data['records'] as $key => &$record) { + $result = [ + 'id' => $record['id'], + 'flows_name' => $record['instance']['flows']['name'], + + 'instance_id' => $record['instance_id'], + + 'biz_id' => $record['instance']['biz_id'], + 'biz_desc' => '备货单【'.$record['instance']['bizInfo']['order_no'].'】', + 'biz_type' => $record['instance']['flow_type'], + + 'initiator_name' => $record['instance']['initiator']['name'], + 'initiator_avatar' => $record['instance']['initiator']['avatar'], + + 'approver_name' => $record['approver']['name'], + 'approver_avatar' => $record['approver']['avatar'], + + 'step_index' => $record['step_index'], + 'action' => $record['action'], + 'action_desc' => SysDictItem::where('item_value', $record['action'])->where('status', 1)->value('item_label'), + 'status' => $record['status'], + 'status_desc' => SysDictItem::where('item_value', $record['status'])->where('status', 1)->value('item_label'), + 'comment' => $record['comment'], + 'approved_at' => $record['approved_at'], + + 'finished_at' => $record['instance']['finished_at'], + 'deadline_at' => $record['instance']['deadline_at'], + + 'created_at' => $record['created_at'] + ]; + $record = $result; + } + } + return $data; + } + + + public function approve(Request $request) + { + try { + $res = $this->service->serviceApprove($request); + return $this->success([], $res['msg']); + } catch (\Throwable $e) { + return $this->error($e->getMessage()); + } + } + + + public function logs(Request $request) + { + try { + $instanceId = $request->get('instance_id'); + $appro = new \app\common\utils\approval\service\ApprovalFlowService(); + $res = $appro->getSimpleTimeline($instanceId, 'name', 'prepend'); + return $this->success($res); + } catch (\Throwable $e) { + return $this->error($e->getMessage()); + } + } + + + public function todo(Request $request) + { + try { + $raw = $this->service->serviceList($request, 'todo'); + $data = $this->transformData($raw, method_exists($this, 'todoFormat') + ? [$this, 'todoFormat'] + : null); + return $this->success($data); + } catch (\Throwable $e) { + return $this->error($e->getMessage()); + } + } + + protected function todoFormat($data) + { + if (isset($data['records']) && count($data['records']) > 0) { + foreach ($data['records'] as $key => &$record) { + $result = [ + 'id' => $record['id'], + 'flows_name' => $record['instance']['flows']['name'], + + 'instance_id' => $record['instance_id'], + + 'biz_id' => $record['instance']['biz_id'], + 'biz_desc' => '备货单号【'.$record['instance']['bizInfo']['order_no'].'】', + 'biz_type' => $record['instance']['flow_type'], + + 'initiator_name' => $record['instance']['initiator']['name'], + 'initiator_avatar' => $record['instance']['initiator']['avatar'], + + 'approver_name' => $record['approver']['name'], + 'approver_avatar' => $record['approver']['avatar'], + + 'step_index' => $record['step_index'], + 'action' => $record['action'], + 'action_desc' => SysDictItem::where('item_value', $record['action'])->where('status', 1)->value('item_label'), + 'status' => $record['status'], + 'status_desc' => SysDictItem::where('item_value', $record['status'])->where('status', 1)->value('item_label'), + 'comment' => $record['comment'], + 'approved_at' => $record['approved_at'], + + 'finished_at' => $record['instance']['finished_at'], + 'deadline_at' => $record['instance']['deadline_at'], + + 'created_at' => $record['created_at'] + ]; + $record = $result; + } + } + return $data; + } +} \ No newline at end of file diff --git a/app/admin/controller/BrandController.php b/app/admin/controller/BrandController.php new file mode 100644 index 0000000..f416d9d --- /dev/null +++ b/app/admin/controller/BrandController.php @@ -0,0 +1,53 @@ +service = new BrandService(); + } + + /** + * 列表页数据格式化 + * + * @param array $data + * @return array + */ + protected function indexFormat($data) + { + if (isset($data['created_at'])) + $data['created_at'] = date('Y-m-d H:i:s', $data['created_at']); + if (isset($data['updated_at'])) + $data['updated_at'] = date('Y-m-d H:i:s', $data['updated_at']); + if (!empty($data['logo'])) + $data['logo_full'] = (new OssService)->generatePrivateUrl($data['logo']); + return $data; + } + + /** + * 数据格式化 + * + * @param array $data + * @return array + */ + protected function showFormat($data) + { + if (isset($data['created_at'])) + $data['created_at'] = date('Y-m-d H:i:s', $data['created_at']); + if (isset($data['updated_at'])) + $data['updated_at'] = date('Y-m-d H:i:s', $data['updated_at']); + if (empty($data['logo'])) + $data['logo_full'] = (new OssService)->generatePrivateUrl($data['logo']); + return $data; + } +} \ No newline at end of file diff --git a/app/admin/controller/BrandSeriesController.php b/app/admin/controller/BrandSeriesController.php new file mode 100644 index 0000000..c2d95f7 --- /dev/null +++ b/app/admin/controller/BrandSeriesController.php @@ -0,0 +1,48 @@ +service = new BrandSeriesService(); + } + + /** + * 列表页数据格式化 + * + * @param array $data + * @return array + */ + protected function indexFormat($data) + { + if (isset($data['created_at'])) + $data['created_at'] = date('Y-m-d H:i:s', $data['created_at']); + if (isset($data['updated_at'])) + $data['updated_at'] = date('Y-m-d H:i:s', $data['updated_at']); + return $data; + } + + /** + * 数据格式化 + * + * @param array $data + * @return array + */ + protected function showFormat($data) + { + if (isset($data['created_at'])) + $data['created_at'] = date('Y-m-d H:i:s', $data['created_at']); + if (isset($data['updated_at'])) + $data['updated_at'] = date('Y-m-d H:i:s', $data['updated_at']); + return $data; + } +} \ No newline at end of file diff --git a/app/admin/controller/StockOrdersController.php b/app/admin/controller/StockOrdersController.php new file mode 100644 index 0000000..8383f52 --- /dev/null +++ b/app/admin/controller/StockOrdersController.php @@ -0,0 +1,251 @@ +service = new StockOrdersService(); + } + + /** + * 列表页数据格式化 + * + * @param array $data + * @return array + */ + protected function indexFormat($data) + { + if (isset($data['created_at'])) $data['created_at'] = date('Y-m-d H:i:s', $data['created_at']); + if (isset($data['updated_at'])) $data['updated_at'] = date('Y-m-d H:i:s', $data['updated_at']); + if (isset($data['eta_ship_date'])) $data['eta_ship_date'] = date('Y-m-d', $data['eta_ship_date']); + if (isset($data['biz_date'])) $data['biz_date'] = date('Y-m-d', $data['biz_date']); + if (isset($data['latest_arrival_date'])) $data['latest_arrival_date'] = date('Y-m-d', $data['latest_arrival_date']); + + if (isset($data['id'])) { + $appro = new \app\common\utils\approval\service\ApprovalFlowService(); + $appro = $appro->getBriefStatusByBiz($data['id'], null, 'name'); + $map = ['processing'=>'审批中','pending'=>'审批中','approved'=>'已通过','rejected'=>'已拒绝','revoked'=>'已撤回']; + if ($appro) { + $data['appro_status'] = [ + 'status' => $appro['status'], + 'status_desc' => $map[$appro['status']], + 'name' => isset($appro['step'])?$appro['step']['name']:'' + ]; + } + } + return $data; + } + + + protected function showFormat($data) + { + if (isset($data['created_at'])) $data['created_at'] = date('Y-m-d H:i:s', $data['created_at']); + if (isset($data['updated_at'])) $data['updated_at'] = date('Y-m-d H:i:s', $data['updated_at']); + if (isset($data['eta_ship_date'])) $data['eta_ship_date'] = date('Y-m-d', $data['eta_ship_date']); + if (isset($data['biz_date'])) $data['biz_date'] = date('Y-m-d', $data['biz_date']); + if (isset($data['latest_arrival_date'])) $data['latest_arrival_date'] = date('Y-m-d', $data['latest_arrival_date']); + + if (!empty($data['contract'])) { + $data['contract_url'] = (new \app\common\service\OssService)->generatePrivateUrl($data['contract']); + } + + if (count($data['moneyValues']) > 0) { + $data['cif_total_amount'] = [ + 'exchange_rate' => $data['moneyValues']['exchange_rate'], + 'amount' => $data['moneyValues']['amount'], + 'currency' => $data['moneyValues']['currency'], + 'id' => $data['moneyValues']['id'] + ]; + } + + foreach ($data['items'] as &$item) { + foreach ($item['moneyValues'] as $mv) { + $item[$mv['field']] = [ + 'exchange_rate' => $mv['exchange_rate'], + 'amount' => $mv['amount'], + 'currency' => $mv['currency'], + 'id' => $mv['id'] + ]; + } + unset($item['moneyValues']); + + foreach ($item['options'] as $opt) { + $item['modify_list'][] = [ + "id" => $opt['id'], + "order_item_id" => $opt['order_item_id'], + "option_id" => $opt['option_id'], + "option_name_snapshot" => $opt['option_name_snapshot'], + "option_price" => $opt['option_price'] + ]; + } + unset($item['options']); + } + unset($item); + + + foreach ($data['accounts'] as $acc) { + $data['account_list'][] = [ + "id" => $acc['id'], + "order_id" => $acc['order_id'], + "seq_no" => $acc['seq_no'], + "remit_country_id" => $acc['remit_country_id'], + "remit_country_name_snapshot" => $acc['remit_country_name_snapshot'], + "remit_date" => date('Y-m-d', $acc['remit_date']), + "currency_code" => $acc['currency_code'], + "currency_name_snapshot" => $acc['currency_name_snapshot'], + "account_id" => $acc['account_id'], + "account_name_snapshot" => $acc['account_name_snapshot'], + "remitter_name" => $acc['remitter_name'], + "amount" => $acc['amount'] + ]; + } + unset($data['accounts']); + + $data['veh_list'] = $data['items']; + unset($data['items']); + return $data; + } + + + protected function printFormat($data) + { + $data['file_url_full'] = (!empty($data['file_url'])) ? (new \app\common\service\OssService)->generatePrivateUrl($data['file_url']) : ''; + return $data; + } + + + public function print(Request $request) + { + try { + $raw = $this->service->servicePrint($request); + $data = $this->transformData($raw, method_exists($this, 'printFormat') + ? [$this, 'printFormat'] + : null); + return $this->success($data); + } catch (\Throwable $e) { + return $this->error($e->getMessage()); + } + } + + public function detail(Request $request) + { + try { + $raw = $this->service->serviceDetail($request); + $data = $this->transformData($raw, method_exists($this, 'detailFormat') + ? [$this, 'detailFormat'] + : null); + return $this->success($data); + } catch (\Throwable $e) { + return $this->error($e->getMessage()); + } + } + + public function desc(Request $request) + { + + $data = [ + 'id' => '', //'主键', + 'order_no' => '', //'备货单编号(业务唯一)', + 'pi_no' => '', //'PI号'/'阿里订单号', + 'biz_date' => '', //'单据日期(时间戳)', + 'salesperson_id' => '', //'业务员ID(关联数据)', + 'salesperson_snapshot' => '', //'业务员姓名快照', + 'clearance_country_id' => '', //'清关目的国ID(关联数据)', + 'clearance_country_snapshot' => '', //'清关目的国名称快照', + // 'ali_order_no' => '', //'阿里订单号', + 'customer_source_id' => '', //'客户来源ID(关联数据)', + 'customer_source_snapshot' => '', //'客户来源名称快照', + 'customer_id' => '', //'客户ID(关联数据)', + 'customer_snapshot' => '', //'客户名称快照', + 'customer_country_id' => '', //'客户所在国ID(关联数据)', + 'customer_country_snapshot' => '', //'客户所在国名称快照', + 'declarant_name' => '', //'清关人(文本)', + 'eta_ship_date' => '', //'预计发运日(时间戳)', + 'shipping_method' => '', //'出运方式(如海运/空运/铁路等)', + 'cif_total_amount' => '', //'0.00' COMMENT '合计到岸价(CIF)', + 'domestic_port_id' => '', //'国内收货港ID(关联数据)', + 'domestic_port_snapshot' => '', //'国内收货港名称快照', + 'trade_term_id' => '', //'贸易条款ID(关联数据,EXW/FOB/CIF等)', + 'trade_term_snapshot' => '', //'贸易条款名称快照', + 'clearance_port_id' => '', //'清关目的港ID(关联数据)', + 'clearance_port_snapshot' => '', //'清关目的港名称快照', + 'latest_arrival_date' => '', //'最迟到港时间(时间戳)', + 'contract_exchange_rate' => '', //'合同汇率(下单锁定)', + 'settlement_currency' => '', //'CNY' COMMENT '结算币种', + 'remark' => '', //'备注', + // 'real_veh' => '', //'0' COMMENT '实车ID', + 'is_final' => '', //'0' COMMENT '是否终版1是0否', + 'status' => '', //'draft' COMMENT '单据状态:draft/submitted/approved/void/reversed等', + ]; + + // (?<=\x60 )[^\r\n']*(?=') + + $items = [ + // 'id' => '', // '主键', + // 'order_id' => '', // '所属备货单ID', + // 'product_code' => '', // '商品编号(可作外部编码)', + // 'sku_snapshot' => '', // '商品/车型名称快照', + // 'unit_price_value_id' => '', // '单价', + // 'handling_fee_value_id' => '', // '操作费', + // 'fob_transport_fee_value_id' => '', // 'FOB内陆运费', + // 'port_misc_fee_value_id' => '', // '港杂费', + // 'retrofit_fee_value_id' => '', // '改装费', + // 'total_price_value_id' => '', // '合计(按订单结算币计算)', + + 'vin' => '', // '车架号', + 'sku_id' => '', // '商品/车型ID(关联数据)', + 'model_snapshot' => '', // '车型(若与sku一致可复用;此处为显示快照)', + 'quantity' => '', // '1.000000' COMMENT '数量', + 'color_interior_id' => '', // '颜色内饰ID(关联数据)', + 'color_interior_snapshot' => '', // '颜色内饰名称快照', + 'color_exterior_id' => '', // '颜色外饰ID(关联数据)', + 'color_exterior_snapshot' => '', // '颜色外饰名称快照', + 'remark' => '', // '备注', + 'unit_price' => '', // '0.000000' COMMENT '单价(含/不含税按业务口径约定)', + 'handling_fee' => '', // '0.000000' COMMENT '手续费(按单车)', + 'fob_transport_fee' => '', // '0.000000' COMMENT 'FOB运输费(按单车)', + 'port_misc_fee' => '', // '0.000000' COMMENT '港杂(按单车)', + 'retrofit_fee' => '', // '0.000000' COMMENT '改装费(按单车)', + 'total_price' => '', // '0.000000' COMMENT '总价(quantity*unit_price,写时落地)', + 'official_price' => '', // 官方指导价 + 'discount_price' => '', // 优惠价 + + + 'modify_list' => [ + // 'order_item_id' => '', // '所属明细ID', + 'option_id' => '', // '配置项ID(关联数据)', + 'option_name_snapshot' => '', // '配置项名称快照', + 'option_price' => '', // '配置项价格(可选,写时快照)', + ] + ]; + + + $account = [ + 'order_id' => '', // '所属备货单ID', + // 'seq_no' => '', // '1' COMMENT '账户序号(行号)', + 'remit_country_id' => '', // '汇款国ID(关联数据)', + 'remit_country_name_snapshot' => '', // '汇款国名称快照', + 'remit_date' => '', // '汇款日期(时间戳)', + 'currency_code' => '', // 'USD' COMMENT '币种代码(如USD/CNY/AED等)', + 'currency_name_snapshot' => '', // '币种名称快照(可选)', + 'account_id' => '', // '账户ID(关联数据)', + 'account_name_snapshot' => '', // '账户名称/开户名快照', + 'remitter_name' => '', // '汇款人', + 'amount' => '', // '0.00' COMMENT '金额', + ]; + } +} \ No newline at end of file diff --git a/app/admin/controller/SysAccountController.php b/app/admin/controller/SysAccountController.php new file mode 100644 index 0000000..9343a84 --- /dev/null +++ b/app/admin/controller/SysAccountController.php @@ -0,0 +1,59 @@ +service = new SysAccountService(); + } + + /** + * 列表页数据格式化 + * + * @param array $data + * @return array + */ + protected function indexFormat($data) + { + if (isset($data['created_at'])) + $data['created_at'] = date('Y-m-d H:i:s', $data['created_at']); + if (isset($data['updated_at'])) + $data['updated_at'] = date('Y-m-d H:i:s', $data['updated_at']); + if (isset($data['currency_id']) && $data['currency_id']) + $data['currency_name'] = SysCurrency::where('id', $data['currency_id'])->value('currency_name'); + if (isset($data['corp_id']) && $data['corp_id']) + $data['corp_name'] = Corp::where('id', $data['corp_id'])->value('name'); + return $data; + } + + /** + * 数据格式化 + * + * @param array $data + * @return array + */ + protected function showFormat($data) + { + if (isset($data['created_at'])) + $data['created_at'] = date('Y-m-d H:i:s', $data['created_at']); + if (isset($data['updated_at'])) + $data['updated_at'] = date('Y-m-d H:i:s', $data['updated_at']); + if (isset($data['currency_id'])) + $data['currency_name'] = SysCurrency::where('id', $data['currency_id'])->value('currency_name'); + if (isset($data['corp_id']) && $data['corp_id']) + $data['corp_name'] = Corp::where('id', $data['corp_id'])->value('name'); + return $data; + } +} \ No newline at end of file diff --git a/app/admin/controller/SysCurrencyController.php b/app/admin/controller/SysCurrencyController.php new file mode 100644 index 0000000..db9d589 --- /dev/null +++ b/app/admin/controller/SysCurrencyController.php @@ -0,0 +1,63 @@ +service = new SysCurrencyService(); + } + + /** + * 列表页数据格式化 + * + * @param array $data + * @return array + */ + protected function indexFormat($data) + { + if (isset($data['created_at'])) + $data['created_at'] = date('Y-m-d H:i:s', $data['created_at']); + if (isset($data['updated_at'])) + $data['updated_at'] = date('Y-m-d H:i:s', $data['updated_at']); + if (isset($data['last_updated'])) + $data['last_updated'] = date('Y-m-d H:i:s', $data['last_updated']); + if (isset($data['country_id'])){ + $data['country_name'] = Country::where('id',$data['country_id'])->value('item_label'); + } else { + $data['country_name'] = ''; + } + return $data; + } + + /** + * 列表页数据格式化 + * + * @param array $data + * @return array + */ + protected function showFormat($data) + { + if (isset($data['created_at'])) + $data['created_at'] = date('Y-m-d H:i:s', $data['created_at']); + if (isset($data['updated_at'])) + $data['updated_at'] = date('Y-m-d H:i:s', $data['updated_at']); + if (isset($data['last_updated'])) + $data['last_updated'] = date('Y-m-d H:i:s', $data['last_updated']); + if (isset($data['country_id'])){ + $data['country_name'] = Country::where('id',$data['country_id'])->value('item_label'); + } else { + $data['country_name'] = ''; + } + return $data; + } +} \ No newline at end of file diff --git a/app/admin/controller/SysDictCategoryController.php b/app/admin/controller/SysDictCategoryController.php new file mode 100644 index 0000000..c0c2593 --- /dev/null +++ b/app/admin/controller/SysDictCategoryController.php @@ -0,0 +1,60 @@ +service = new SysDictCategoryService(); + } + + /** + * 列表页数据格式化 + * + * @param array $data + * @return array + */ + protected function indexFormat($data) + { + if (isset($data['created_at'])) + $data['created_at'] = date('Y-m-d H:i:s', $data['created_at']); + if (isset($data['updated_at'])) + $data['updated_at'] = date('Y-m-d H:i:s', $data['updated_at']); + return $data; + } + + /** + * 数据格式化 + * + * @param array $data + * @return array + */ + protected function showFormat($data) + { + if (isset($data['created_at'])) + $data['created_at'] = date('Y-m-d H:i:s', $data['created_at']); + if (isset($data['updated_at'])) + $data['updated_at'] = date('Y-m-d H:i:s', $data['updated_at']); + return $data; + } + + public function preset(Request $request) + { + try { + $params = $request->all(); + $result = (new SysDictCategoryService)->getList($params); + return $this->success($result, '获取字典项列表'); + } catch (\Throwable $e) { + return $this->error($e->getMessage()); + } + } +} \ No newline at end of file diff --git a/app/admin/controller/SysDictController.php b/app/admin/controller/SysDictController.php new file mode 100644 index 0000000..b75f923 --- /dev/null +++ b/app/admin/controller/SysDictController.php @@ -0,0 +1,95 @@ +service = new SysDictService(); + } + + /** + * 列表页数据格式化 + * + * @param array $data + * @return array + */ + protected function indexFormat($data) + { + if (isset($data['sysDictItem'])) + $data['sysDictItem'] = SysDictItem::where('dict_id',$data['id'])->get(); + if (isset($data['created_at'])) + $data['created_at'] = date('Y-m-d H:i:s', $data['created_at']); + if (isset($data['updated_at'])) + $data['updated_at'] = date('Y-m-d H:i:s', $data['updated_at']); + if (isset($data['category_id'])){ + $data['category'] = SysDictItem::where('id',$data['category_id'])->value('item_label'); + } else { + $data['category'] = ''; + } + return $data; + } + + /** + * 数据格式化 + * + * @param array $data + * @return array + */ + protected function showFormat($data) + { + if (isset($data['created_at'])) + $data['created_at'] = date('Y-m-d H:i:s', $data['created_at']); + if (isset($data['updated_at'])) + $data['updated_at'] = date('Y-m-d H:i:s', $data['updated_at']); + $data['sysDictItem'] = SysDictItem::where('dict_id',$data['id'])->get(); + return $data; + } + + + + public function listScopeFormat($data) + { + $rdata = []; + foreach ($data as $item) { + $rdata[$item['dict_code']] = $item['sysDictItem']; + } + return $rdata; + } + + public function listScope(Request $request) + { + try { + $raw = $this->service->serviceListAllScope($request); + $data = $this->transformData($raw, method_exists($this, 'listScopeFormat') + ? [$this, 'listScopeFormat'] + : null); + return $this->success($data); + } catch (\Throwable $e) { + return $this->error($e->getMessage()); + } + } + + public function group(Request $request) + { + try { + $raw = $this->service->group($request); + $data = $this->transformData($raw, method_exists($this, 'groupFormat') + ? [$this, 'groupFormat'] + : null); + return $this->success($data); + } catch (\Throwable $e) { + return $this->error($e->getMessage()); + } + } +} \ No newline at end of file diff --git a/app/admin/controller/SysDictItemController.php b/app/admin/controller/SysDictItemController.php new file mode 100644 index 0000000..9262933 --- /dev/null +++ b/app/admin/controller/SysDictItemController.php @@ -0,0 +1,48 @@ +service = new SysDictItemService(); + } + + /** + * 列表页数据格式化 + * + * @param array $data + * @return array + */ + protected function indexFormat($data) + { + if (isset($data['created_at'])) + $data['created_at'] = date('Y-m-d H:i:s', $data['created_at']); + if (isset($data['updated_at'])) + $data['updated_at'] = date('Y-m-d H:i:s', $data['updated_at']); + return $data; + } + + /** + * 数据格式化 + * + * @param array $data + * @return array + */ + protected function showFormat($data) + { + if (isset($data['created_at'])) + $data['created_at'] = date('Y-m-d H:i:s', $data['created_at']); + if (isset($data['updated_at'])) + $data['updated_at'] = date('Y-m-d H:i:s', $data['updated_at']); + return $data; + } +} \ No newline at end of file diff --git a/app/admin/controller/SysPortController.php b/app/admin/controller/SysPortController.php new file mode 100644 index 0000000..d98577e --- /dev/null +++ b/app/admin/controller/SysPortController.php @@ -0,0 +1,53 @@ +service = new SysPortService(); + } + + /** + * 列表页数据格式化 + * + * @param array $data + * @return array + */ + protected function indexFormat($data) + { + if (isset($data['created_at'])) + $data['created_at'] = date('Y-m-d H:i:s', $data['created_at']); + if (isset($data['updated_at'])) + $data['updated_at'] = date('Y-m-d H:i:s', $data['updated_at']); + if (isset($data['country_id'])) + if (isset($data['country_id'])) + $data['country_name'] = Country::where('id', $data['country_id'])->value('cn_addr'); + return $data; + } + + /** + * 数据格式化 + * + * @param array $data + * @return array + */ + protected function showFormat($data) + { + if (isset($data['created_at'])) + $data['created_at'] = date('Y-m-d H:i:s', $data['created_at']); + if (isset($data['updated_at'])) + $data['updated_at'] = date('Y-m-d H:i:s', $data['updated_at']); + $data['country_name'] = Country::where('id', $data['country_id'])->value('cn_addr'); + return $data; + } +} \ No newline at end of file diff --git a/app/admin/controller/UserController.php b/app/admin/controller/UserController.php index 25f2385..8c24c6c 100644 --- a/app/admin/controller/UserController.php +++ b/app/admin/controller/UserController.php @@ -154,4 +154,16 @@ class UserController extends BaseController return $this->error($e->getMessage()); } } + + + public function updateSign(Request $request) + { + try { + $data = $request->post(); + $this->service->serviceUpdateSign($request, $data); + return $this->success([], '签名成功'); + } catch (\Throwable $e) { + return $this->error($e->getMessage()); + } + } } \ No newline at end of file diff --git a/app/admin/controller/VehColorController.php b/app/admin/controller/VehColorController.php new file mode 100644 index 0000000..9f17c7c --- /dev/null +++ b/app/admin/controller/VehColorController.php @@ -0,0 +1,48 @@ +service = new VehColorService(); + } + + /** + * 列表页数据格式化 + * + * @param array $data + * @return array + */ + protected function indexFormat($data) + { + if (isset($data['created_at'])) + $data['created_at'] = date('Y-m-d H:i:s', $data['created_at']); + if (isset($data['updated_at'])) + $data['updated_at'] = date('Y-m-d H:i:s', $data['updated_at']); + return $data; + } + + /** + * 数据格式化 + * + * @param array $data + * @return array + */ + protected function showFormat($data) + { + if (isset($data['created_at'])) + $data['created_at'] = date('Y-m-d H:i:s', $data['created_at']); + if (isset($data['updated_at'])) + $data['updated_at'] = date('Y-m-d H:i:s', $data['updated_at']); + return $data; + } +} \ No newline at end of file diff --git a/app/admin/controller/VehModificationController.php b/app/admin/controller/VehModificationController.php new file mode 100644 index 0000000..fc057e1 --- /dev/null +++ b/app/admin/controller/VehModificationController.php @@ -0,0 +1,48 @@ +service = new VehModificationService(); + } + + /** + * 列表页数据格式化 + * + * @param array $data + * @return array + */ + protected function indexFormat($data) + { + if (isset($data['created_at'])) + $data['created_at'] = date('Y-m-d H:i:s', $data['created_at']); + if (isset($data['updated_at'])) + $data['updated_at'] = date('Y-m-d H:i:s', $data['updated_at']); + return $data; + } + + /** + * 数据格式化 + * + * @param array $data + * @return array + */ + protected function showFormat($data) + { + if (isset($data['created_at'])) + $data['created_at'] = date('Y-m-d H:i:s', $data['created_at']); + if (isset($data['updated_at'])) + $data['updated_at'] = date('Y-m-d H:i:s', $data['updated_at']); + return $data; + } +} \ No newline at end of file diff --git a/app/admin/logic/BrandLogic.php b/app/admin/logic/BrandLogic.php new file mode 100644 index 0000000..6b12364 --- /dev/null +++ b/app/admin/logic/BrandLogic.php @@ -0,0 +1,17 @@ +orderBy('sort','desc')->get(['id', 'category_name']); + } +} \ No newline at end of file diff --git a/app/admin/logic/SysDictItemLogic.php b/app/admin/logic/SysDictItemLogic.php new file mode 100644 index 0000000..99db163 --- /dev/null +++ b/app/admin/logic/SysDictItemLogic.php @@ -0,0 +1,17 @@ +validator; + } + + /** + * 构建查询条件 + */ + + protected function buildQueryConfig(Request $request, string $scene = 'default'): array + { + $p = $request->all(); + + $config = []; + $config['model'] = $this->modelClass; + switch ($scene) { + case 'index': + // ====== ====== 排序 ====== ====== + $config['order']['sort'] = 'desc'; + $config['order']['id'] = 'desc'; + + // ====== ====== 筛选 ====== ====== + $config['where']['brand_id'] = $p['brand_id']; + // 状态 + if (isset($p['status']) && in_array($p['status'], [0,1])) { + $config['where']['status'] = $p['status']; + } + // 关键字 + if (isset($p['keyword']) && $p['keyword']) { + $k = '%' . $p['keyword'] . '%'; + foreach (['series_code','series_name'] as $col) { + $config['orWhere'][] = [$col, 'like', $k]; + } + } + //$config['where']['status'] = 1; + break; + case 'show': + $config['where']['id'] = $p['id']; + //$config['where']['status'] = 1; + break; + case 'store': + break; + case 'update': + $config['where']['id'] = $p['id']; + break; + case 'destroy': + $config['where']['id'] = $p['id']; + break; + } + + return $config; + } + + /** + * 生成唯一的7位颜色编码 + * 格式:C + (O/I) + 5位字母数字组合 + * + * @param int $type 类型 + * @return string 7位唯一编码 + */ + public function generateUniqueModificationCode($brand_id) + { + $chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ'; + $maxAttempts = 10; // 防止无限循环 + + $brand_code = Brand::where('id', $brand_id)->value('brand_code'); + $brand_code = $brand_code??'BS'; + for ($attempt = 0; $attempt < $maxAttempts; $attempt++) { + // 生成后5位随机字符 + $suffix = ''; + for ($i = 0; $i < 6; $i++) { + $suffix .= $chars[rand(0, strlen($chars) - 1)]; + } + + $code = $brand_code . $suffix; + + // 检查是否已存在 + $existing = BrandSeries::where('series_code', $code)->first(); + if (!$existing) { + return $code; + } + } + + // 如果1000次尝试后仍然重复,使用时间戳确保唯一性 + $timestamp = substr(time(), -6); + return $brand_code . $timestamp; + } + + /** + * 添加前置处理 + */ + public function beforeServiceStore($data, Request $request) + { + $brand_id = $data['brand_id'] ?? 0; + $data['series_code'] = $this->generateUniqueModificationCode($brand_id); + return $data; + } +} \ No newline at end of file diff --git a/app/admin/service/BrandService.php b/app/admin/service/BrandService.php new file mode 100644 index 0000000..61e1e65 --- /dev/null +++ b/app/admin/service/BrandService.php @@ -0,0 +1,80 @@ +validator; + } + + /** + * 构建查询条件 + */ + + protected function buildQueryConfig(Request $request, string $scene = 'default'): array + { + $p = $request->all(); + + $config = []; + $config['model'] = $this->modelClass; + switch ($scene) { + case 'index': + // ====== ====== 排序 ====== ====== + $config['order']['hot'] = 'desc'; + $config['order']['sort'] = 'desc'; + $config['order']['id'] = 'desc'; + + // ====== ====== 筛选 ====== ====== + // 状态 + if (isset($p['status']) && in_array($p['status'], [0,1])) { + $config['where']['status'] = $p['status']; + } + // 关键字 + if (isset($p['keyword']) && $p['keyword']) { + $k = '%' . $p['keyword'] . '%'; + foreach (['brand_code','brand_name'] as $col) { + $config['orWhere'][] = [$col, 'like', $k]; + } + } + //$config['where']['status'] = 1; + break; + case 'show': + $config['where']['id'] = $p['id']; + //$config['where']['status'] = 1; + break; + case 'store': + break; + case 'update': + $config['where']['id'] = $p['id']; + break; + case 'destroy': + $config['where']['id'] = $p['id']; + break; + } + + return $config; + } +} \ No newline at end of file diff --git a/app/admin/service/SysAccountService.php b/app/admin/service/SysAccountService.php new file mode 100644 index 0000000..d413778 --- /dev/null +++ b/app/admin/service/SysAccountService.php @@ -0,0 +1,82 @@ +validator; + } + + /** + * 构建查询条件 + */ + protected function buildQueryConfig(Request $request, string $scene = 'default'): array + { + $p = $request->all(); + + $config = []; + $config['model'] = $this->modelClass; + switch ($scene) { + case 'index': + // ====== ====== 排序 ====== ====== + $config['order']['sort'] = 'desc'; + $config['order']['id'] = 'desc'; + + // ====== ====== 筛选 ====== ====== + // 状态 + if (isset($p['status']) && in_array($p['status'], [0,1])) { + $config['where']['status'] = $p['status']; + } + // 状态 + if (isset($p['type']) && $p['type']) { + $config['where']['type'] = $p['type']; + } + // 关键字 + if (isset($p['keyword']) && $p['keyword']) { + $k = '%' . $p['keyword'] . '%'; + foreach (['account_name','account_number','bank_name','bank_branch','description'] as $col) { + $config['orWhere'][] = [$col, 'like', $k]; + } + } + //$config['where']['status'] = 1; + break; + case 'show': + $config['where']['id'] = $p['id']; + //$config['where']['status'] = 1; + break; + case 'store': + break; + case 'update': + $config['where']['id'] = $p['id']; + break; + case 'destroy': + $config['where']['id'] = $p['id']; + break; + } + + return $config; + } +} \ No newline at end of file diff --git a/app/admin/service/SysCurrencyService.php b/app/admin/service/SysCurrencyService.php new file mode 100644 index 0000000..c3086da --- /dev/null +++ b/app/admin/service/SysCurrencyService.php @@ -0,0 +1,79 @@ +validator; + } + + + /** + * 构造 CrudUtil 查询配置 + */ + protected function buildQueryConfig(Request $request, string $scene = 'default'): array + { + $p = $request->all(); + + $config = []; + $config['model'] = $this->modelClass; + switch ($scene) { + case 'default': + $config['order']['id'] = 'desc'; + case 'index': + break; + case 'show': + $config['where']['id'] = $p['id']; + break; + case 'store': + break; + case 'update': + case 'destroy': + $config['where']['id'] = $p['id']; + break; + } + + return $config; + } + + public $translatFields = ['currency_name']; + + public function afterServiceStore($res) + { + $sourceLocale = locale(); + $data = $res->toArray(); + $traitsourceFields = Translat::extractSourceFields($data, $this->translatFields); + + (new TranslationService())->queueTranslations( + 'sys_currency', + $res['id'], + $sourceLocale, + $traitsourceFields + ); + } +} \ No newline at end of file diff --git a/app/admin/service/SysDictCategoryService.php b/app/admin/service/SysDictCategoryService.php new file mode 100644 index 0000000..28e7be6 --- /dev/null +++ b/app/admin/service/SysDictCategoryService.php @@ -0,0 +1,40 @@ +validator; + } + /** + * 获取系统list + */ + public function getList($params) + { + return (new SysDictCategoryLogic)->getList(); + } +} \ No newline at end of file diff --git a/app/admin/service/SysDictItemService.php b/app/admin/service/SysDictItemService.php new file mode 100644 index 0000000..98430b9 --- /dev/null +++ b/app/admin/service/SysDictItemService.php @@ -0,0 +1,93 @@ +validator; + } + + /** + * 构建查询条件 + */ + protected function buildQueryConfig(Request $request, string $scene = 'index'): array + { + $p = $request->all(); + + $config = []; + $config['model'] = $this->modelClass; + switch ($scene) { + case 'index': + // ====== ====== 排序 ====== ====== + $config['order']['sort'] = 'desc'; + + // ====== ====== 筛选 ====== ====== + + $config['where']['dict_id'] = $p['id']; + // 状态 + if (isset($p['status']) && in_array($p['status'], [0,1])) { + $config['where']['status'] = $p['status']; + } + // 关键字 + if (isset($p['keyword']) && $p['keyword']) { + $k = '%' . $p['keyword'] . '%'; + foreach (['item_label','item_value','remark'] as $col) { + $config['orWhere'][] = [$col, 'like', $k]; + } + } + break; + case 'show': + $config['where']['id'] = $p['id']; + break; + case 'update': + case 'destroy': + $config['where']['id'] = $p['id']; + break; + case 'store': + break; + } + + return $config; + } + + public $translatFields = ['item_label','remark']; + + public function afterServiceStore($res) + { + $sourceLocale = locale(); + $data = $res->toArray(); + $traitsourceFields = Translat::extractSourceFields($data, $this->translatFields); + + (new TranslationService())->queueTranslations( + 'sys_dict_item', + $res['id'], + $sourceLocale, + $traitsourceFields + ); + } +} \ No newline at end of file diff --git a/app/admin/service/SysDictService.php b/app/admin/service/SysDictService.php new file mode 100644 index 0000000..e46b07b --- /dev/null +++ b/app/admin/service/SysDictService.php @@ -0,0 +1,240 @@ +validator; + } + + + /** + * 构建查询条件 + */ + protected function buildQueryConfig(Request $request, string $scene = 'index'): array + { + $p = $request->all(); + + $config = []; + $config['model'] = $this->modelClass; + switch ($scene) { + case 'index': + // ====== ====== 排序 ====== ====== + $config['order']['id'] = 'desc'; + + // ====== ====== 筛选 ====== ====== + // 状态 + if (isset($p['status']) && in_array($p['status'], [0,1])) { + $config['where']['status'] = $p['status']; + } + // 分类筛选 + if (isset($p['category_id']) && !empty($p['category_id'])) { + $config['where']['category_id'] = $p['category_id']; + } + // 关键字 + if (isset($p['keyword']) && $p['keyword']) { + $k = '%' . $p['keyword'] . '%'; + foreach (['dict_name','dict_code','description'] as $col) { + $config['orWhere'][] = [$col, 'like', $k]; + } + } + + // ====== ====== 关联 ====== ====== + $config['with']['sysDictItem'][] = 'item_label'; + $config['with']['sysDictItem'][] = 'item_value'; + break; + case 'show': + $config['where']['id'] = $p['id']; + // ====== ====== 关联 ====== ====== + $config['with']['sysDictItem'][] = 'item_label'; + $config['with']['sysDictItem'][] = 'item_value'; + break; + case 'update': + case 'destroy': + $config['where']['id'] = $p['id']; + break; + case 'store': + break; + case 'scope': + $config['order']['id'] = 'desc'; + // 决定查id还是code + $scopeType = 0; + if (isset($p['scope_type']) && $p['scope_type'] > 0) { + $scopeType = 1; + } + if (isset($p['scope']) && !empty($p['scope'])) { + if ($scopeType == 1) { + $config['where']['dict_code'] = ['in', explode(',', $p['scope'])]; + } else { + $config['where']['id'] = ['in', explode(',', $p['scope'])]; + } + } + $config['with']['sysDictItem'][] = 'dict_id'; + $config['with']['sysDictItem'][] = 'item_label'; + $config['with']['sysDictItem'][] = 'item_value'; + break; + case 'group': + $config['order']['id'] = 'desc'; + $config['where']['id'] = 1; + $config['with']['sysDictItem'][] = 'dict_id'; + $config['with']['sysDictItem'][] = 'item_label'; + $config['with']['sysDictItem'][] = 'item_value'; + break; + } + + return $config; + } + + public function group(Request $request) + { + $config = $this->buildQueryConfig($request, 'group'); + $result = $this->logicShow($config); + return $this->postProcess('group', $result); + } + + + public function serviceListAllScope(Request $request) + { + $config = $this->buildQueryConfig($request, 'scope'); + $result = $this->logicListAll($config); + return $this->postProcess('ListAllScope', $result); + } + + + public function afterServiceListAllScope($data) + { + + // $data->each(function (&$item) { + + // }); + + return $data; + } + + public $translatFields = ['dict_name', 'description']; + + public function afterServiceStore($res) + { + $sourceLocale = locale(); + $data = $res->toArray(); + $traitsourceFields = Translat::extractSourceFields($data, $this->translatFields); + + (new TranslationService())->queueTranslations( + 'sys_dict', + $res['id'], + $sourceLocale, + $traitsourceFields + ); + } + + + /* + + 示例用法: + + // 1) 获取分类为“车辆类型”的所有字典 + $list = $dictService->getDictsByCategoryName('车辆类型'); + // => [['dict_code' => 'SUV', 'dict_name' => '越野车'], …] + + // 2) 获取某分类下某个 code 的名称 + $name = $dictService->getDictName('车辆类型', 'SUV'); + // => "越野车" + + // 3) 获取某分类下拉选项 + $options = $dictService->getDictOptions('车辆类型'); + // => ['SUV' => '越野车', 'SEDAN' => '轿车', …] + + // 4) 通过 code 直接拿名称(不管分类) + $name = $dictService->getNameByCode('SUV'); + // => "越野车" + + */ + + + /** + * 根据分类名获取字典列表 + */ + public function getDictsByCategoryName(string $categoryName): array + { + $category = SysDictCategory::where('category_name', $categoryName) + ->where('status', 1) + ->first(); + + if (!$category) { + return []; + } + + return $category->dicts()->get(['dict_code', 'dict_name'])->toArray(); + } + + /** + * 根据分类名 + 字典code 获取字典项名称 + */ + public function getDictName(string $categoryName, string $dictCode): ?string + { + $dict = SysDict::whereHas('category', function ($q) use ($categoryName) { + $q->where('category_name', $categoryName)->where('status', 1); + }) + ->where('dict_code', $dictCode) + ->where('status', 1) + ->whereNull('deleted_at') + ->first(); + + return $dict?->dict_name; + } + + /** + * 根据分类名获取下拉键值对 + * 返回格式:['code1' => '名称1', 'code2' => '名称2'] + */ + public function getDictOptions(string $categoryName): array + { + $dicts = $this->getDictsByCategoryName($categoryName); + + $options = []; + foreach ($dicts as $item) { + $options[$item['dict_code']] = $item['dict_name']; + } + + return $options; + } + + /** + * 根据字典 code 获取对应的名称(不依赖分类) + */ + public function getNameByCode(string $dictCode): ?string + { + $dict = SysDict::where('dict_code', $dictCode) + ->where('status', 1) + ->whereNull('deleted_at') + ->first(); + + return $dict?->dict_name; + } +} \ No newline at end of file diff --git a/app/admin/service/SysPortService.php b/app/admin/service/SysPortService.php new file mode 100644 index 0000000..a487848 --- /dev/null +++ b/app/admin/service/SysPortService.php @@ -0,0 +1,129 @@ +validator; + } + + /** + * 构建查询条件 + */ + + protected function buildQueryConfig(Request $request, string $scene = 'default'): array + { + $p = $request->all(); + + $config = []; + $config['model'] = $this->modelClass; + switch ($scene) { + case 'index': + // ====== ====== 排序 ====== ====== + $config['order']['sort'] = 'desc'; + $config['order']['id'] = 'desc'; + + // ====== ====== 筛选 ====== ====== + // 状态 + if (isset($p['status']) && in_array($p['status'], [0,1])) { + $config['where']['status'] = $p['status']; + } + // 关键字 + if (isset($p['keyword']) && $p['keyword']) { + $k = '%' . $p['keyword'] . '%'; + foreach (['port_code','port_name'] as $col) { + $config['orWhere'][] = [$col, 'like', $k]; + } + } + // 场景 + if (isset($p['scene']) && $p['scene']) { + if($p['country_id'] == 'domestic'){ + $config['where']['country_id'] = 45; + }else{ + $config['where']['country_id'] = ['<>', 45]; + } + } + //$config['where']['status'] = 1; + break; + case 'show': + $config['where']['id'] = $p['id']; + //$config['where']['status'] = 1; + break; + case 'store': + break; + case 'update': + $config['where']['id'] = $p['id']; + break; + case 'destroy': + $config['where']['id'] = $p['id']; + break; + } + + return $config; + } + + /** + * 添加前置处理 + */ + public function beforeServiceStore($data, Request $request) + { + if(empty($data['lat'])) + unset($data['lat']); + if(empty($data['lng'])) + unset($data['lng']); + return $data; + } + + public function afterServiceStore($res) + { + $sourceLocale = locale(); + $data = $res->toArray(); + $traitsourceFields = Translat::extractSourceFields($data, $this->translatFields); + + (new TranslationService())->queueTranslations( + 'sys_port_translation', + $res['id'], + $sourceLocale, + $traitsourceFields + ); + } + + /** + * 添加前置处理 + */ + public function beforeServiceUpdate($data, Request $request) + { + if(empty($data['lat'])) + unset($data['lat']); + if(empty($data['lng'])) + unset($data['lng']); + return $data; + } +} \ No newline at end of file diff --git a/app/admin/service/UserService.php b/app/admin/service/UserService.php index 6675a89..7f2fadd 100644 --- a/app/admin/service/UserService.php +++ b/app/admin/service/UserService.php @@ -90,6 +90,9 @@ class UserService if (isset($p['id']) && $p['id']) { $config['where']['id'] = $p['id']; } + if (isset($p['corp_id']) && $p['corp_id']) { + $config['where']['corp_id'] = $p['corp_id']; + } $config['fields'] = ['id','sn', 'name', 'email', 'sex', 'phone', 'avatar', 'corp_id', 'country_id', 'position', 'report_to', 'status', 'created_at']; @@ -113,6 +116,7 @@ class UserService $config['where']['id'] = $p['id']; break; case 'edit': + case 'updateSign': $config['where']['id'] = $request->user->uid; break; } @@ -154,4 +158,13 @@ class UserService $user = $this->logicUpdateBy($config,$data); return $user; } + + + + public function serviceUpdateSign(Request $request, array $data) + { + $config = $this->buildQueryConfig($request, 'updateSign'); + $user = $this->logicUpdateBy($config,$data); + return $user; + } } \ No newline at end of file diff --git a/app/admin/service/VehColorService.php b/app/admin/service/VehColorService.php new file mode 100644 index 0000000..7c111c8 --- /dev/null +++ b/app/admin/service/VehColorService.php @@ -0,0 +1,105 @@ +validator; + } + + public function afterServiceStore($res) + { + $sourceLocale = locale(); + $data = $res->toArray(); + $traitsourceFields = Translat::extractSourceFields($data, $this->translatFields); + + (new TranslationService())->queueTranslations( + 'veh_color_translation', + $res['id'], + $sourceLocale, + $traitsourceFields + ); + } + + /** + * 生成唯一的7位颜色编码 + * 格式:C + (O/I) + 5位字母数字组合 + * + * @param int $type 类型:2=I,其他=O + * @return string 7位唯一编码 + */ + public function generateUniqueColorCode($type) + { + $secondChar = $type ==2 ? 'I' : 'O'; + $chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ'; + $maxAttempts = 10; // 防止无限循环 + + for ($attempt = 0; $attempt < $maxAttempts; $attempt++) { + // 生成后5位随机字符 + $suffix = ''; + for ($i = 0; $i < 5; $i++) { + $suffix .= $chars[rand(0, strlen($chars) - 1)]; + } + + $colorCode = 'C' . $secondChar . $suffix; + + // 检查是否已存在 + $existing = VehColor::where('color_code', $colorCode)->first(); + if (!$existing) { + return $colorCode; + } + } + + // 如果1000次尝试后仍然重复,使用时间戳确保唯一性 + $timestamp = substr(time(), -5); + return 'C' . $secondChar . $timestamp; + } + + /** + * 添加前置处理 + */ + public function beforeServiceStore($data, Request $request) + { + $type = $data['type'] ?? 0; + $data['color_code'] = $this->generateUniqueColorCode($type); + $data['created_by'] = $request->user->uid; + $data['updated_by'] = $request->user->uid; + return $data; + } + + /** + * 添加前置处理 + */ + public function beforeServiceUpdate($data, Request $request) + { + $data['updated_by'] = $request->user->uid; + return $data; + } +} \ No newline at end of file diff --git a/app/admin/service/VehModificationService.php b/app/admin/service/VehModificationService.php new file mode 100644 index 0000000..038129b --- /dev/null +++ b/app/admin/service/VehModificationService.php @@ -0,0 +1,126 @@ +validator; + } + + /** + * 构建查询条件 + */ + + protected function buildQueryConfig(Request $request, string $scene = 'default'): array + { + $p = $request->all(); + + $config = []; + $config['model'] = $this->modelClass; + switch ($scene) { + case 'index': + // ====== ====== 排序 ====== ====== + $config['order']['sort'] = 'desc'; + $config['order']['id'] = 'desc'; + + // ====== ====== 筛选 ====== ====== + // 状态 + if (isset($p['status']) && in_array($p['status'], [0,1])) { + $config['where']['status'] = $p['status']; + } + // 状态 + if (isset($p['type']) && $p['type']) { + $config['where']['type'] = $p['type']; + } + // 关键字 + if (isset($p['keyword']) && $p['keyword']) { + $k = '%' . $p['keyword'] . '%'; + foreach (['code','name'] as $col) { + $config['orWhere'][] = [$col, 'like', $k]; + } + } + //$config['where']['status'] = 1; + break; + case 'show': + $config['where']['id'] = $p['id']; + //$config['where']['status'] = 1; + break; + case 'store': + break; + case 'update': + $config['where']['id'] = $p['id']; + break; + case 'destroy': + $config['where']['id'] = $p['id']; + break; + } + + return $config; + } + + /** + * 生成唯一的7位颜色编码 + * 格式:C + (O/I) + 5位字母数字组合 + * + * @param int $type 类型 + * @return string 7位唯一编码 + */ + public function generateUniqueModificationCode($type) + { + $chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ'; + $maxAttempts = 10; // 防止无限循环 + + for ($attempt = 0; $attempt < $maxAttempts; $attempt++) { + // 生成后5位随机字符 + $suffix = ''; + for ($i = 0; $i < 5; $i++) { + $suffix .= $chars[rand(0, strlen($chars) - 1)]; + } + + $code = 'C' . $type . $suffix; + + // 检查是否已存在 + $existing = VehModification::where('code', $code)->first(); + if (!$existing) { + return $code; + } + } + + // 如果1000次尝试后仍然重复,使用时间戳确保唯一性 + $timestamp = substr(time(), -5); + return 'C' . $type . $timestamp; + } + + /** + * 添加前置处理 + */ + public function beforeServiceStore($data, Request $request) + { + $type = $data['type'] ?? 0; + $data['code'] = $this->generateUniqueModificationCode($type); + return $data; + } +} \ No newline at end of file diff --git a/app/admin/targets/controller/SalesAnnualTargetsController.php b/app/admin/targets/controller/SalesAnnualTargetsController.php new file mode 100644 index 0000000..6a51d08 --- /dev/null +++ b/app/admin/targets/controller/SalesAnnualTargetsController.php @@ -0,0 +1,45 @@ +service = new SalesAnnualTargetsService(); + } + + /** + * 列表页数据格式化 + * + * @param array $data + * @return array + */ + protected function indexFormat($data) + { + if (isset($data['created_at'])) + $data['created_at'] = date('Y-m-d H:i:s', $data['created_at']); + if (isset($data['updated_at'])) + $data['updated_at'] = date('Y-m-d H:i:s', $data['updated_at']); + if (isset($data['corp_id'])){ + $data['corp_name'] = Corp::where('id', $data['corp_id'])->value('name'); +// $data['corp_name'] = $data['corp_name']??'合塑集团'; + } + if (isset($data['user_id'])) + $data['user_name'] = User::where('id', $data['user_id'])->value('name'); + + if (empty($data['corp_id']) || empty($data['user_id'])){ + $data['user_name'] = '合塑集团'; + } + return $data; + } +} \ No newline at end of file diff --git a/app/admin/targets/controller/SalesMonthlyTargetsController.php b/app/admin/targets/controller/SalesMonthlyTargetsController.php new file mode 100644 index 0000000..461d503 --- /dev/null +++ b/app/admin/targets/controller/SalesMonthlyTargetsController.php @@ -0,0 +1,40 @@ +service = new SalesMonthlyTargetsService(); + } + + /** + * 列表页数据格式化 + * + * @param array $data + * @return array + */ + protected function indexFormat($data) + { + if (isset($data['created_at'])) + $data['created_at'] = date('Y-m-d H:i:s', $data['created_at']); + if (isset($data['updated_at'])) + $data['updated_at'] = date('Y-m-d H:i:s', $data['updated_at']); + if (isset($data['corp_id'])) + $data['corp_name'] = Corp::where('id', $data['corp_id'])->value('name'); + if (isset($data['user_id'])) + $data['user_name'] = User::where('id', $data['user_id'])->value('name'); + + return $data; + } +} \ No newline at end of file diff --git a/app/admin/targets/controller/StatisticsController.php b/app/admin/targets/controller/StatisticsController.php new file mode 100644 index 0000000..950f247 --- /dev/null +++ b/app/admin/targets/controller/StatisticsController.php @@ -0,0 +1,156 @@ +service = new SalesAnnualTargetsService(); + } + + public function total(Request $request) + { + $param = $request->all(); + if (empty($param['year'])) { + return $this->error('year不能为空'); + } + + $data = SalesAnnualTargets::select('annual_target','corp_id') + ->whereNull('user_id') + ->where('year',$param['year']) + ->orderBy('corp_id') + ->get() + ->toArray(); + + foreach ($data as &$value) { + $where = []; + $where[] = ['year', '=', $param['year']]; + if (!empty($value['corp_id'])) { + $where[] = ['corp_id', '=', $value['corp_id']]; + } + $value['achieved'] = SalesMonthlyTargets::where($where)->sum('achieved'); + $value['progress'] = empty($value['annual_target'])?'100%':number_format(($value['achieved']/$value['annual_target']*100),2) . '%'; + $value['corp_name'] = empty($value['corp_id'])?'集团':Corp::where(['id' => $value['corp_id']])->value('name'); + } + return $this->success($data); + } + + public function ranking(Request $request){ + $param = $request->all(); + $year = empty($param['year']) ? date('Y') : $param['year']; + $where = []; + $where[] = ['year', '=', $year]; + $ranking = SalesMonthlyTargets::select('corp_id', 'user_id', Db::raw('SUM(achieved) AS achieved')) + ->where($where) + ->groupBy('corp_id', 'user_id') + ->orderBy('achieved', 'desc') + ->get(); + foreach ($ranking as $key => $value) { + $ranking[$key]['corp_name'] = Corp::where(['id' => $value['corp_id']])->value('name'); + $ranking[$key]['user_name'] = User::where(['id' => $value['user_id']])->value('name'); + } + return $this->success($ranking); + } + + public function achievedUserByMonth(Request $request){ + $param = $request->all(); + $year = empty($param['year']) ? date('Y') : $param['year']; + $where = []; + $where[] = ['year', '=', $year]; + $ranking = SalesMonthlyTargets::select('corp_id', 'user_id', 'month', Db::raw('SUM(achieved) AS achieved')) + ->where($where) + ->groupBy('corp_id', 'user_id', 'month') + ->orderBy('user_id', 'asc') + ->orderBy('month', 'asc') + ->get(); + $result = []; + foreach ($ranking as $key => $value) { + $result[$value['user_id']]['month'][$value['month']] = $value['achieved']; + $result[$value['user_id']]['user_id'] = $value['user_id']; + $result[$value['user_id']]['corp_id'] = $value['corp_id']; + } + foreach ($result as $key => $value) { + for($i = 1; $i <= 12; $i++){ + $result[$key]['month'][$i] = $result[$key]['month'][$i]??0; + } + $result[$key]['month'] = array_values($result[$key]['month']); + $result[$key]['corp_name'] = Corp::where(['id' => $value['corp_id']])->value('name'); + $result[$key]['user_name'] = User::where(['id' => $value['user_id']])->value('name'); + } + $result = array_values($result); + return $this->success($result); + } + + /** + * 个人年度目标完成度(百分比) + * @param Request $request + * @return \support\Response + */ + public function userProgressh(Request $request){ + $param = $request->all(); + $year = empty($param['year']) ? date('Y') : $param['year']; + $where = []; + $where[] = ['year', '=', $year]; + $achieved = SalesMonthlyTargets::select('corp_id', 'user_id', Db::raw('SUM(achieved) AS achieved')) + ->where($where) + ->groupBy('corp_id', 'user_id') + ->orderBy('achieved', 'desc') + ->get() + ->toArray(); + $sort = []; + foreach ($achieved as $key => $value) { + $achieved[$key]['corp_name'] = Corp::where(['id' => $value['corp_id']])->value('name'); + $achieved[$key]['user_name'] = User::where(['id' => $value['user_id']])->value('name'); + $target = SalesAnnualTargets::where(['user_id' => $value['user_id']])->value('annual_target'); + $achieved[$key]['progressh'] = empty($target) ? 0 : round(($value['achieved'] / $target) * 100, 2); + $sort[] = $achieved[$key]['progressh']; + } + array_multisort($sort, SORT_DESC, SORT_NUMERIC, $achieved); + + foreach ($achieved as $key => $value) { + $achieved[$key]['progressh'] = $value['progressh']; + } + return $this->success($achieved); + } + + /** + * 个人累积完成数量(辆) + * @param Request $request + * @return \support\Response + */ + public function userAchieved(Request $request){ + $param = $request->all(); + $year = empty($param['year']) ? date('Y') : $param['year']; + $where = []; + $where[] = ['year', '=', $year]; + $achieved = SalesMonthlyTargets::select('corp_id', 'user_id', Db::raw('SUM(achieved) AS achieved')) + ->where($where) + ->groupBy('corp_id', 'user_id') + ->orderBy('achieved', 'desc') + ->get() + ->toArray(); + foreach ($achieved as $key => $value) { + $achieved[$key]['corp_name'] = Corp::where(['id' => $value['corp_id']])->value('name'); + $achieved[$key]['user_name'] = User::where(['id' => $value['user_id']])->value('name'); + $target = SalesAnnualTargets::where(['user_id' => $value['user_id']])->value('annual_target'); + $achieved[$key]['annual_target'] = empty($target) ? 0 : $target; + $achieved[$key]['total'] = [intval($value['achieved']), $achieved[$key]['annual_target']]; + } + return $this->success($achieved); + } + +} \ No newline at end of file diff --git a/app/admin/targets/logic/SalesAnnualTargetsLogic.php b/app/admin/targets/logic/SalesAnnualTargetsLogic.php new file mode 100644 index 0000000..e184e42 --- /dev/null +++ b/app/admin/targets/logic/SalesAnnualTargetsLogic.php @@ -0,0 +1,17 @@ +validator; + } + + /** + * 构建查询条件 + */ + protected function buildQueryConfig(Request $request, string $scene = 'index'): array + { + $p = $request->all(); + + $config = []; + $config['model'] = $this->modelClass; + switch ($scene) { + case 'index': + if (isset($p['year']) && $p['year']) { + $config['where']['year'] = $p['year']; + } + $config['order']['id'] = 'desc'; + break; + case 'show': + case 'update': + case 'destroy': + $config['where']['id'] = $p['id']; + break; + case 'store': + break; + } + + return $config; + } + // ====== 前置处理 ====== + /** + * 添加前置处理 + */ + public function beforeServiceStore($data, Request $request) + { + if(isset($data['user_id']) && $data['user_id'] && empty($data['corp_id'])){ + $data['corp_id'] = User::where('id', $data['user_id'])->value('corp_id'); + } + if(isset($data['corp_id']) && empty($data['corp_id'])){ + unset($data['corp_id']); + } + if(isset($data['user_id']) && empty($data['user_id'])){ + unset($data['user_id']); + } + if(isset($data['user_id']) && isset($data['user_id'])){ + $annualTargets = SalesAnnualTargets::where('year',$data['year'])->where('corp_id',$data['corp_id'])->where('user_id', $data['user_id'])->first(); + }else{ + $annualTargets = SalesAnnualTargets::where('year',$data['year'])->whereNull('corp_id')->whereNull('user_id')->first(); + } + + if($annualTargets){ + throw new BusinessException('目标重复添加'); + } + + return $data; + } +} \ No newline at end of file diff --git a/app/admin/targets/service/SalesMonthlyTargetsService.php b/app/admin/targets/service/SalesMonthlyTargetsService.php new file mode 100644 index 0000000..a093fc5 --- /dev/null +++ b/app/admin/targets/service/SalesMonthlyTargetsService.php @@ -0,0 +1,75 @@ +validator; + } + + /** + * 构建查询条件 + */ + protected function buildQueryConfig(Request $request, string $scene = 'index'): array + { + $p = $request->all(); + + $config = []; + $config['model'] = $this->modelClass; + switch ($scene) { + case 'index': + if (isset($p['year']) && $p['year']) { + $config['where']['year'] = $p['year']; + } + $config['order']['year'] = 'desc'; + $config['order']['user_id'] = 'desc'; + $config['order']['month'] = 'asc'; + $config['order']['id'] = 'desc'; + break; + case 'show': + case 'update': + case 'destroy': + $config['where']['id'] = $p['id']; + break; + case 'store': + break; + } + + return $config; + } + + // ====== 前置处理 ====== + /** + * 添加前置处理 + */ + public function beforeServiceStore($data, Request $request) + { + $data['corp_id'] = User::where('id', $data['user_id'])->value('corp_id'); + + return $data; + } +} \ No newline at end of file diff --git a/app/admin/targets/validate/SalesAnnualTargetsValidate.php b/app/admin/targets/validate/SalesAnnualTargetsValidate.php new file mode 100644 index 0000000..4a32545 --- /dev/null +++ b/app/admin/targets/validate/SalesAnnualTargetsValidate.php @@ -0,0 +1,29 @@ + [ + + ], + + 'update' => [ + + ] + ]; +} \ No newline at end of file diff --git a/app/admin/targets/validate/SalesMonthlyTargetsValidate.php b/app/admin/targets/validate/SalesMonthlyTargetsValidate.php new file mode 100644 index 0000000..955adbb --- /dev/null +++ b/app/admin/targets/validate/SalesMonthlyTargetsValidate.php @@ -0,0 +1,29 @@ + [ + + ], + + 'update' => [ + + ] + ]; +} \ No newline at end of file diff --git a/app/admin/validate/BrandSeriesValidate.php b/app/admin/validate/BrandSeriesValidate.php new file mode 100644 index 0000000..31e30e2 --- /dev/null +++ b/app/admin/validate/BrandSeriesValidate.php @@ -0,0 +1,29 @@ + [ + + ], + + 'update' => [ + + ] + ]; +} \ No newline at end of file diff --git a/app/admin/validate/BrandValidate.php b/app/admin/validate/BrandValidate.php new file mode 100644 index 0000000..370728d --- /dev/null +++ b/app/admin/validate/BrandValidate.php @@ -0,0 +1,29 @@ + [ + + ], + + 'update' => [ + + ] + ]; +} \ No newline at end of file diff --git a/app/admin/validate/SysAccountValidate.php b/app/admin/validate/SysAccountValidate.php new file mode 100644 index 0000000..093ebb1 --- /dev/null +++ b/app/admin/validate/SysAccountValidate.php @@ -0,0 +1,31 @@ + 'require|integer|gt:0', + ]; + + // 提示 + protected $message = [ + 'corp_id.require' => 'ID不能为空', + 'corp_id.integer' => 'ID必须是整数', + 'corp_id.gt' => 'ID必须大于0', + ]; + + // 场景 + protected $scene = [ + 'store' => [ + 'corp_id' + ], + + 'update' => [ + 'corp_id' + ] + ]; +} \ No newline at end of file diff --git a/app/admin/validate/SysCurrencyValidate.php b/app/admin/validate/SysCurrencyValidate.php new file mode 100644 index 0000000..4aebb21 --- /dev/null +++ b/app/admin/validate/SysCurrencyValidate.php @@ -0,0 +1,29 @@ + [ + + ], + + 'update' => [ + + ] + ]; +} \ No newline at end of file diff --git a/app/admin/validate/SysDictCategoryValidate.php b/app/admin/validate/SysDictCategoryValidate.php new file mode 100644 index 0000000..f494854 --- /dev/null +++ b/app/admin/validate/SysDictCategoryValidate.php @@ -0,0 +1,29 @@ + [ + + ], + + 'update' => [ + + ] + ]; +} \ No newline at end of file diff --git a/app/admin/validate/SysDictItemValidate.php b/app/admin/validate/SysDictItemValidate.php new file mode 100644 index 0000000..2d4ad58 --- /dev/null +++ b/app/admin/validate/SysDictItemValidate.php @@ -0,0 +1,29 @@ + [ + + ], + + 'update' => [ + + ] + ]; +} \ No newline at end of file diff --git a/app/admin/validate/SysDictValidate.php b/app/admin/validate/SysDictValidate.php new file mode 100644 index 0000000..bdb6d1e --- /dev/null +++ b/app/admin/validate/SysDictValidate.php @@ -0,0 +1,29 @@ + [ + + ], + + 'update' => [ + + ] + ]; +} \ No newline at end of file diff --git a/app/admin/validate/SysPortValidate.php b/app/admin/validate/SysPortValidate.php new file mode 100644 index 0000000..26cb9cd --- /dev/null +++ b/app/admin/validate/SysPortValidate.php @@ -0,0 +1,29 @@ + [ + + ], + + 'update' => [ + + ] + ]; +} \ No newline at end of file diff --git a/app/admin/validate/VehColorValidate.php b/app/admin/validate/VehColorValidate.php new file mode 100644 index 0000000..1c619ec --- /dev/null +++ b/app/admin/validate/VehColorValidate.php @@ -0,0 +1,29 @@ + [ + + ], + + 'update' => [ + + ] + ]; +} \ No newline at end of file diff --git a/app/admin/validate/VehModificationValidate.php b/app/admin/validate/VehModificationValidate.php new file mode 100644 index 0000000..805b049 --- /dev/null +++ b/app/admin/validate/VehModificationValidate.php @@ -0,0 +1,29 @@ + [ + + ], + + 'update' => [ + + ] + ]; +} \ No newline at end of file diff --git a/app/common/controller/PresetController.php b/app/common/controller/PresetController.php index 37073c4..1abe3de 100644 --- a/app/common/controller/PresetController.php +++ b/app/common/controller/PresetController.php @@ -3,6 +3,15 @@ namespace app\common\controller; use app\admin\service\RoleService; +use app\common\service\BrandSeriesService; +use app\common\service\BrandService; +use app\common\service\SysAccountService; +use app\common\service\SysCurrencyService; +use app\common\service\SysDictItemService; +use app\common\service\SysDictService; +use app\common\service\SysPortService; +use app\common\service\VehColorService; +use app\common\service\VehModificationService; use support\Request; use app\common\service\CorpService; use app\common\service\CorpFeishuAppService; @@ -76,6 +85,100 @@ class PresetController extends BaseController } } + public function currency() + { + try { + $result = (new SysCurrencyService)->getList(); + return $this->success($result, '获取货币列表'); + } catch (\Throwable $e) { + return $this->error($e->getMessage()); + } + } + + public function brand() + { + try { + $result = (new BrandService)->getList(); + return $this->success($result, '获取品牌列表'); + } catch (\Throwable $e) { + return $this->error($e->getMessage()); + } + } + + public function series(Request $request) + { + try { + $params = $request->all(); + $result = (new BrandSeriesService)->getList($params); + return $this->success($result, '获取车系列表'); + } catch (\Throwable $e) { + return $this->error($e->getMessage()); + } + } + + public function account() + { + try { + $result = (new SysAccountService)->getList(); + return $this->success($result, '获取账号列表'); + } catch (\Throwable $e) { + return $this->error($e->getMessage()); + } + } + + public function dict() + { + try { + $result = (new SysDictService)->getList(); + return $this->success($result, '获取字典列表'); + } catch (\Throwable $e) { + return $this->error($e->getMessage()); + } + } + + public function dictItem(Request $request) + { + try { + $params = $request->all(); + $result = (new SysDictItemService)->getList($params); + return $this->success($result, '获取字典项列表'); + } catch (\Throwable $e) { + return $this->error($e->getMessage()); + } + } + + public function port(Request $request) + { + try { + $params = $request->all(); + $result = (new SysPortService)->getList($params); + return $this->success($result, '获取港口列表'); + } catch (\Throwable $e) { + return $this->error($e->getMessage()); + } + } + + public function color(Request $request) + { + try { + $params = $request->all(); + $result = (new VehColorService)->getList($params); + return $this->success($result, '获取颜色列表'); + } catch (\Throwable $e) { + return $this->error($e->getMessage()); + } + } + + public function modification() + { + try { + $result = (new VehModificationService)->getList(); + return $this->success($result, '获取货币列表'); + } catch (\Throwable $e) { + return $this->error($e->getMessage()); + } + } + public function cacheClear() { diff --git a/app/common/controller/UploadController.php b/app/common/controller/UploadController.php index e08fa9b..678034b 100644 --- a/app/common/controller/UploadController.php +++ b/app/common/controller/UploadController.php @@ -3,8 +3,12 @@ namespace app\common\controller; use app\common\service\OssService; +use app\common\service\UploadedFilesService; +use app\common\service\UploadService; use app\common\traits\ResponseTrait; +use support\Log; use support\Request; +use support\exception\BusinessException; use Shopwwi\WebmanFilesystem\Facade\Storage; class UploadController @@ -22,15 +26,41 @@ class UploadController $scene = $request->input('scene', 'image'); $file = $request->file('file'); if ($file && $file->isValid()) { - $pathUrl = '/uploads/'.$scene.'/'.date('Ymd').'/'.md5($file->getUploadName() . time()).'.'.$file->getUploadExtension(); + $fileName = md5($file->getUploadName() . time()).'.'.$file->getUploadExtension(); + $pathUrl = '/uploads/'.$scene.'/'.date('Ymd').'/'.$fileName; $fullPathUrl = public_path() . $pathUrl; $file->move($fullPathUrl); + $data = [ 'domain' => getenv('MAIN_DOMAIN'), 'file_url' => $pathUrl, 'full_url' => getenv('MAIN_DOMAIN') . $pathUrl, 'scene' => $scene, ]; + + // 记录文件信息到数据库 + try { + $uploadedFilesService = new UploadedFilesService(); + $partition = $scene; + $category = $request->input('category', 'general'); + + $fileInfo = [ + 'file_name' => basename($fileName), + 'file_path' => $pathUrl, + 'file_size' => $file->getSize(), + 'file_type' => $file->getUploadMimeType(), + 'original_name' => $file->getUploadName(), + ]; + + $recordResult = $uploadedFilesService->recordFileInfo($fileInfo, $request, $partition, $category); + $data['file_id'] = $recordResult['id']; + $data['partition'] = $partition; + $data['category'] = $category; + } catch (\Throwable $e) { + // 记录失败不影响上传结果,只记录日志 + Log::error('记录文件信息失败: ' . $e->getMessage()); + } + return $this->success($data, 'upload success'); } return $this->error('file not found'); @@ -43,15 +73,22 @@ class UploadController public function uploadFile(Request $request) { try { + $scene = $request->input('scene'); $file = $request->file('file'); if (!$file || !$file->isValid()) { return $this->error('上传失败'); } + if (empty($scene)) { +// return $this->error('上传场景不能为空'); + $sceneUrl = ''; + }else{ + $sceneUrl = $scene . '/'; + } $originalName = $file->getUploadName(); $result = Storage::adapter('oss') - ->path('vehicle_album/'.date('Ymd')) + ->path('x_server/' . $sceneUrl . date('Ymd')) ->size(1024*1024*5) ->upload($file); @@ -63,6 +100,11 @@ class UploadController 'file_url' => $ossservice->generatePrivateUrl($result->file_name) ]; + // 记录文件信息到数据库 + $category = $request->input('category', 'general'); + $res = $this->recordFileInfo($result,$request,$file,$scene,$category); + $data = array_merge($data, $res); + return $this->success($data); }catch (\Throwable $e){ return $this->error($e->getMessage()); @@ -73,14 +115,21 @@ class UploadController public function uploadImage(Request $request) { try { + $scene = $request->input('scene'); $file = $request->file('image'); if (!$file || !$file->isValid()) { return $this->error('上传失败'); } + if (empty($scene)) { +// return $this->error('上传场景不能为空'); + $sceneUrl = ''; + }else{ + $sceneUrl = $scene . '/'; + } $result = Storage::adapter('oss') - ->path('vehicle_album/'.date('Ymd')) + ->path('x_server/' . $sceneUrl . date('Ymd')) ->size(1024*1024*5) ->extYes(['image/jpeg','image/png']) ->extNo(['image/gif']) @@ -94,6 +143,11 @@ class UploadController 'url' => $ossservice->generatePrivateUrl($result->file_name) ]; + // 记录文件信息到数据库 + $category = $request->input('category', 'general'); + $res = $this->recordFileInfo($result,$request,$file,$scene,$category); + $data = array_merge($data, $res); + return json(['errno' => 0, 'data' => $data]); // return $this->success($data); } catch (\Throwable $e) { @@ -102,6 +156,123 @@ class UploadController } } + public function recordFileInfo($result, $request, $file, $scene, $category) + { + try { + $uploadedFilesService = new UploadedFilesService(); + $partition = $scene; + + $fileInfo = [ + 'file_name' => basename($result->file_name), + 'file_path' => $result->file_name, // OSS存储路径 + 'file_size' => $file->getSize(), + 'file_type' => $file->getUploadMimeType(), + 'original_name' => $file->getUploadName(), + ]; + + $recordResult = $uploadedFilesService->recordFileInfo($fileInfo, $request, $partition, $category); + $data = []; + $data['file_id'] = $recordResult['id']; + $data['partition'] = $partition; + $data['category'] = $category; + return $data; + } catch (\Throwable $e) { + // 记录失败不影响上传结果,只记录日志 + Log::error('记录文件信息失败: ' . $e->getMessage()); + return []; + } + } + + /** + * 获取文件信息 + * + * @param Request $request + * @return \support\Response + */ + public function getFileInfo(Request $request) + { + try { + $fileId = $request->input('id'); + + if (!$fileId) { + return $this->error('文件ID不能为空'); + } + + $uploadedFilesService = new UploadedFilesService(); + $fileInfo = $uploadedFilesService->getFileInfo($fileId); + + if (!$fileInfo) { + return $this->error('文件不存在'); + } + + return $this->success($fileInfo); + } catch (\Throwable $e) { + return $this->error('获取文件信息失败: ' . $e->getMessage()); + } + } + + /** + * 删除文件 + * + * @param Request $request + * @return \support\Response + */ + public function deleteFile(Request $request) + { + try { + $fileId = $request->input('id'); + $path = $request->input('path'); + + $uploadedFilesService = new UploadedFilesService(); + if(!empty($path) && empty($fileId)){ + $where = ['file_path' => $path]; + $file = $uploadedFilesService->getFile($where); + $fileId = $file->id; + }elseif(empty($path) && !empty($fileId)){ + $where = ['id' => $fileId]; + $file = $uploadedFilesService->getFile($where); + $path = $file->file_path; + } + if (!$fileId) { + return $this->error('文件ID与路径不能为空'); + } + + // 删除 OSS 上的文件 + $ossService = new OssService(); + $ossService->deleteFile($path); + + $result = $uploadedFilesService->deleteFile($fileId); + + if ($result) { + return $this->success([], '文件删除成功'); + } else { + return $this->error('文件删除失败'); + } + } catch (\Throwable $e) { + return $this->error('文件删除失败: ' . $e->getMessage()); + } + } + + /** + * 获取分区配置 + * + * @param Request $request + * @return \support\Response + */ + public function getPartitionConfig(Request $request) + { + try { + $uploadedFilesService = new UploadedFilesService(); + $config = $uploadedFilesService->getPartitionConfig(); + + return $this->success($config); + } catch (\Throwable $e) { + return $this->error('获取分区配置失败: ' . $e->getMessage()); + } + } + + + public function proxy(Request $request) { $filePath = $request->get('file'); // 原OSS文件路径 diff --git a/app/common/controller/VehColorController.php b/app/common/controller/VehColorController.php new file mode 100644 index 0000000..4473c44 --- /dev/null +++ b/app/common/controller/VehColorController.php @@ -0,0 +1,56 @@ +service = new VehColorService(); + } + + /** + * 列表页数据格式化 + * + * @param array $data + * @return array + */ + protected function indexFormat($data) + { + if (isset($data['created_at'])) + $data['created_at'] = date('Y-m-d H:i:s', $data['created_at']); + if (isset($data['updated_at'])) + $data['updated_at'] = date('Y-m-d H:i:s', $data['updated_at']); + if (isset($data['created_by']) && $data['created_by']) + $data['created_by_name'] = User::where('id', $data['created_by'])->value('name'); + if (isset($data['updated_by']) && $data['updated_by']) + $data['updated_by_name'] = User::where('id', $data['updated_by'])->value('name'); + return $data; + } + + /** + * 数据格式化 + * + * @param array $data + * @return array + */ + protected function showFormat($data) + { + if (isset($data['created_at'])) + $data['created_at'] = date('Y-m-d H:i:s', $data['created_at']); + if (isset($data['updated_at'])) + $data['updated_at'] = date('Y-m-d H:i:s', $data['updated_at']); + if (isset($data['created_by']) && $data['created_by']) + $data['created_by_name'] = User::where('id', $data['created_by'])->value('name'); + if (isset($data['updated_by']) && $data['updated_by']) + $data['updated_by_name'] = User::where('id', $data['updated_by'])->value('name'); + return $data; + } +} \ No newline at end of file diff --git a/app/common/controller/VehDictI18nController.php b/app/common/controller/VehDictI18nController.php new file mode 100644 index 0000000..6a87c9f --- /dev/null +++ b/app/common/controller/VehDictI18nController.php @@ -0,0 +1,33 @@ +service = new VehDictI18nService(); + } + + /** + * 列表页数据格式化 + * + * @param array $data + * @return array + */ + // protected function indexFormat($data) + // { + // if (isset($data['created_at'])) + // $data['created_at'] = date('Y-m-d H:i:s', $data['created_at']); + // if (isset($data['updated_at'])) + // $data['updated_at'] = date('Y-m-d H:i:s', $data['updated_at']); + // return $data; + // } +} \ No newline at end of file diff --git a/app/common/controller/VehDictMappingController.php b/app/common/controller/VehDictMappingController.php new file mode 100644 index 0000000..d1a1dfe --- /dev/null +++ b/app/common/controller/VehDictMappingController.php @@ -0,0 +1,33 @@ +service = new VehDictMappingService(); + } + + /** + * 列表页数据格式化 + * + * @param array $data + * @return array + */ + // protected function indexFormat($data) + // { + // if (isset($data['created_at'])) + // $data['created_at'] = date('Y-m-d H:i:s', $data['created_at']); + // if (isset($data['updated_at'])) + // $data['updated_at'] = date('Y-m-d H:i:s', $data['updated_at']); + // return $data; + // } +} \ No newline at end of file diff --git a/app/common/controller/VehModificationController.php b/app/common/controller/VehModificationController.php new file mode 100644 index 0000000..314ca40 --- /dev/null +++ b/app/common/controller/VehModificationController.php @@ -0,0 +1,47 @@ +service = new VehModificationService(); + } + + /** + * 列表页数据格式化 + * + * @param array $data + * @return array + */ + protected function indexFormat($data) + { + if (isset($data['created_at'])) + $data['created_at'] = date('Y-m-d H:i:s', $data['created_at']); + if (isset($data['updated_at'])) + $data['updated_at'] = date('Y-m-d H:i:s', $data['updated_at']); + return $data; + } + + /** + * 数据格式化 + * + * @param array $data + * @return array + */ + protected function showFormat($data) + { + if (isset($data['created_at'])) + $data['created_at'] = date('Y-m-d H:i:s', $data['created_at']); + if (isset($data['updated_at'])) + $data['updated_at'] = date('Y-m-d H:i:s', $data['updated_at']); + return $data; + } +} \ No newline at end of file diff --git a/app/common/controller/VehMoneyValueController.php b/app/common/controller/VehMoneyValueController.php new file mode 100644 index 0000000..b96d3f9 --- /dev/null +++ b/app/common/controller/VehMoneyValueController.php @@ -0,0 +1,33 @@ +service = new VehMoneyValueService(); + } + + /** + * 列表页数据格式化 + * + * @param array $data + * @return array + */ + // protected function indexFormat($data) + // { + // if (isset($data['created_at'])) + // $data['created_at'] = date('Y-m-d H:i:s', $data['created_at']); + // if (isset($data['updated_at'])) + // $data['updated_at'] = date('Y-m-d H:i:s', $data['updated_at']); + // return $data; + // } +} \ No newline at end of file diff --git a/app/common/controller/VehVehicleListingController.php b/app/common/controller/VehVehicleListingController.php new file mode 100644 index 0000000..59d49aa --- /dev/null +++ b/app/common/controller/VehVehicleListingController.php @@ -0,0 +1,33 @@ +service = new VehVehicleListingService(); + } + + /** + * 列表页数据格式化 + * + * @param array $data + * @return array + */ + // protected function indexFormat($data) + // { + // if (isset($data['created_at'])) + // $data['created_at'] = date('Y-m-d H:i:s', $data['created_at']); + // if (isset($data['updated_at'])) + // $data['updated_at'] = date('Y-m-d H:i:s', $data['updated_at']); + // return $data; + // } +} \ No newline at end of file diff --git a/app/common/controller/VehVehicleSkuController.php b/app/common/controller/VehVehicleSkuController.php new file mode 100644 index 0000000..3272f28 --- /dev/null +++ b/app/common/controller/VehVehicleSkuController.php @@ -0,0 +1,204 @@ +service = new VehVehicleSkuService(); + } + + /** + * 列表页数据格式化 + * + * @param array $data + * @return array + */ + protected function indexFormat($data) + { + if (isset($data['created_at'])) + $data['created_at'] = date('Y-m-d H:i:s', $data['created_at']); + if (isset($data['updated_at'])) + $data['updated_at'] = date('Y-m-d H:i:s', $data['updated_at']); + return $data; + } + + protected function showFormat($data) + { + if (isset($data['created_at'])) + $data['created_at'] = date('Y-m-d H:i:s', $data['created_at']); + if (isset($data['updated_at'])) + $data['updated_at'] = date('Y-m-d H:i:s', $data['updated_at']); + return $data; + } + + + public function store(Request $request) + { + try { + $data = $this->validate($this->service->getValidator(), $request->post(), 'store'); + $res = $this->service->serviceStore($request, $data); + return $this->success($res, '创建成功'); + } catch (\PDOException $e) { + $errorCode = $e->getCode(); + $errorMessage = $e->getMessage(); + if ($errorCode == '23000') { + if (strpos($errorMessage, "Duplicate entry") !== false && strpos($errorMessage, "uk_fingerprint") !== false) { + return $this->error("该车辆指纹已存在,请勿重复提交"); + } + // 其他类型的完整性约束错误 + throw new BusinessException("数据完整性错误", 0, $e); + } + // 重新抛出其他类型的异常 + throw $e; + } catch (\Throwable $e) { + return $this->error($e->getMessage()); + } + } + + public function update(Request $request) + { + try { + $data = $this->validate($this->service->getValidator(), $request->post(), 'update'); + $res = $this->service->serviceUpdate($request, $data); + return $this->success([], '更新成功'); + } catch (\PDOException $e) { + $errorCode = $e->getCode(); + $errorMessage = $e->getMessage(); + if ($errorCode == '23000') { + if (strpos($errorMessage, "Duplicate entry") !== false && strpos($errorMessage, "uk_fingerprint") !== false) { + return $this->error("该车辆指纹已存在,请勿重复提交"); + } + // 其他类型的完整性约束错误 + throw new BusinessException("数据完整性错误", 0, $e); + } + // 重新抛出其他类型的异常 + throw $e; + } catch (\Throwable $e) { + return $this->error($e->getMessage()); + } + } + + + public function fields_desc(Request $request) + { + $data = [ + 'sku_code' , // 'SKU编码,系统生成', + 'fingerprint' , // '规格指纹(sha256)去重', + 'brand_code' , // '品牌代码,如 BYD、HYUNDAI、KIA', + 'brand_name' , // '品牌名称展示,如 比亚迪、现代、起亚', + 'series_code' , // '车系代码,如 ELANTRA、SONGPLUS', + 'series_name' , // '车系名称展示', + 'model_code' , // '车型/配置代码(工厂/内部编码)', + 'model_name' , // '车型/配置名称(可读,如 伊兰特 1.5L、海狮07)', + 'model_year' , // '年款(YYYY)', + 'trim_code' , // '版型代码(TRIM)', + 'trim_level_code' , // '配置等级代码(TRIM_LEVEL)', + 'edition_code' , // '特别版代码(EDITION)', + 'trim' , // '版型/细分配置(如 TOP旗舰版、荣耀版)', + 'trim_level' , // '配置等级(豪华型/旗舰型等,便于筛选)', + 'edition_name' , // '特别版/纪念版(如 荣耀版、致敬版等)', + 'body_type_code' , // '车身类型:SEDAN/SUV/HB/MPV/Pickup', + 'drive_type_code' , // '驱动:FWD/RWD/AWD/4WD', + 'motor_layout_code' , // '电驱/电机布置(MOTOR_LAYOUT):FWD/RWD/AWD/DM-i/DM-p 等', + 'fuel_type_code' , // '能源:GAS/HEV/PHEV/EV/EREV', + 'engine_disp_l' , // '排量(L),如 1.5;纯电为空', + 'engine_turbo' , // '0' COMMENT '是否涡轮(0/1)', + 'engine_power_kw' , // '最大功率(kW)', + 'battery_kwh' , // '电池容量(kWh)', + 'transmission_code' , // '变速箱:MT/AT/CVT/DCT/1SPEED', + 'gears' , // '挡位数(EV多为1)', + 'range_km' , // '续航里程(km)', + 'range_std_code' , // '续航标准:CLTC/WLTP/EPA/NEDC', + 'doors' , // '门数', + 'seats' , // '座位数', + 'weight_kg' , // '整备质量(kg)', + 'dimensions_mm' , // '尺寸(mm):{length,width,height,wheelbase}', + 'color_exterior_code' , // '外观色代码', + 'color_exterior_text' , // '外观色名称', + 'color_interior_code' , // '内饰色代码', + 'color_interior_text' , // '内饰色名称', + 'options_official' , // '官方选装(结构化 JSON)', + 'features' , // '功能/安全/智能配置(结构化 JSON)', + 'emission_std_code' , // '排放标准:CHN6/EURO6/EPA', + 'origin_country' , // '原产国 ISO-3166-1 alpha-2', + 'currency' , // 'CNY' COMMENT '币种 ISO-4217', + 'msrp' , // '建议零售价/参考价', + 'is_active' , // '1' COMMENT '启用(1)/禁用(0)', + 'corp_id' , // '0' COMMENT '租户/公司(全局库可写0)', + 'remark' , // '备注', + 'created_by' , // '0', + 'updated_by' , // '0', + ]; + + + $data = [ + 'fingerprint' , // '规格指纹(sha256)去重', + 'brand_name' , // '品牌名称展示,如 比亚迪、现代、起亚', + 'series_name' , // '车系名称展示', + 'model_name' , // '车型/配置名称(可读,如 伊兰特 1.5L、海狮07)', + 'model_year' , // '年款(YYYY)', + 'trim' , // '版型/细分配置(如 TOP旗舰版、荣耀版)', + 'trim_level' , // '配置等级(豪华型/旗舰型等,便于筛选)', + 'edition_name' , // '特别版/纪念版(如 荣耀版、致敬版等)', + 'engine_disp_l' , // '排量(L),如 1.5;纯电为空', + 'engine_turbo' , // '0' COMMENT '是否涡轮(0/1)', + 'engine_power_kw' , // '最大功率(kW)', + 'battery_kwh' , // '电池容量(kWh)', + 'gears' , // '挡位数(EV多为1)', + 'range_km' , // '续航里程(km)', + 'doors' , // '门数', + 'seats' , // '座位数', + 'weight_kg' , // '整备质量(kg)', + 'dimensions_mm' , // '尺寸(mm):{length,width,height,wheelbase}', + 'color_exterior_text' , // '外观色名称', + 'color_interior_text' , // '内饰色名称', + 'options_official' , // '官方选装(结构化 JSON)', + 'features' , // '功能/安全/智能配置(结构化 JSON)', + 'origin_country' , // '原产国 ISO-3166-1 alpha-2', + 'currency' , // 'CNY' COMMENT '币种 ISO-4217', + 'msrp' , // '建议零售价/参考价', + 'is_active' , // '1' COMMENT '启用(1)/禁用(0)', + 'corp_id' , // '0' COMMENT '租户/公司(全局库可写0)', + 'remark' , // '备注', + 'created_by' , // '0', + 'updated_by' , // '0', + + 'body_type', // '车身类型' + 'drive_type', // '驱动:FWD/RWD/AWD/4WD', + 'motor_layout', // '电机布置(MOTOR_LAYOUT):FWD/RWD/AWD/DM-i/DM-p 等' + 'fuel_type', // '能源:GAS/HEV/PHEV/EV/EREV' + 'transmission', // '变速箱:MT/AT/CVT/DCT/1SPEED' + 'range_std', // '续航标准:CLTC/WLTP/EPA/NEDC' + 'emission_std', // '排放标准:CHN6/EURO6/EPA' + + 'sku_code' , // 'SKU编码,系统生成', + 'brand_code' , // '品牌代码,如 BYD、HYUNDAI、KIA', + 'series_code' , // '车系代码,如 ELANTRA、SONGPLUS', + 'model_code' , // '车型/配置代码(工厂/内部编码)', + 'trim_code' , // '版型代码(TRIM)', + 'trim_level_code' , // '配置等级代码(TRIM_LEVEL)', + 'edition_code' , // '特别版代码(EDITION)', + 'color_exterior_code' , // '外观色代码', + 'color_interior_code' , // '内饰色代码', + + 'body_type_code' , // '车身类型:SEDAN/SUV/HB/MPV/Pickup', + 'drive_type_code' , // '驱动:FWD/RWD/AWD/4WD', + 'motor_layout_code' , // '电驱/电机布置(MOTOR_LAYOUT):FWD/RWD/AWD/DM-i/DM-p 等', + 'fuel_type_code' , // '能源:GAS/HEV/PHEV/EV/EREV', + 'transmission_code' , // '变速箱:MT/AT/CVT/DCT/1SPEED', + 'range_std_code' , // '续航标准:CLTC/WLTP/EPA/NEDC', + 'emission_std_code' , // '排放标准:CHN6/EURO6/EPA', + ]; + } +} \ No newline at end of file diff --git a/app/common/controller/VehVehicleUnitController.php b/app/common/controller/VehVehicleUnitController.php new file mode 100644 index 0000000..3d2e7e2 --- /dev/null +++ b/app/common/controller/VehVehicleUnitController.php @@ -0,0 +1,33 @@ +service = new VehVehicleUnitService(); + } + + /** + * 列表页数据格式化 + * + * @param array $data + * @return array + */ + // protected function indexFormat($data) + // { + // if (isset($data['created_at'])) + // $data['created_at'] = date('Y-m-d H:i:s', $data['created_at']); + // if (isset($data['updated_at'])) + // $data['updated_at'] = date('Y-m-d H:i:s', $data['updated_at']); + // return $data; + // } +} \ No newline at end of file diff --git a/app/common/logic/ApprovalFlowsLogic.php b/app/common/logic/ApprovalFlowsLogic.php new file mode 100644 index 0000000..2157d28 --- /dev/null +++ b/app/common/logic/ApprovalFlowsLogic.php @@ -0,0 +1,17 @@ +orderBy('hot','desc')->orderBy('sort','desc')->get(['id', 'brand_code', 'brand_name', 'logo', 'description']); + } + +} \ No newline at end of file diff --git a/app/common/logic/BrandSeriesLogic.php b/app/common/logic/BrandSeriesLogic.php new file mode 100644 index 0000000..956ca55 --- /dev/null +++ b/app/common/logic/BrandSeriesLogic.php @@ -0,0 +1,22 @@ +where($where)->orderBy('sort','desc')->get(['id', 'series_code', 'series_name', 'description']); + } + +} \ No newline at end of file diff --git a/app/common/logic/Fingerprint.php b/app/common/logic/Fingerprint.php new file mode 100644 index 0000000..a066ada --- /dev/null +++ b/app/common/logic/Fingerprint.php @@ -0,0 +1,262 @@ +['msrp']]); // 可选排除 + */ +class Fingerprint +{ + /** 你的 veh_vehicle_sku 所有列(保持与表结构一致,顺序无关但建议完整) */ + public const COLS = [ + 'id','sku_code','fingerprint', + 'brand_code','brand_name','series_code','series_name', + 'model_code','model_name','model_year', + 'trim_code','trim_level_code','edition_code', + 'trim','trim_level','edition_name', + 'body_type_code','drive_type_code','motor_layout_code','fuel_type_code', + 'engine_disp_l','engine_turbo','engine_power_kw','battery_kwh', + 'transmission_code','gears','range_km','range_std_code', + 'doors','seats','weight_kg','dimensions_mm', + 'color_exterior_code','color_exterior_text', + 'color_interior_code','color_interior_text', + 'options_official','features','emission_std_code', + 'origin_country','currency','msrp', + 'is_active','corp_id','remark', + 'created_by','updated_by','created_at','updated_at', + ]; + + /** 默认剔除:技术/循环依赖字段 */ + public const EXCLUDE_DEFAULT = [ + 'id','sku_code','fingerprint', 'dimensions_mm', 'options_official', 'features', + 'origin_country','currency','msrp', + 'is_active','corp_id','remark', + 'created_by','updated_by','created_at','updated_at' + ]; + + /** 识别为“code类”的字段模式(统一大写) */ + private const CODE_FIELD_REGEX = '/(_code|_country|^currency)$/i'; + + /** 识别为 JSON 字段 */ + private const JSON_FIELDS = ['dimensions_mm','options_official','features']; + + /** + * 从 Eloquent 模型生成 + */ + public static function makeFromModel($model, array $opts = []): string + { + return self::makeFromArray($model->toArray(), $opts); + } + + /** + * 从数组生成指纹 + * @param array $row 一般为 $_POST 合并后的业务数据或 $model->toArray() + * @param array $opts ['exclude'=>[], 'extra'=>[], 'salt'=>string] + * - exclude: 额外排除的字段(默认仅排除 id,fingerprint) + * - extra : 额外拼入的键值(例如上下文、租户策略),会参与计算 + * - salt : 额外加盐字符串 + */ + public static function makeFromArray(array $row, array $opts = []): string + { + $exclude = array_values(array_unique(array_merge(self::EXCLUDE_DEFAULT, $opts['exclude'] ?? []))); + $extra = $opts['extra'] ?? []; + $salt = $opts['salt'] ?? ''; + + // 1) 统一抽取“表中定义的字段” + extra,缺失字段也要纳入(值按 NULL 处理) + $data = []; + foreach (self::COLS as $col) { + if (in_array($col, $exclude, true)) continue; + $data[$col] = array_key_exists($col, $row) ? $row[$col] : null; + } + // extra 也参与 + foreach ($extra as $k => $v) { + if (!is_string($k) || $k === '') continue; + $data['__extra__'.$k] = $v; + } + + // 2) 规范化(按字段特征) + $canon = []; + foreach ($data as $k => $v) { + $canon[$k] = self::normalize($k, $v); + } + + // 3) 键名排序,确保顺序无关 + ksort($canon, SORT_STRING); + + // 4) 拼成稳定串: key=value | key=value... + $parts = []; + foreach ($canon as $k => $v) { + // 为空也必须占位,避免 key 消失影响其他键的意义 + $parts[] = $k.'='.$v; + } + if ($salt !== '') { + $parts[] = '__salt__='.$salt; + } + $plain = implode('|', $parts); + + // 5) sha256 + return hash('sha256', $plain); + } + + /** + * 规范化单个字段 + * - NULL -> 特殊占位 "∅"(避免与空串混淆) + * - 布尔/整数/浮点 -> 统一数字字符串 + * - code类字段 -> 去首尾空白 & 转大写 + * - 普通字符串 -> trim & 压缩连续空白为单空格 + * - JSON字段 -> 解析后做“键排序 + 值规范化 + 索引数组可排序”,再无空白编码 + */ + private static function normalize(string $field, $value): string + { + if ($value === null) return '∅'; + + // 数值类型 + if (is_int($value)) return (string)$value; + if (is_bool($value)) return $value ? '1' : '0'; + + if (is_float($value) || (is_string($value) && is_numeric($value) && self::looksLikeNumberField($field))) { + // 统一浮点格式,最多 6 位小数并去尾零 + $num = (float)$value; + $s = number_format($num, 6, '.', ''); + $s = rtrim(rtrim($s, '0'), '.'); + return $s === '' ? '0' : $s; + } + + // JSON 字段 + if (in_array($field, self::JSON_FIELDS, true)) { + return self::normalizeJson($value); + } + + // 字符串 + if (is_string($value)) { + $s = trim($value); + // code类:统一大写 + if (preg_match(self::CODE_FIELD_REGEX, $field)) { + $s = mb_strtoupper($s, 'UTF-8'); + } else { + // 普通文本:压缩连续空白 + $s = preg_replace('/\s+/u', ' ', $s); + } + return $s === '' ? '' : $s; + } + + // 数组(极少出现,按 JSON 处理) + if (is_array($value)) { + return self::normalizeJson($value); + } + + // 其他类型(对象等):转 JSON 后处理 + return self::normalizeJson($value); + } + + /** 判断这个字段是否“看起来像数值字段” */ + private static function looksLikeNumberField(string $field): bool + { + static $numKeys = [ + 'engine_disp_l','engine_power_kw','battery_kwh','gears', + 'range_km','doors','seats','weight_kg','msrp','model_year', + 'created_at','updated_at','corp_id','is_active' + ]; + return in_array($field, $numKeys, true); + } + + /** + * JSON 规范化: + * - 字符串:若能 json_decode 则按 JSON;否则当普通字符串再规范化 + * - 对象:键名排序;值递归规范化 + * - 索引数组:元素递归规范化后,按字符串值排序(视为集合,避免顺序抖动) + * - 最终 json_encode(JSON_UNESCAPED_UNICODE|UNESCAPED_SLASHES) + */ + private static function normalizeJson($val): string + { + // 已是数组/对象 + if (is_array($val)) { + $normalized = self::canonArray($val); + return json_encode($normalized, JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES); + } + + // 字符串尝试解析 + if (is_string($val)) { + $val = trim($val); + if ($val === '') return '""'; + $decoded = json_decode($val, true); + if (json_last_error() === JSON_ERROR_NONE) { + $normalized = self::canonArray($decoded); + return json_encode($normalized, JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES); + } + // 非 JSON 字符串,按普通字符串规范化 + $s = preg_replace('/\s+/u', ' ', $val); + return json_encode($s, JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES); + } + + // 其他类型:直接编码 + return json_encode($val, JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES); + } + + /** 递归规范化数组/对象 */ + private static function canonArray($arr) + { + if (!is_array($arr)) return $arr; + + // 关联数组(对象) + if (self::isAssoc($arr)) { + $out = []; + ksort($arr, SORT_STRING); + foreach ($arr as $k => $v) { + $out[(string)$k] = self::canonValue($v); + } + return $out; + } + + // 索引数组(当集合处理:元素字符串化后排序) + $tmp = []; + foreach ($arr as $v) { + $tmp[] = self::canonValue($v); + } + // 将元素转为字符串再排序,确保稳定 + usort($tmp, function($a, $b) { + $sa = is_scalar($a) ? (string)$a : json_encode($a, JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES); + $sb = is_scalar($b) ? (string)$b : json_encode($b, JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES); + return strcmp($sa, $sb); + }); + return $tmp; + } + + private static function canonValue($v) + { + if (is_array($v)) return self::canonArray($v); + if (is_bool($v)) return $v ? 1 : 0; + if (is_int($v)) return $v; + if (is_float($v)) { + $s = number_format($v, 6, '.', ''); + $s = rtrim(rtrim($s, '0'), '.'); + return $s === '' ? '0' : $s; + } + if (is_string($v)) { + $s = trim($v); + $s = preg_replace('/\s+/u', ' ', $s); + return $s; + } + return $v; + } + + private static function isAssoc(array $arr): bool + { + // 有任意一个键不是 0..n 的递增整数,就视为关联数组 + $i = 0; + foreach ($arr as $k => $_) { + if ($k !== $i++) return true; + } + return false; + } +} \ No newline at end of file diff --git a/app/common/logic/ModelNaming.php b/app/common/logic/ModelNaming.php new file mode 100644 index 0000000..b50a4cf --- /dev/null +++ b/app/common/logic/ModelNaming.php @@ -0,0 +1,101 @@ + 15L + if (!empty($s['engine_turbo']) && (int)$s['engine_turbo'] === 1) $key[] = 'T'; + if (!empty($s['transmission_code'])) $key[] = self::tok($s['transmission_code']); + break; + + case 'HEV': + case 'PHEV': + case 'EREV': + $key[] = $fuel; + if (!empty($s['engine_disp_l'])) $key[] = self::dispToken($s['engine_disp_l']); + if (!empty($s['motor_layout_code'])) $key[] = self::tok($s['motor_layout_code']); // 如 DM-I/DM-P/AWD + break; + + default: + $key[] = $fuel; + } + + if (!empty($s['drive_type_code'])) $key[] = self::tok($s['drive_type_code']); + + $parts[] = implode('_', array_filter($key)); + return strtoupper(implode('_', array_filter($parts))); + } + + /** 生成中文可读的 model_name(走快照中文,不依赖 i18n) */ + public static function buildModelName(array $s): string + { + $seriesName = trim((string)($s['series_name'] ?? $s['series_code'] ?? '')); + $yearPart = !empty($s['model_year']) ? ($s['model_year'] . '款 ') : ''; + + $fuel = strtoupper($s['fuel_type_code'] ?? ''); + $tail = ''; + + if ($fuel === 'EV') { + $range = !empty($s['range_km']) ? (int)$s['range_km'] . 'KM ' : ''; + $drive = trim((string)($s['drive_type'] ?? $s['drive_type_code'] ?? '')); + $tail = "EV {$range}{$drive}"; + } elseif ($fuel === 'GAS') { + $disp = (!empty($s['engine_disp_l'])) + ? rtrim(rtrim(number_format((float)$s['engine_disp_l'], 1, '.', ''), '0'), '.') . 'L ' + : ''; + $trans = trim((string)($s['transmission'] ?? $s['transmission_code'] ?? '')); + $tail = "{$disp}{$trans}"; + } else { + $tail = trim((string)($s['fuel_type'] ?? $fuel)); + } + + $extras = []; + foreach (['trim','trim_level','edition_name'] as $k) { + if (!empty($s[$k])) $extras[] = trim((string)$s[$k]); + } + $extraStr = $extras ? ' ' . implode(' ', $extras) : ''; + + return trim($seriesName . ' ' . $yearPart . $tail . $extraStr); + } + + /* ===== Helpers ===== */ + + private static function tok($v): string + { + $v = strtoupper((string)$v); + return preg_replace('/[^A-Z0-9_]/', '', $v ?? ''); + } + + private static function numToken($v): string + { + return preg_replace('/[^0-9]/', '', (string)$v); + } + + /** 1.5L -> 15L;2.0L -> 20L(便于 code 稳定检索) */ + private static function dispToken($l): string + { + $x = (float)$l; + return (string)round($x * 10) . 'L'; + } +} \ No newline at end of file diff --git a/app/common/logic/SkuCodeBuilder.php b/app/common/logic/SkuCodeBuilder.php new file mode 100644 index 0000000..823b8dc --- /dev/null +++ b/app/common/logic/SkuCodeBuilder.php @@ -0,0 +1,233 @@ + 'FLG', + 'PREMIUM' => 'PRM', + 'STANDARD' => 'STD', + 'DELUXE' => 'DLX', + // edition + 'HONOR' => 'HNR', + 'CHAMPION' => 'CHP', + 'ANNIV' => 'ANV', + // trans + '1SPEED' => '1SP', + // drive/fuel/body 等一般已短,不必压缩 + ]; + + /** 入口:生成 sku_code */ + public static function build(array $s, array $opts = []): string + { + $maxLen = $opts['max_len'] ?? 40; + $ensureUnique = $opts['ensure_unique'] ?? true; + + $tokens = [ + 'brand' => self::tok($s['brand_code'] ?? ''), + 'series' => self::tok($s['series_code'] ?? ''), + 'year' => isset($s['model_year']) ? (string)intval($s['model_year']) : '', + 'fuel' => self::tok($s['fuel_type_code'] ?? ''), + 'drive' => self::tok($s['drive_type_code'] ?? ''), + 'spec' => self::buildSpec($s, self::tok($s['fuel_type_code'] ?? '')), + 'level' => self::abbr(self::tok($s['trim_level_code'] ?? '')), + 'edition' => self::abbr(self::tok($s['edition_code'] ?? '')), + ]; + + // 过滤空段并按既定顺序拼接 + $order = ['brand','series','year','fuel','drive','spec','level','edition']; + $parts = []; + foreach ($order as $k) { + if (!empty($tokens[$k])) $parts[$k] = $tokens[$k]; + } + + $sku = self::normalizeSku(implode('-', $parts)); + + if (strlen($sku) > $maxLen) { + $sku = self::shrink($parts, $maxLen); // 传入“带键名的 parts” + } + + if ($ensureUnique) { + $sku = self::ensureUnique($sku, $maxLen); + } + return $sku; + } + + /** 构造规格位(SPEC),按能源类型差异化 */ + private static function buildSpec(array $s, string $fuel): string + { + $fuel = strtoupper($fuel); + if ($fuel === 'EV') { + if (!empty($s['range_km'])) { + $r = (int)$s['range_km']; + $r10 = (int)round($r / 10) * 10; // 到 10 位 + return (string)$r10; + } + if (!empty($s['battery_kwh'])) { + return self::numToken($s['battery_kwh']) . 'KWH'; + } + return ''; + } + + if ($fuel === 'GAS') { + $bits = []; + if (!empty($s['engine_disp_l'])) { + $bits[] = self::dispToken($s['engine_disp_l']); // 1.5L -> 15L + } + if (!empty($s['engine_turbo']) && intval($s['engine_turbo']) === 1) { + $bits[] = 'T'; + } + if (!empty($s['transmission_code'])) { + $bits[] = self::abbr(self::tok($s['transmission_code'])); + } + return implode('', $bits); // 如 15LTCVT + } + + // HEV/PHEV/EREV 等 + $bits = []; + if (!empty($s['engine_disp_l'])) { + $bits[] = self::dispToken($s['engine_disp_l']); // 可空 + } + if (!empty($s['motor_layout_code'])) { + $bits[] = self::tok($s['motor_layout_code']); // 如 AWD / DM-I / DM-P + } + return implode('', $bits); + } + + /** 冲突检测并追加短校验(全局唯一) */ + private static function ensureUnique(string $sku, int $maxLen): string + { + if (!VehVehicleSku::where('sku_code', $sku)->exists()) { + return $sku; + } + // 用 crc32 生成 5~6 位 base36 校验(稳定且短) + $crc = self::base36Crc($sku); + $suffix = '-X' . $crc; + + if (strlen($sku . $suffix) > $maxLen) { + $sku = substr($sku, 0, max(0, $maxLen - strlen($suffix))); + $sku = rtrim($sku, '-'); // 避免尾部 '-' + } + $sku .= $suffix; + + // 若极端情况下仍冲突,追加递增计数 + $i = 2; + $candidate = $sku; + while (VehVehicleSku::where('sku_code', $candidate)->exists()) { + $extra = '-'.$i; + $cut = max(0, $maxLen - strlen($extra)); + $candidate = substr($sku, 0, $cut) . $extra; + $i++; + } + return $candidate; + } + + /** 把 token 数组尽量压到 maxLen(逐步压缩策略) */ + private static function shrink(array $partsByKey, int $maxLen): string + { + // 1) 先对 series/level/edition 做激进压缩 + $p = $partsByKey; + foreach (['series','level','edition'] as $k) { + if (!empty($p[$k])) $p[$k] = self::heavyAbbr($p[$k]); + } + $sku = self::normalizeSku(implode('-', $p)); + if (strlen($sku) <= $maxLen) return $sku; + + // 2) 依次移除不太关键的信息:edition → level(若存在) + if (!empty($p['edition'])) { unset($p['edition']); } + $sku = self::normalizeSku(implode('-', $p)); + if (strlen($sku) <= $maxLen) return $sku; + + if (!empty($p['level'])) { unset($p['level']); } + $sku = self::normalizeSku(implode('-', $p)); + if (strlen($sku) <= $maxLen) return $sku; + + // 3) 压缩 spec(如去 KM/KWH,仅保留核心) + if (!empty($p['spec'])) { $p['spec'] = self::compactSpec($p['spec']); } + $sku = self::normalizeSku(implode('-', $p)); + if (strlen($sku) <= $maxLen) return $sku; + + // 4) 仍超长:从右向左截断次要段(spec/drive/year),保留前三段(brand/series/fuel)优先 + $dropOrder = ['spec','drive','year']; + $q = $p; + foreach ($dropOrder as $k) { + if (!empty($q[$k])) { unset($q[$k]); } + $sku = self::normalizeSku(implode('-', $q)); + if (strlen($sku) <= $maxLen) return $sku; + } + + // 5) 最后兜底:硬截到 maxLen,后续 ensureUnique 会追加 -X 校验并避免冲突 + return substr(self::normalizeSku(implode('-', $p)), 0, $maxLen); + } + + /* ===== Helpers ===== */ + + private static function tok($v): string + { + $v = strtoupper((string)$v); + return preg_replace('/[^A-Z0-9_]/', '', $v ?? ''); + } + + private static function abbr(string $t): string + { + return self::ABBR[$t] ?? $t; + } + + private static function heavyAbbr(string $t): string + { + // 更激进:去除元音,连字符化 + $t = str_replace(['STANDARD','FLAGSHIP','PREMIUM','DELUXE'], ['STD','FLG','PRM','DLX'], $t); + $t = preg_replace('/[AEIOU]/', '', $t); + return $t; + } + + private static function compactSpec(string $s): string + { + // 规则:去 KM/KWH 等单位,只保留数字与核心字母 + $s = strtoupper($s); + $s = str_replace(['KM','KWH'], '', $s); + return $s; + } + + private static function numToken($v): string + { + return preg_replace('/[^0-9]/', '', (string)$v); + } + + /** 1.5L -> 15L;2.0L -> 20L */ + private static function dispToken($l): string + { + $x = (float)$l; + return (string)round($x * 10) . 'L'; + } + + private static function base36Crc(string $s): string + { + $crc = sprintf('%u', crc32($s)); // 无符号 + $n = (int)$crc; + $chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ'; + $res = ''; + do { + $res = $chars[$n % 36] . $res; + $n = intdiv($n, 36); + } while ($n > 0); + return substr($res, -6); // 取后6位足够 + } + + private static function normalizeSku(string $s): string + { + // 连续的 '-' 合并,首尾修剪 + $s = preg_replace('/-+/', '-', $s); + $s = trim($s, '-'); + return $s; + } +} \ No newline at end of file diff --git a/app/common/logic/StockOrderAccountsLogic.php b/app/common/logic/StockOrderAccountsLogic.php new file mode 100644 index 0000000..b76c961 --- /dev/null +++ b/app/common/logic/StockOrderAccountsLogic.php @@ -0,0 +1,17 @@ +orderBy('sort','desc')->get(['id', 'account_name', 'currency_id', 'account_number', 'bank_name', 'bank_branch', 'description', 'short_desc']); + } + +} \ No newline at end of file diff --git a/app/common/logic/SysCurrencyLogic.php b/app/common/logic/SysCurrencyLogic.php new file mode 100644 index 0000000..4de7559 --- /dev/null +++ b/app/common/logic/SysCurrencyLogic.php @@ -0,0 +1,21 @@ +get(['id', 'country_id', 'currency_code', 'currency_name', 'currency_symbol', 'exchange_rate', 'float_percent_up', 'float_percent_down']); + } +} \ No newline at end of file diff --git a/app/common/logic/SysDictItemLogic.php b/app/common/logic/SysDictItemLogic.php new file mode 100644 index 0000000..d6efc72 --- /dev/null +++ b/app/common/logic/SysDictItemLogic.php @@ -0,0 +1,22 @@ +where($where)->orderBy('sort','desc')->get(['id', 'item_label', 'item_value', 'remark', 'dict_id']); + } + +} \ No newline at end of file diff --git a/app/common/logic/SysDictLogic.php b/app/common/logic/SysDictLogic.php new file mode 100644 index 0000000..6447ed3 --- /dev/null +++ b/app/common/logic/SysDictLogic.php @@ -0,0 +1,27 @@ +where('code', $code)->orderBy('sort','desc')->first(['id']); + } + + public function getList() + { + return SysDict::where('status', 1)->get(['id', 'dict_name', 'dict_code', 'description']); + } + +} \ No newline at end of file diff --git a/app/common/logic/SysPortLogic.php b/app/common/logic/SysPortLogic.php new file mode 100644 index 0000000..4de5cdf --- /dev/null +++ b/app/common/logic/SysPortLogic.php @@ -0,0 +1,22 @@ +where($where)->orderBy('sort','desc')->get(['id', 'port_code', 'port_name', 'port_name_en', 'country_id', 'description']); + } + +} \ No newline at end of file diff --git a/app/common/logic/UploadedFilesLogic.php b/app/common/logic/UploadedFilesLogic.php new file mode 100644 index 0000000..3490cf2 --- /dev/null +++ b/app/common/logic/UploadedFilesLogic.php @@ -0,0 +1,169 @@ + $this->modelClass, + 'where' => ['partition' => $partition], + 'order' => ['created_at' => 'desc', 'id' => 'desc'] + ]; + + return $this->logicList($config, $limit, $page); + } + + /** + * 根据分类获取文件列表 + * + * @param string $category + * @param int $limit + * @param int $page + * @return mixed + */ + public function getFilesByCategory(string $category, int $limit = 15, int $page = 1) + { + $config = [ + 'model' => $this->modelClass, + 'where' => ['category' => $category], + 'order' => ['created_at' => 'desc', 'id' => 'desc'] + ]; + + return $this->logicList($config, $limit, $page); + } + + /** + * 根据文件类型获取文件列表 + * + * @param string $fileType + * @param int $limit + * @param int $page + * @return mixed + */ + public function getFilesByType(string $fileType, int $limit = 15, int $page = 1) + { + $config = [ + 'model' => $this->modelClass, + 'where' => ['file_type' => $fileType], + 'order' => ['created_at' => 'desc', 'id' => 'desc'] + ]; + + return $this->logicList($config, $limit, $page); + } + + /** + * 搜索文件 + * + * @param string $keyword + * @param int $limit + * @param int $page + * @return mixed + */ + public function searchFiles(string $keyword, int $limit = 15, int $page = 1) + { + $config = [ + 'model' => $this->modelClass, + 'where' => [ + 'or' => [ + ['file_name', 'like', '%' . $keyword . '%'], + ['original_name', 'like', '%' . $keyword . '%'], + ['category', 'like', '%' . $keyword . '%'] + ] + ], + 'order' => ['created_at' => 'desc', 'id' => 'desc'] + ]; + + return $this->logicList($config, $limit, $page); + } + + /** + * 获取文件统计信息 + * + * @return array + */ + public function getFileStatistics(): array + { + $model = new $this->modelClass(); + + $totalFiles = $model->count(); + $totalSize = $model->sum('file_size'); + + $partitionStats = $model->selectRaw('partition, COUNT(*) as count, SUM(file_size) as size') + ->groupBy('partition') + ->get() + ->toArray(); + + $categoryStats = $model->selectRaw('category, COUNT(*) as count, SUM(file_size) as size') + ->groupBy('category') + ->get() + ->toArray(); + + $typeStats = $model->selectRaw('file_type, COUNT(*) as count, SUM(file_size) as size') + ->groupBy('file_type') + ->get() + ->toArray(); + + return [ + 'total_files' => $totalFiles, + 'total_size' => $totalSize, + 'partition_stats' => $partitionStats, + 'category_stats' => $categoryStats, + 'type_stats' => $typeStats + ]; + } + + /** + * 清理过期文件 + * + * @param int $days 保留天数 + * @return int 清理的文件数量 + */ + public function cleanExpiredFiles(int $days = 30): int + { + $expiredTime = time() - ($days * 24 * 60 * 60); + + $config = [ + 'model' => $this->modelClass, + 'where' => [ + ['created_at', '<', $expiredTime] + ] + ]; + + $files = $this->logicListAll($config); + $count = 0; + + foreach ($files as $file) { + // 删除物理文件 + $fullPath = public_path() . $file->file_path; + if (file_exists($fullPath)) { + unlink($fullPath); + } + + // 删除数据库记录 + $this->logicDeleteBy(['where' => ['id' => $file->id]]); + $count++; + } + + return $count; + } +} \ No newline at end of file diff --git a/app/common/logic/VehColorLogic.php b/app/common/logic/VehColorLogic.php new file mode 100644 index 0000000..7a36811 --- /dev/null +++ b/app/common/logic/VehColorLogic.php @@ -0,0 +1,22 @@ +where($where)->orderBy('sort','desc')->get(['id', 'color_code', 'color_text']); + } + +} \ No newline at end of file diff --git a/app/common/logic/VehDictI18nLogic.php b/app/common/logic/VehDictI18nLogic.php new file mode 100644 index 0000000..f25b50b --- /dev/null +++ b/app/common/logic/VehDictI18nLogic.php @@ -0,0 +1,17 @@ +orderBy('sort','desc')->get(['id', 'code', 'name', 'type', 'price']); + } + +} \ No newline at end of file diff --git a/app/common/logic/VehMoneyValueLogic.php b/app/common/logic/VehMoneyValueLogic.php new file mode 100644 index 0000000..5006eaa --- /dev/null +++ b/app/common/logic/VehMoneyValueLogic.php @@ -0,0 +1,17 @@ + 'array', + ]; +} \ No newline at end of file diff --git a/app/common/model/ApprovalInstances.php b/app/common/model/ApprovalInstances.php new file mode 100644 index 0000000..48fe707 --- /dev/null +++ b/app/common/model/ApprovalInstances.php @@ -0,0 +1,101 @@ + 'array', // 自动转换 JSON 字段为数组 + 'finished_at' => 'datetime', + 'deadline_at' => 'datetime', + 'created_at' => 'datetime', + 'updated_at' => 'datetime', + ]; + + public function records() + { + return $this->hasMany(ApprovalRecords::class, 'instance_id', 'id'); + } + + public function flows() + { + return $this->belongsTo(ApprovalFlows::class, 'flow_type', 'type'); + } + + public function initiator() + { + return $this->belongsTo(User::class, 'initiator_id', 'id'); + } + + public function bizInfo() + { + return $this->belongsTo(StockOrders::class, 'biz_id', 'id'); + } + + /** + * 获取当前步骤的所有审批记录(pending 状态) + */ + public function currentStepRecords() + { + return $this->approvalRecords() + ->where('step_index', $this->current_step) + ->where('status', 'pending'); + } +} \ No newline at end of file diff --git a/app/common/model/ApprovalRecords.php b/app/common/model/ApprovalRecords.php new file mode 100644 index 0000000..660f1a4 --- /dev/null +++ b/app/common/model/ApprovalRecords.php @@ -0,0 +1,78 @@ + 'datetime', + 'created_at' => 'datetime', + 'updated_at' => 'datetime', + ]; + + public function instance() + { + return $this->belongsTo(ApprovalInstances::class, 'instance_id', 'id'); + } + + + public function approver() + { + return $this->belongsTo(User::class, 'approver_id', 'id'); + } +} \ No newline at end of file diff --git a/app/common/model/Brand.php b/app/common/model/Brand.php new file mode 100644 index 0000000..22c4704 --- /dev/null +++ b/app/common/model/Brand.php @@ -0,0 +1,49 @@ +belongsTo(StockOrders::class, 'order_id', 'id'); + } +} \ No newline at end of file diff --git a/app/common/model/StockOrderItemOptions.php b/app/common/model/StockOrderItemOptions.php new file mode 100644 index 0000000..e6f8122 --- /dev/null +++ b/app/common/model/StockOrderItemOptions.php @@ -0,0 +1,58 @@ +belongsTo(StockOrderItems::class, 'order_item_id', 'id'); + } +} \ No newline at end of file diff --git a/app/common/model/StockOrderItems.php b/app/common/model/StockOrderItems.php new file mode 100644 index 0000000..f45c532 --- /dev/null +++ b/app/common/model/StockOrderItems.php @@ -0,0 +1,99 @@ +moneyLatest('unit_price')?->amount; } + public function getHandlingFeeAmountAttribute() { return $this->moneyLatest('handling_fee')?->amount; } + public function getFobTransportFeeAmountAttribute() { return $this->moneyLatest('fob_transport_fee')?->amount; } + public function getPortMiscFeeAmountAttribute() { return $this->moneyLatest('port_misc_fee')?->amount; } + public function getRetrofitFeeAmountAttribute() { return $this->moneyLatest('retrofit_fee')?->amount; } + public function getTotalPriceAmountAttribute() { return $this->moneyLatest('total_price')?->amount; } + + + /** + * 关联主订单 + */ + public function order() + { + return $this->belongsTo(StockOrders::class, 'order_id', 'id'); + } + + /** + * 关联订单项选项 + */ + public function options() + { + return $this->hasMany(StockOrderItemOptions::class, 'order_item_id', 'id'); + } + + + /** + * 关联货币价值信息(一个订单项可以有多个货币价值记录) + */ + public function moneyValues() + { + return $this->hasMany(VehMoneyValue::class, 'object_id', 'id') + ->where('object_scene', 'stock_order_items'); // 场景标识区分不同对象类型 + } +} \ No newline at end of file diff --git a/app/common/model/StockOrders.php b/app/common/model/StockOrders.php new file mode 100644 index 0000000..0b97fe4 --- /dev/null +++ b/app/common/model/StockOrders.php @@ -0,0 +1,98 @@ +hasMany(StockOrderItems::class, 'order_id', 'id'); + } + + /** + * 关联订单账户信息 + */ + public function accounts() + { + return $this->hasMany(StockOrderAccounts::class, 'order_id', 'id'); + } + + /** + * 关联货币价值信息(一个订单可以有多个货币价值记录) + */ + public function moneyValues() + { + return $this->hasMany(VehMoneyValue::class, 'object_id', 'id')->where('object_scene', 'stock_orders'); + } + + + public function salesperson() + { + return $this->hasOne(User::class, 'id', 'salesperson_id'); + } +} \ No newline at end of file diff --git a/app/common/model/SysAccount.php b/app/common/model/SysAccount.php new file mode 100644 index 0000000..38e48fa --- /dev/null +++ b/app/common/model/SysAccount.php @@ -0,0 +1,49 @@ +hasMany(SysDictItem::class, 'dict_id', 'id'); + } + + + public function category() + { + return $this->belongsTo(SysDictCategory::class, 'category_id', 'id'); + } + + /** + * 一个字典有多个字典项 + */ + public function items() + { + return $this->hasMany(SysDictItem::class, 'dict_id', 'id'); + } + + /** + * 通过 dict_code 快速获取所有 item_value + */ + public function scopeGetItemValuesByCode($query, $dictCode) + { + return $query->where('dict_code', $dictCode) + ->with('items') // 预加载关联项 + ->first()?->items->pluck('item_value'); // 返回 item_value 数组 + } + + /** + * 通过 dict_code 获取完整字典项(含 label + value) + */ + public function scopeGetItemsByCode($query, $dictCode) + { + return $query->where('dict_code', $dictCode) + ->with('items') + ->first()?->items; + } +} \ No newline at end of file diff --git a/app/common/model/SysDictCategory.php b/app/common/model/SysDictCategory.php new file mode 100644 index 0000000..cbca49a --- /dev/null +++ b/app/common/model/SysDictCategory.php @@ -0,0 +1,58 @@ +hasMany(SysDict::class, 'category_id', 'id') + ->where('status', 1) + ->whereNull('deleted_at') + ->orderBy('id', 'asc'); + } +} \ No newline at end of file diff --git a/app/common/model/SysDictItem.php b/app/common/model/SysDictItem.php new file mode 100644 index 0000000..8fbceb9 --- /dev/null +++ b/app/common/model/SysDictItem.php @@ -0,0 +1,67 @@ +belongsTo(SysDict::class, 'dict_id', 'id'); + } + + /** + * 字典项属于一个字典 + */ + public function dict() + { + return $this->belongsTo(SysDict::class, 'dict_id', 'id'); + } +} \ No newline at end of file diff --git a/app/common/model/SysDictItemTranslation.php b/app/common/model/SysDictItemTranslation.php new file mode 100644 index 0000000..8d33913 --- /dev/null +++ b/app/common/model/SysDictItemTranslation.php @@ -0,0 +1,49 @@ + 'integer', + 'enabled' => 'integer', + 'sort_order' => 'integer', + 'created_at' => 'integer', + 'updated_at' => 'integer', + ]; + + protected $fillable = [ + 'field','dict','required','enabled','sort_order', + 'remark','created_at','updated_at' + ]; +} \ No newline at end of file diff --git a/app/common/model/VehModification.php b/app/common/model/VehModification.php new file mode 100644 index 0000000..d3bd16e --- /dev/null +++ b/app/common/model/VehModification.php @@ -0,0 +1,49 @@ + 'string', + 'exchange_rate' => 'string', + ]; + + /** 按本行汇率换到结算币;无汇率返回 null */ + public function amountInSettlement(int $scale = 2): ?string + { + if ($this->exchange_rate === null) return null; + return (string) bcmul((string)$this->amount, (string)$this->exchange_rate, $scale); + } + + + /** + * 关联订单 + */ + public function order() + { + return $this->belongsTo(StockOrders::class, 'object_id', 'id'); + } + + /** + * 关联订单项(一个货币价值记录可能关联到订单项) + */ + public function orderItem() + { + return $this->belongsTo(StockOrderItems::class, 'object_id', 'id'); + } + + /** + * 限定查询特定对象类型的货币价值记录 + */ + public function scopeOfScene($query, $scene) + { + return $query->where('object_scene', $scene); + } +} \ No newline at end of file diff --git a/app/common/model/VehMoneyValue_bak.php b/app/common/model/VehMoneyValue_bak.php new file mode 100644 index 0000000..50de155 --- /dev/null +++ b/app/common/model/VehMoneyValue_bak.php @@ -0,0 +1,49 @@ + 'array', + 'features' => 'array', + 'dimensions_mm' => 'array', + 'model_year' => 'integer', + 'engine_turbo' => 'integer', + 'gears' => 'integer', + 'range_km' => 'integer', + 'doors' => 'integer', + 'seats' => 'integer', + 'weight_kg' => 'integer', + 'msrp' => 'float', + 'battery_kwh' => 'float', + 'engine_disp_l' => 'float', + 'is_active' => 'integer', + 'corp_id' => 'integer', + 'created_at' => 'integer', + 'updated_at' => 'integer', + ]; + + protected $fillable = [ + 'sku_code','fingerprint', + 'brand_code','brand_name', + 'series_code','series_name', + 'model_code','model_name','model_year', + 'trim_code','trim_level_code','edition_code', + 'trim','trim_level','edition_name', + 'body_type_code','drive_type_code','motor_layout_code', + 'fuel_type_code','engine_disp_l','engine_turbo','engine_power_kw', + 'battery_kwh','transmission_code','gears', + 'range_km','range_std_code', + 'doors','seats','weight_kg','dimensions_mm', + 'color_exterior_code','color_exterior_text', + 'color_interior_code','color_interior_text', + 'options_official','features','emission_std_code', + 'origin_country','currency','msrp', + 'is_active','corp_id','remark', + 'created_by','updated_by','created_at','updated_at', + 'body_type', 'drive_type', 'motor_layout', 'fuel_type', 'transmission', + 'range_std', 'emission_std' + ]; +} \ No newline at end of file diff --git a/app/common/model/VehVehicleUnit.php b/app/common/model/VehVehicleUnit.php new file mode 100644 index 0000000..28e38f1 --- /dev/null +++ b/app/common/model/VehVehicleUnit.php @@ -0,0 +1,49 @@ +validator; + } + + + protected function buildQueryConfig(Request $request, string $scene = 'index'): array + { + $p = $request->all(); + + $config = []; + $config['model'] = $this->modelClass; + switch ($scene) { + case 'index': + $config['order']['id'] = 'desc'; + break; + case 'show': + $config['where']['id'] = $p['id']; + break; + case 'store': + break; + case 'update': + $config['where']['id'] = $p['id']; + break; + case 'destroy': + $config['where']['id'] = $p['id']; + break; + } + + return $config; + } + + + +} \ No newline at end of file diff --git a/app/common/service/ApprovalInstancesService.php b/app/common/service/ApprovalInstancesService.php new file mode 100644 index 0000000..5096321 --- /dev/null +++ b/app/common/service/ApprovalInstancesService.php @@ -0,0 +1,31 @@ +validator; + } +} \ No newline at end of file diff --git a/app/common/service/ApprovalRecordsService.php b/app/common/service/ApprovalRecordsService.php new file mode 100644 index 0000000..b2af9a7 --- /dev/null +++ b/app/common/service/ApprovalRecordsService.php @@ -0,0 +1,118 @@ +validator; + } + + + protected function buildQueryConfig(Request $request, string $scene = 'default'): array + { + $p = $request->all(); + + $config = []; + $config['model'] = $this->modelClass; + switch ($scene) { + case 'index': + $config['order']['id'] = 'desc'; + + $config['with'] = [ + 'instance', + 'approver', + 'instance.flows', + 'instance.initiator', + 'instance.bizInfo' + ]; + + // $config['dataLimit']['type'] = 'relation'; + // $config['dataLimit']['userId'] = $request->user->uid; + // $config['dataLimit']['userField'] = 'approver_id'; + break; + case 'todo': + + if ($p['scene'] === 'todo') { + // if (!in_array(1, $request->user->role_ids)) { + $config['where']['approver_id'] = $request->user->uid; + // } + $config['where']['status'] = 'pending'; + $config['where']['instance.status'] = 'processing'; + } + + if ($p['scene'] === 'history') { + if (!in_array(1, $request->user->role_ids)) { + $config['where']['approver_id'] = $request->user->uid; + } + $config['where']['status'] = ['!=', 'pending']; + } + + $config['order']['id'] = 'desc'; + + $config['with'] = [ + 'instance', + 'approver', + 'instance.flows', + 'instance.initiator', + 'instance.bizInfo' + ]; + break; + case 'show': + $config['where']['id'] = $p['id']; + break; + case 'store': + break; + case 'update': + $config['where']['id'] = $p['id']; + break; + case 'destroy': + $config['where']['id'] = $p['id']; + break; + case 'logs': + $config['where']['instance_id'] = $p['instance_id']; + $config['order']['id'] = 'desc'; + break; + } + + return $config; + } + + + public function serviceApprove(Request $request) + { + $param = $request->all(); + $appro = new \app\common\utils\approval\service\ApprovalFlowService(); + return $appro->approve($param['instance_id'], $request->user->uid, $param['action'], $param['comment']); + } + + + public function serviceLogs(Request $request) + { + $config = $this->buildQueryConfig($request, 'logs'); + $item = $this->logicListAll($config); + return $this->postProcess('Logs', $item); + } +} \ No newline at end of file diff --git a/app/common/service/BrandSeriesService.php b/app/common/service/BrandSeriesService.php new file mode 100644 index 0000000..ae91723 --- /dev/null +++ b/app/common/service/BrandSeriesService.php @@ -0,0 +1,34 @@ +logic = (new BrandSeriesLogic()); + } + + /** + * 获取系统list + */ + public function getList($params) + { + if (empty($params['brand_id'])) + throw new BusinessException('品牌id不能为空'); + $where = []; + if(isset($params['brand_id']) && $params['brand_id']) + $where[] = ['brand_id', '=', $params['brand_id']]; + return $this->logic->getList($where); + } +} \ No newline at end of file diff --git a/app/common/service/BrandService.php b/app/common/service/BrandService.php new file mode 100644 index 0000000..c3430fe --- /dev/null +++ b/app/common/service/BrandService.php @@ -0,0 +1,24 @@ +logic = (new BrandLogic()); + } + + /** + * 获取系统list + */ + public function getList() + { + return $this->logic->getList(); + } +} \ No newline at end of file diff --git a/app/common/service/MessageNotificationsService.php b/app/common/service/MessageNotificationsService.php index 806bc1d..cebd4b9 100644 --- a/app/common/service/MessageNotificationsService.php +++ b/app/common/service/MessageNotificationsService.php @@ -86,7 +86,7 @@ class MessageNotificationsService return ['msg_id' => $msgId]; } - public function afterServiceList($data) + public function afterServiceIndex($data) { $data->each(function (&$item) { $temp = explode(',', $item->recipient); @@ -273,4 +273,14 @@ class MessageNotificationsService { return User::where('status', 1)->pluck('id')->toArray(); } + + + public function getUserEmailsById(array $con = []): array + { + $user = User::where('status', 1); + if (count($con) > 0) { + return $user->whereIn('id', $con)->where('email', '!=', '')->pluck('email')->toArray(); + } + return $user->where('email', '!=', '')->pluck('email')->toArray(); + } } \ No newline at end of file diff --git a/app/common/service/MessageUserStatusService.php b/app/common/service/MessageUserStatusService.php index 2162f86..ca324dc 100644 --- a/app/common/service/MessageUserStatusService.php +++ b/app/common/service/MessageUserStatusService.php @@ -97,7 +97,7 @@ class MessageUserStatusService } - protected function afterServiceList($res) + protected function afterServiceIndex($res) { $res->each(function (&$item) { if (isset($item->message)) { diff --git a/app/common/service/OssService.php b/app/common/service/OssService.php index 4d75b1f..72a956e 100644 --- a/app/common/service/OssService.php +++ b/app/common/service/OssService.php @@ -56,4 +56,24 @@ class OssService // 生成 OSS 私有 URL(有效期为 3600 秒) return $this->ossClient->signUrl($this->bucket, $objectKey, $expireTime); } + + /** + * 删除 OSS 上的文件 + * @param string $objectKey OSS上存储的对象名称 + * @return bool 删除是否成功 + */ + public function deleteFile($objectKey) + { + try { + // 删除 OSS 上的文件 + $this->ossClient->deleteObject($this->bucket, $objectKey); + return true; + } catch (OssException $e) { + // 错误处理 + throw new \RuntimeException('OSS Delete Error: ' . $e->getMessage()); + } catch (\Throwable $th) { + // 错误处理 + throw new BusinessException('Delete Error: ' . $th->getMessage()); + } + } } \ No newline at end of file diff --git a/app/common/service/ResourceService.php b/app/common/service/ResourceService.php index c1654a5..2d093f8 100644 --- a/app/common/service/ResourceService.php +++ b/app/common/service/ResourceService.php @@ -95,7 +95,7 @@ class ResourceService } - protected function afterServiceListAll($data) + protected function afterServiceDefault($data) { $data = Tree::buildMenuTreeWithReference($data->toArray()); return $data; diff --git a/app/common/service/StockOrderAccountsService.php b/app/common/service/StockOrderAccountsService.php new file mode 100644 index 0000000..cc455a7 --- /dev/null +++ b/app/common/service/StockOrderAccountsService.php @@ -0,0 +1,31 @@ +validator; + } +} \ No newline at end of file diff --git a/app/common/service/StockOrderItemOptionsService.php b/app/common/service/StockOrderItemOptionsService.php new file mode 100644 index 0000000..2e6e48e --- /dev/null +++ b/app/common/service/StockOrderItemOptionsService.php @@ -0,0 +1,31 @@ +validator; + } +} \ No newline at end of file diff --git a/app/common/service/StockOrderItemsService.php b/app/common/service/StockOrderItemsService.php new file mode 100644 index 0000000..9130bdc --- /dev/null +++ b/app/common/service/StockOrderItemsService.php @@ -0,0 +1,31 @@ +validator; + } +} \ No newline at end of file diff --git a/app/common/service/StockOrdersService.php b/app/common/service/StockOrdersService.php new file mode 100644 index 0000000..9308fde --- /dev/null +++ b/app/common/service/StockOrdersService.php @@ -0,0 +1,742 @@ +validator; + } + + + protected function buildQueryConfig(Request $request, string $scene = 'index'): array + { + $p = $request->all(); + + $config = []; + $config['model'] = $this->modelClass; + switch ($scene) { + case 'index': + + // ====== ====== 筛选 ====== ====== + // 业务id + if (isset($p['salesperson_id']) && $p['salesperson_id']) { + $config['where']['salesperson_id'] = $p['salesperson_id']; + } + // 初、终版 + $config['where']['is_final'] = $p['is_final'] ?? 0; + // 关键字 + if (isset($p['keyword']) && $p['keyword']) { + $k = '%' . $p['keyword'] . '%'; + foreach (['pi_no', 'order_no', 'customer_snapshot'] as $col) { + $config['orWhere'][] = [$col, 'like', $k]; + } + } + + $config['order']['id'] = 'desc'; + + // $config['where']['salesperson_id'] = $request->user->uid; + break; + case 'print': + case 'approval': + $config['where']['id'] = $p['id']; + break; + case 'detail': + case 'show': + $config['where']['id'] = $p['id']; + + $config['with'][] = 'items'; + $config['with'][] = 'items.options'; + $config['with'][] = 'items.moneyValues'; + $config['with'][] = 'accounts'; + $config['with'][] = 'moneyValues'; + $config['with'][] = 'salesperson'; + break; + case 'store': + break; + case 'update': + $config['where']['id'] = $p['id']; + break; + case 'destroy': + $config['where']['id'] = $p['id']; + break; + } + + return $config; + } + + public function beforeServiceStore($data, Request $request) + { + $userId = $request->user->uid; + + $data['order_no'] = generateStockNo(); + $data['biz_date'] = strtotime($data['biz_date']); + $data['eta_ship_date'] = strtotime($data['eta_ship_date']); + $data['latest_arrival_date'] = strtotime($data['latest_arrival_date']); + $data['salesperson_id'] = $userId; + $data['salesperson_snapshot'] = User::where('id', $userId)->value('name') ?? ''; + $data['approval_status'] = $data['approval_status'] == 1 ? 1 : 0; + + // if ($data['is_final'] == 1) { + $data['pick_up_date'] = $data['pick_up_date'] ? strtotime($data['pick_up_date']) : 0; + $data['collect_all_date'] = $data['collect_all_date'] ? strtotime($data['collect_all_date']) : 0; + // } + return $data; + } + + protected function afterServiceStore($res) + { + // 处理原单 + if ($res->is_final == 1 && $res->object_id > 0) { + $order = StockOrders::find($res->object_id); + $order->is_use = 1; + $order->object_id = $res->id; + if (!$order->save()) { + flg('备货单【'.$order->order_no.'】创建终版成功,但更新原单失败'); + } + } + // 启动审批 + if ($res->approval_status == 1) { + $this->startApproval($res); + } + return $res; + } + + /** + * 启动审批 + */ + public function startApproval($item) + { + // 启动审批 + $appro = new \app\common\utils\approval\service\ApprovalFlowService(); + $appro->start('stock_orders', 'stock_orders', $item->id, $item->toArray(), $item->salesperson_id); + // 异步生成pdf文件 + \Webman\RedisQueue\Redis::send('stock-order-pdf', ['id'=>$item->id]); + } + + /** + * 创建PDF文件 + */ + public function createdPdfFile($id) + { + $config = []; + $config['model'] = $this->modelClass; + $config['where']['id'] = $id; + $config['with'][] = 'items'; + $config['with'][] = 'items.options'; + $config['with'][] = 'items.moneyValues'; + $config['with'][] = 'accounts'; + $config['with'][] = 'moneyValues'; + $config['with'][] = 'salesperson'; + $item = $this->logicShow($config); + $pdfData = $this->pdfData($item); + + // 渲染模板为最终HTML + $tplPath = app_path('common/utils/pdf/template/stockOrder.stub'); + $html = \app\common\utils\pdf\service\PdfHelper::render($tplPath, ['data' => $pdfData]); + + // 生成PDF + $driver = new \app\common\utils\pdf\service\MpdfDriver(); + $pdfBin = $driver->htmlToPdf($html); + + // 存储 + $dir = public_path('pdf').'/'; + $fileName = ($pdfData['order_no'] ?? date('YmdHis')) . '.pdf'; + + // 本地保存 + \app\common\utils\pdf\service\PdfStorage::saveBinary( + $pdfBin, + $dir, + $fileName + ); + + // 上传云端 + $param = [ + 'path' => $dir . $fileName, + 'scene' => 'stock_orders', + 'category' => 'pdf', + 'original_name' => $fileName + ]; + $up = new \app\common\service\UploadService(); + $res = $up->uploadLocalFile($param); + + @unlink($dir . $fileName); + + // 更新文件地址 + $item->file_url = $res['file_name']; + $item->save(); + + return $res; + } + + public function pdfData($item) + { + $vehList = []; + $index = 0; + $totalProfit = 0; // 总利润 + + // $priceFields = [ + // 'official_price', // '官方指导价', + // 'discount_price', // '优惠', + // 'listing_price', // '二手挂牌价价', + // 'reduction_price', // '优惠', + + // 'handling_fee', // '手续费', + // 'retrofit_fee', // '改装费', + // 'fob_transport_fee', // 'FOB运输费', + // 'port_misc_fee', // '港杂', + // 'ocean_freight', // '海运费', + + // 'unit_price', // '单价', + // 'total_price', // '总价', + // // 'profit', // '利润' + // ]; + + foreach ($item->items as $k => $v) { + $currencyInfo = SysCurrency::where('currency_code', $item->settlement_currency)->first(['currency_symbol', 'currency_name'])->toArray(); + + // 存在则数量加1 + if (count($vehList) > 0 && isset($vehList[$v->unique_id])) { + $vehList[$v->unique_id]['num'] += 1; + $vehList[$v->unique_id]['total_price']['currency_info'] = $currencyInfo['currency_symbol'].number_format($vehList[$v->unique_id]['total_price']['amount']*$vehList[$v->unique_id]['num'], 2, '.', ','); + $vehList[$v->unique_id]['profit']['currency_info'] = $currencyInfo['currency_symbol'].number_format($vehList[$v->unique_id]['profit']['amount']*$vehList[$v->unique_id]['num'], 2, '.', ','); + continue; + } + + $index += 1; + + // 处理金额 (每次循环重新初始化) + $mv = []; + foreach ($v->moneyValues as $money) { + $mv[$money->field] = [ + 'currency' => $money->currency, + 'amount' => $money->amount, + 'rate' => $money->exchange_rate, + 'currency_info' => SysCurrency::where('currency_code', $money->currency)->value('currency_symbol').number_format($money->amount, 2, '.', ',') + ]; + } + + // 处理改装 (每次循环重新初始化) + $mfy = ''; + foreach ($v->options as $my) { + $mfy .= $my->option_name_snapshot.' '; + } + + // 计算单项利润并累加到总利润 + $itemProfit = $this->calculateProfit($mv, $item); + $totalProfit += $itemProfit; + + // 处理结果集 + $vehListItem = [ + 'index' => $index, + 'model_snapshot' => $v->model_snapshot, + 'color_interior_snapshot' => $v->color_interior_snapshot, + 'color_exterior_snapshot' => $v->color_exterior_snapshot, + 'num' => 1, + 'modify' => $mfy, + 'mileage' => $v->mileage + ]; + + $vehList[$v->unique_id] = $vehListItem + $mv; + } + + $mvs = []; + foreach ($item->moneyValues as $moneys) { + $mvs[$moneys->field] = [ + 'currency' => $moneys->currency, + 'amount' => $moneys->amount, + 'rate' => $moneys->exchange_rate, + 'currency_info' => SysCurrency::where('currency_code', $moneys->currency)->value('currency_symbol').number_format($moneys->amount, 2, '.', ','), + ]; + } + + $accList = []; + foreach ($item->accounts as $k => $v) { + $accList[] = [ + 'index' => '收款'.($k + 1), + 'remit_country_name_snapshot' => $v->remit_country_name_snapshot, + 'remit_date' => date('Y-m-d', $v->remit_date), + 'currency_name_snapshot' => $v->currency_name_snapshot, + 'account_name_snapshot' => $v->account_name_snapshot, + 'remitter_name' => $v->remitter_name, + 'amount' => $v->amount, + ]; + } + + + + $veh = [ + 'order_no' => $item->order_no, + 'salesperson_snapshot' => $item->salesperson_snapshot, + 'sign_img_url' => (!empty($item->salesperson->sign_img)) ? + (new \app\common\service\OssService)->generatePrivateUrl($item->salesperson->sign_img) : '', + 'created_at' => date('Y-m-d', strtotime($item->created_at)), + + 'pi_no' => $item->pi_no, + 'biz_date' => date('Y-m-d', $item->biz_date), + 'clearance_country_snapshot' => $item->clearance_country_snapshot, + 'customer_source_snapshot' => $item->customer_source_snapshot, + 'customer_snapshot' => $item->customer_snapshot, + 'customer_country_snapshot' => $item->customer_country_snapshot, + + // 订单项 + 'veh_list' => array_values($vehList), // 重新索引数组 + + 'contract_exchange_rate' => $item->contract_exchange_rate, + 'cif_total_amount' => $mvs['cif_total_amount'], + 'profit_total' => $mvs['profit_total'], + + 'declarant_name' => $item->declarant_name, + 'eta_ship_date' => date('Y-m-d', $item->eta_ship_date), + 'shipping_method_snapshot' => $item->shipping_method_snapshot, + 'domestic_port_snapshot' => $item->domestic_port_snapshot, + 'trade_term_snapshot' => $item->trade_term_snapshot, + 'clearance_port_snapshot' => $item->clearance_port_snapshot, + 'latest_arrival_date' => date('Y-m-d', $item->latest_arrival_date), + 'pick_up_date' => date('Y-m-d', $item->pick_up_date), + 'collect_all_date' => date('Y-m-d', $item->collect_all_date), + 'is_final' => $item->is_final, + + // 账户 + 'account_list' => $accList, + 'acc_index' => count($accList)+1, + 'currency_symbol' => $item->settlement_currency, + 'sign_imgs' => $this->queryAppro($item->id), + 'contract_url' => (!empty($item->contract)) ? (new \app\common\service\OssService)->generatePrivateUrl($item->contract) : '' + ]; + + return $veh; + } + + + public function queryAppro($bizId) + { + $instances = ApprovalInstances::where('biz_id', $bizId)->where('biz_type', 'stock_orders')->first(); + if (!$instances) { + return []; + } + + $records = ApprovalRecords::where('instance_id', $instances->id)->where('action', 'approve')->orderBy('step_index', 'asc')->pluck('approver_id')->toArray(); + if (count($records) == 0) { + return []; + } + + $signImgs = []; + foreach ($records as $k => $v) { + $signImg = User::where('id', $v)->value('sign_img'); + $signImgs[$k] = (!empty($signImg))?(new \app\common\service\OssService)->generatePrivateUrl($signImg):''; + } + + return $signImgs; + } + + /** + * 打印数据 + */ + public function servicePrint(Request $request) + { + $config = $this->buildQueryConfig($request, 'show'); + $item = $this->logicShow($config); + return $this->postProcess('Print', $item); + } + + public function serviceDetail(Request $request) + { + $config = $this->buildQueryConfig($request, 'detail'); + $item = $this->logicShow($config); + $pdfData = $this->pdfData($item); + return $this->postProcess('Detail', $pdfData); + } + + /** + * 审批 + */ + public function serviceApproval(Request $request) + { + $config = $this->buildQueryConfig($request, 'approval'); + $this->logicUpdateBy($config, [ + 'approval_status' => 1 + ]); + $item = $this->logicShow($config); + $this->startApproval($item); + return $this->postProcess('Approval', $item); + } + + /** + * 创建 + * + * @param Request $request + * @param array $data + * @return mixed + */ + public function serviceStore(Request $request, array $data = []) + { + // 关联表较多,增加事务处理 + return Db::transaction(function () use ($request, $data) { + $data = $this->preProcess('Store', $data, $request); + $dl = $this->getDataLimitFromRequest($request); + $created = $this->logicCreate($data, $dl); + + // 处理车辆和账户信息 + $this->processOrderItems($created, $data); + + return $this->postProcess('Store', $created); + }); + } + + protected function onlyFillableData(Model|string $model, array $data, array $extraWhitelist = []): array + { + // 兼容传类名或实例 + $m = \is_string($model) ? new $model : $model; + // 1) 优先使用 fillable + $keys = $m->getFillable(); + // 2) 若没配置 fillable(常见:用 guarded),回退到完整表字段,避免误伤 + if (empty($keys)) { + $keys = $m->getConnection() + ->getSchemaBuilder() + ->getColumnListing($m->getTable()); + } + // 3) 允许额外白名单(如虚拟字段、JSON映射字段) + if ($extraWhitelist) { + $keys = array_merge($keys, $extraWhitelist); + } + return array_intersect_key($data, array_flip($keys)); + } + + /** + * 更新前处理 + * + * @param array $data + * @param Request $request + * @return array + */ + public function beforeServiceUpdate($data, Request $request) + { + $data['biz_date'] = strtotime($data['biz_date']); + $data['eta_ship_date'] = strtotime($data['eta_ship_date']); + $data['latest_arrival_date'] = strtotime($data['latest_arrival_date']); + $data['approval_status'] = $data['approval_status'] == 1 ? 1 : 0; + + $data['pick_up_date'] = $data['pick_up_date'] ? strtotime($data['pick_up_date']) : 0; + $data['collect_all_date'] = $data['collect_all_date'] ? strtotime($data['collect_all_date']) : 0; + return $data; + } + + /** + * 更新后处理 + * + * @param mixed $res + * @return mixed + */ + protected function afterServiceUpdate($res) + { + // 启动审批 + if ($res->approval_status == 1) { + $this->startApproval($res); + } + return $res; + } + + /** + * 更新 + * + * @param Request $request + * @param array $data + * @return mixed + */ + public function serviceUpdate(Request $request, array $data = []) + { + // 关联表较多,增加事务处理 + return Db::transaction(function () use ($request, $data) { + $data = $this->preProcess('Update', $data, $request); + $config = $this->buildQueryConfig($request, 'update'); + // 过滤掉非库字段 + $dbData = $this->onlyFillableData($config['model'], $data); + // 更新主表 + $updated = $this->logicUpdateBy($config, $dbData); + // 获取更新后的数据 + $order = $this->logicShow($config); + // 处理车辆和账户信息 + $this->processOrderItems($order, $data, true); + return $this->postProcess('Update', $order); + }); + } + + /** + * 处理订单项(车辆和账户) + * @param StockOrders $order 订单对象 + * @param array $data 数据 + * @param bool $isUpdate 是否为更新操作 + */ + private function processOrderItems($order, array $data, bool $isUpdate = false): void + { + // 如果是更新操作,先删除原有的关联数据 + if ($isUpdate) { + $this->clearExistingOrderItems($order->id); + } + + // 处理车辆项 + $this->processVehicleItems($order, $data['veh_list'] ?? []); + + // 处理账户项 + $this->processAccountItems($order, $data['account_list'] ?? []); + } + + /** + * (更新时)清除已存在的订单项 + * @param int $orderId 订单ID + */ + private function clearExistingOrderItems(int $orderId): void + { + // 删除车辆改装项 + StockOrderItemOptions::whereHas('item', function ($query) use ($orderId) { + $query->where('order_id', $orderId); + })->delete(); + + // 删除车辆项及其相关金额记录 + $orderItems = StockOrderItems::where('order_id', $orderId)->get(); + foreach ($orderItems as $item) { + // 删除车辆项的金额记录 + Db::table('veh_money_value') + ->where('object_scene', 'stock_order_items') + ->where('object_id', $item->id) + ->delete(); + } + + // 删除车辆项 + StockOrderItems::where('order_id', $orderId)->delete(); + + // 删除订单本身的金额记录 + Db::table('veh_money_value') + ->where('object_scene', 'stock_orders') + ->where('object_id', $orderId) + ->delete(); + + // 删除账户项 + StockOrderAccounts::where('order_id', $orderId)->delete(); + } + + /** + * 处理车辆项 + */ + private function processVehicleItems($order, array $vehList): void + { + $cifTotalAmount = 0.00; + // $cifTotalAmountRate = 0; + // $cifTotalAmountCurrency = 'CNY'; + + $profitTotalAmount = 0.00; + foreach ($vehList as $veh) { + $veh['order_id'] = $order->id; + $veh['unique_id'] = $this->generateVehicleUniqueId($veh); + + $item = StockOrderItems::create($veh); + + // 处理金额信息 + $this->processVehiclePrices($item, $veh); + + // 计算利润 + $profit = $this->calculateProfit($veh, $order); + $item->addMoney('profit', $order->settlement_currency, $profit, $order->contract_exchange_rate, '利润'); + + // 累计到岸价 + $cifTotalAmount += $veh['total_price']['amount'] ?? 0; + $profitTotalAmount += $profit; + + // if (isset($veh['retrofit_fee']['rate'])) { + // $cifTotalAmountRate = $veh['retrofit_fee']['rate']; + // } + // if (isset($veh['retrofit_fee']['currency'])) { + // $cifTotalAmountCurrency = $veh['retrofit_fee']['currency']; + // } + + // 处理改装项 + $this->processModificationItems($item->id, $veh['modify_list'] ?? []); + } + + // 保存合计到岸价 + $order->addMoney('cif_total_amount', $order->settlement_currency, $cifTotalAmount, $order->contract_exchange_rate, '合计到岸价'); + // 利润 + $order->addMoney('profit_total', $order->settlement_currency, $profitTotalAmount, $order->contract_exchange_rate, '总利润'); + } + + /** + * 生成车辆唯一标识 + */ + private function generateVehicleUniqueId(array $veh): string + { + $uniqueIdStr = ""; + $uniqueIdStr .= $veh['sku_id'] ?? ''; + $uniqueIdStr .= $veh['model_snapshot'] ?? ''; + $uniqueIdStr .= $veh['color_interior_code'] ?? ''; + $uniqueIdStr .= $veh['color_exterior_code'] ?? ''; + $uniqueIdStr .= $veh['mileage'] ?? ''; + $uniqueIdStr .= json_encode($veh['modify_list'] ?? []); + + return md5($uniqueIdStr); + } + + /** + * 处理车辆价格信息 + */ + private function processVehiclePrices($item, array $veh): void + { + // 基础费用 + $priceFields = [ + 'unit_price' => '单价', + 'total_price' => '总价', + 'handling_fee' => '手续费', + 'fob_transport_fee' => 'FOB运输费', + 'port_misc_fee' => '港杂', + 'retrofit_fee' => '改装费', + 'ocean_freight' => '海运费' + ]; + + foreach ($priceFields as $field => $description) { + if (!empty($veh[$field]['amount'])) { + $item->addMoney( + $field, + $veh[$field]['currency'], + $veh[$field]['amount'], + $veh[$field]['rate'], + $description + ); + } + } + + // 根据里程处理新车或二手车价格 + if (!empty($veh['mileage']) && $veh['mileage'] > 0) { + // 二手车 + if (!empty($veh['listing_price']['amount'])) { + $item->addMoney( + 'listing_price', + $veh['listing_price']['currency'], + $veh['listing_price']['amount'], + $veh['listing_price']['rate'], + '二手挂牌价' + ); + } + + if (!empty($veh['reduction_price']['amount'])) { + $item->addMoney( + 'reduction_price', + $veh['reduction_price']['currency'], + $veh['reduction_price']['amount'], + $veh['reduction_price']['rate'], + '二手减价' + ); + } + } else { + // 新车 + if (!empty($veh['official_price']['amount'])) { + $item->addMoney( + 'official_price', + $veh['official_price']['currency'], + $veh['official_price']['amount'], + $veh['official_price']['rate'], + '官方指导价' + ); + } + + if (!empty($veh['discount_price']['amount'])) { + $item->addMoney( + 'discount_price', + $veh['discount_price']['currency'], + $veh['discount_price']['amount'], + $veh['discount_price']['rate'], + '优惠价' + ); + } + } + } + + /** + * 计算利润 + */ + private function calculateProfit(array $veh, $order): float + { + $unitPrice = (float)($veh['unit_price']['amount'] ?? 0); + + if (!empty($veh['mileage']) && $veh['mileage'] > 0) { + // 二手车 + $ttPrice = ($veh['listing_price']['amount'] ?? 0) / ($order->contract_exchange_rate ?: 1); + $ddPrice = $veh['reduction_price']['amount'] ?? 0; + } else { + // 新车 + $ttPrice = ($veh['official_price']['amount'] ?? 0) / ($order->contract_exchange_rate ?: 1); + $ddPrice = $veh['discount_price']['amount'] ?? 0; + } + + $handlingFee = (float)($veh['handling_fee']['amount'] ?? 0); + $fobTransportFee = (float)($veh['fob_transport_fee']['amount'] ?? 0); + $portMiscFee = (float)($veh['port_misc_fee']['amount'] ?? 0); + $retrofitFee = (float)($veh['retrofit_fee']['amount'] ?? 0); + $oceanFreight = (float)($veh['ocean_freight']['amount'] ?? 0); + + return $unitPrice - $ttPrice - $ddPrice - $handlingFee - $fobTransportFee - $portMiscFee - $retrofitFee - $oceanFreight; + } + + /** + * 处理改装项 + */ + private function processModificationItems(int $orderItemId, array $modifyList): void + { + if (empty($modifyList)) { + return; + } + + foreach ($modifyList as $modify) { + $modify['order_item_id'] = $orderItemId; + StockOrderItemOptions::create($modify); + } + } + + /** + * 处理账户项 + */ + private function processAccountItems($order, array $accountList): void + { + if (empty($accountList)) { + return; + } + + foreach ($accountList as $account) { + $account['order_id'] = $order->id; + if (!empty($account['remit_date'])) { + $account['remit_date'] = strtotime($account['remit_date']); + } + StockOrderAccounts::create($account); + } + } +} \ No newline at end of file diff --git a/app/common/service/StockOrdersService_bak.php b/app/common/service/StockOrdersService_bak.php new file mode 100644 index 0000000..65ef296 --- /dev/null +++ b/app/common/service/StockOrdersService_bak.php @@ -0,0 +1,435 @@ +validator; + } + + + protected function buildQueryConfig(Request $request, string $scene = 'index'): array + { + $p = $request->all(); + + $config = []; + $config['model'] = $this->modelClass; + switch ($scene) { + case 'index': + + // ====== ====== 筛选 ====== ====== + // 业务id + if (isset($p['salesperson_id']) && $p['salesperson_id']) { + $config['where']['salesperson_id'] = $p['salesperson_id']; + } + // 初、终版 + $config['where']['is_final'] = $p['is_final'] ?? 0; + // 关键字 + if (isset($p['keyword']) && $p['keyword']) { + $k = '%' . $p['keyword'] . '%'; + foreach (['pi_no', 'order_no', 'customer_snapshot'] as $col) { + $config['orWhere'][] = [$col, 'like', $k]; + } + } + + $config['order']['id'] = 'desc'; + + // $config['dataLimit']['type'] = 'relation'; + // $config['dataLimit']['userId'] = $request->user->uid; + // $config['dataLimit']['userField'] = 'salesperson_id'; + break; + case 'print': + $config['where']['id'] = $p['id']; + break; + case 'show': + $config['where']['id'] = $p['id']; + + $config['with'][] = 'items'; + $config['with'][] = 'items.options'; + $config['with'][] = 'items.moneyValues'; + $config['with'][] = 'accounts'; + $config['with'][] = 'moneyValues'; + $config['with'][] = 'salesperson'; + break; + case 'store': + break; + case 'update': + $config['where']['id'] = $p['id']; + break; + case 'destroy': + $config['where']['id'] = $p['id']; + break; + } + + return $config; + } + + + public function serviceStore(Request $request, array $data = []) + { + // 关联表较多,增加事务处理 + return Db::transaction(function () use ($request, $data) { + $data = $this->preProcess('Store', $data, $request); + $dl = $this->getDataLimitFromRequest($request); + $created = $this->logicCreate($data, $dl); // 表1 + + // 车辆 + $cifTotalAmount = 0; + $cifTotalAmountRate = 0; + $cifTotalAmountCurrency = 'CNY'; + // $profit = 0; + foreach ($data['veh_list'] as $veh) { + $veh['order_id'] = $created->id; + + // 唯一标识 + $uniqueIdStr = ""; + $uniqueIdStr .= $veh['sku_id']; + $uniqueIdStr .= $veh['model_snapshot']; + $uniqueIdStr .= $veh['color_interior_code']; + $uniqueIdStr .= $veh['color_exterior_code']; + $uniqueIdStr .= $veh['mileage']; + $uniqueIdStr .= json($veh['modify_list']); + $veh['unique_id'] = md5($uniqueIdStr); + $item = StockOrderItems::create($veh); // 表2 + + // 金额 // 表3 + // $item->addMoney('official_price', $veh['official_price']['currency'], $veh['official_price']['amount'], $veh['official_price']['rate'], '官方指导价'); + // $item->addMoney('discount_price', $veh['discount_price']['currency'], $veh['discount_price']['amount'], $veh['discount_price']['rate'], '优惠价'); + if ($veh['unit_price']['amount']) { + $item->addMoney('unit_price', $veh['unit_price']['currency'], $veh['unit_price']['amount'], $veh['unit_price']['rate'], '单价'); + } + if ($veh['total_price']['amount']) { + $item->addMoney('total_price', $veh['total_price']['currency'], $veh['total_price']['amount'], $veh['total_price']['rate'], '总价'); + } + if ($veh['handling_fee']['amount']) { + $item->addMoney('handling_fee', $veh['handling_fee']['currency'], $veh['handling_fee']['amount'], $veh['handling_fee']['rate'], '手续费'); + } + if ($veh['fob_transport_fee']['amount']) { + $item->addMoney('fob_transport_fee', $veh['fob_transport_fee']['currency'], $veh['fob_transport_fee']['amount'], $veh['fob_transport_fee']['rate'], 'FOB运输费'); + } + if ($veh['port_misc_fee']['amount']) { + $item->addMoney('port_misc_fee', $veh['port_misc_fee']['currency'], $veh['port_misc_fee']['amount'], $veh['port_misc_fee']['rate'], '港杂'); + } + if ($veh['retrofit_fee']['amount']) { + $item->addMoney('retrofit_fee', $veh['retrofit_fee']['currency'], $veh['retrofit_fee']['amount'], $veh['retrofit_fee']['rate'], '改装费'); + } + + // 二手车 or 新车 + if ($veh['mileage'] > 0) { + if ($veh['listing_price']['amount']) { + $item->addMoney('listing_price', $veh['listing_price']['currency'], $veh['listing_price']['amount'], $veh['listing_price']['rate'], '二手挂牌价'); + } + if ($veh['reduction_price']['amount']) { + $item->addMoney('reduction_price', $veh['reduction_price']['currency'], $veh['reduction_price']['amount'], $veh['reduction_price']['rate'], '二手减价'); + } + + $ttPrice = ($veh['listing_price']['amount'] / $created->contract_exchange_rate); + $ddPrice = $veh['reduction_price']['amount']; + } else { + if ($veh['official_price']['amount']) { + $item->addMoney('official_price', $veh['official_price']['currency'], $veh['official_price']['amount'], $veh['official_price']['rate'], '官方指导价'); + } + if ($veh['discount_price']['amount'] ) { + $item->addMoney('discount_price', $veh['discount_price']['currency'], $veh['discount_price']['amount'], $veh['discount_price']['rate'], '优惠价'); + } + + $ttPrice = ($veh['official_price']['amount'] / $created->contract_exchange_rate); + $ddPrice = $veh['discount_price']['amount']; + } + + // 利润 + $profit = (float)$veh['unit_price']['amount'] - (float)$ttPrice - (float)$ddPrice - (float)$veh['handling_fee']['amount'] - (float)$veh['fob_transport_fee']['amount'] - (float)$veh['port_misc_fee']['amount'] - (float)$veh['retrofit_fee']['amount']; + $item->addMoney('profit', $veh['unit_price']['currency'], $profit, $veh['unit_price']['rate'], '利润'); + + // cif_total_amount + $cifTotalAmount += $veh['total_price']['amount']; + $cifTotalAmountRate = $veh['retrofit_fee']['rate']; + $cifTotalAmountCurrency = $veh['retrofit_fee']['currency']; + + // 改装 + if (count($veh['modify_list']) > 0) { + foreach ($veh['modify_list'] as $modify) { + $modify['order_item_id'] = $item->id; + StockOrderItemOptions::create($modify); // 表4 + } + } + } + + // 计算&&插入 表1 + $created->addMoney('cif_total_amount', $cifTotalAmountCurrency, $cifTotalAmount, $cifTotalAmountRate, '合计到岸价'); + + // 账户 + if (count($data['account_list']) > 0) { + foreach ($data['account_list'] as $account) { + $account['order_id'] = $created->id; + $account['remit_date'] = strtotime($account['remit_date']); + StockOrderAccounts::create($account); // 表5 + } + } + + return $this->postProcess('Store', $created); + }); + } + + public function beforeServiceStore($data, Request $request) + { + $userId = $request->user->uid; + + $data['order_no'] = generateStockNo(); + $data['biz_date'] = strtotime($data['biz_date']); + $data['eta_ship_date'] = strtotime($data['eta_ship_date']); + $data['latest_arrival_date'] = strtotime($data['latest_arrival_date']); + $data['salesperson_id'] = $userId; + $data['salesperson_snapshot'] = User::where('id', $userId)->value('name') ?? ''; + + return $data; + } + + protected function afterServiceStore($res) + { + // 处理原单 + if ($res->is_final == 1 && $res->object_id > 0) { + $order = StockOrders::find($res->object_id); + $order->is_use = 1; + $order->object_id = $res->id; + if (!$order->save()) { + flg('备货单【'.$order->order_no.'】创建终版成功,但更新原单失败'); + } + } + // 启动审批 + $appro = new \app\common\utils\approval\service\ApprovalFlowService(); + $result = $appro->start('stock_orders', 'stock_orders', $res->id, $res->toArray(), $res->salesperson_id); + var_dump($result); + + // 异步生成pdf文件 + \Webman\RedisQueue\Redis::send('stock-order-pdf', ['id'=>$res->id]); + return $res; + } + + + public function createdPdfFile($id) + { + $config = []; + $config['model'] = $this->modelClass; + $config['where']['id'] = $id; + $config['with'][] = 'items'; + $config['with'][] = 'items.options'; + $config['with'][] = 'items.moneyValues'; + $config['with'][] = 'accounts'; + $config['with'][] = 'moneyValues'; + $config['with'][] = 'salesperson'; + $item = $this->logicShow($config); + $pdfData = $this->pdfData($item); + + // 渲染模板为最终HTML + $tplPath = app_path('common/utils/pdf/template/stockOrder.stub'); + $html = \app\common\utils\pdf\service\PdfHelper::render($tplPath, ['data' => $pdfData]); + + // 生成PDF + $driver = new \app\common\utils\pdf\service\MpdfDriver(); + $pdfBin = $driver->htmlToPdf($html); + + // 存储 + $dir = public_path('pdf').'/'; + $fileName = ($pdfData['order_no'] ?? date('YmdHis')) . '.pdf'; + + // 本地保存 + \app\common\utils\pdf\service\PdfStorage::saveBinary( + $pdfBin, + $dir, + $fileName + ); + + // // 在线预览 + // return new \support\Response(200, [ + // 'Content-Type' => 'application/pdf', + // 'Content-Disposition' => 'inline; filename="stock-order.pdf"', + // ], $pdfBin); + + // $url = [ + // 'file_name' => $fileName, + // 'file_url' => getenv('MAIN_DOMAIN').'/pdf/'.$fileName + // ]; + + // 上传云端 + $param = [ + 'path' => $dir . $fileName, + 'scene' => 'stock_orders', + 'category' => 'pdf', + 'original_name' => $fileName + ]; + $up = new UploadService(); + $res = $up->uploadLocalFile($param); + + @unlink($dir . $fileName); + + // 更新文件地址 + $item->file_url = $res['file_name']; + $item->save(); + + return $res; + } + + public function servicePrint(Request $request) + { + $config = $this->buildQueryConfig($request, 'show'); + $item = $this->logicShow($config); + return $this->postProcess('Print', $item); + } + + + public function pdfData($item) + { + $vehList = []; + $index = 0; + foreach ($item->items as $k => $v) { + // 存在则数量加1 + if (count($vehList) > 0 && isset($vehList[$v->unique_id])) { + $vehList[$v->unique_id]['num'] += 1; + $vehList[$v->unique_id]['total_price'] += $vehList[$v->unique_id]['unit_price']; + $vehList[$v->unique_id]['total_price_text'] = $vehList[$v->unique_id]['total_price_symbol'].number_format($vehList[$v->unique_id]['total_price'], 2, '.', ','); + continue; + } + $index += 1; + // 处理金额 + $mv = []; + foreach ($v->moneyValues as $money) { + $mv[$money->field] = [ + 'currency' => $money->currency, + 'amount' => $money->amount, + 'rate' => $money->exchange_rate, + 'currency_symbol' => SysCurrency::where('currency_code', $money->currency)->value('currency_symbol'), + ]; + } + // 处理改装 + $mfy = ''; + foreach ($v->options as $my) { + $mfy .= $my->option_name_snapshot.' '; + } + + // 处理结果集 + $vehList[$v->unique_id] = [ + 'index' => $index, + 'model_snapshot' => $v->model_snapshot, + 'color_interior_snapshot' => $v->color_interior_snapshot, + 'color_exterior_snapshot' => $v->color_exterior_snapshot, + 'num' => 1, + 'modify' => $mfy, + 'mileage' => $v->mileage, + 'unit_price' => $mv['unit_price']['amount'], + 'total_price' => $mv['total_price']['amount'], + 'unit_price_symbol' => $mv['unit_price']['currency_symbol'], + 'total_price_symbol' => $mv['total_price']['currency_symbol'], + 'unit_price_text' => $mv['unit_price']['currency_symbol'].number_format($mv['unit_price']['amount'], 2, '.', ','), + 'total_price_text' => $mv['total_price']['currency_symbol'].number_format($mv['total_price']['amount'], 2, '.', ',') + ]; + + // 根据里程处理 新车or二手车 + if ($v->mileage > 0) { + if (isset($mv['reduction_price'])) { + $vehList[$v->unique_id]['reduction_price'] = $mv['reduction_price']['amount']; + $vehList[$v->unique_id]['reduction_price_symbol'] = $mv['reduction_price']['currency_symbol']; + $vehList[$v->unique_id]['reduction_price_text'] = $mv['reduction_price']['currency_symbol'].number_format($mv['reduction_price']['amount'], 2, '.', ','); + } + $vehList[$v->unique_id]['listing_price'] = $mv['listing_price']['amount']; + $vehList[$v->unique_id]['listing_price_symbol'] = $mv['listing_price']['currency_symbol']; + $vehList[$v->unique_id]['listing_price_text'] = $mv['listing_price']['currency_symbol'].number_format($mv['listing_price']['amount'], 2, '.', ','); + } else { + if (isset($mv['discount_price'])) { + $vehList[$v->unique_id]['discount_price'] = $mv['discount_price']['amount']; + $vehList[$v->unique_id]['discount_price_symbol'] = $mv['discount_price']['currency_symbol']; + $vehList[$v->unique_id]['discount_price_text'] = $mv['discount_price']['currency_symbol'].number_format($mv['discount_price']['amount'], 2, '.', ','); + } + $vehList[$v->unique_id]['official_price'] = $mv['official_price']['amount']; + $vehList[$v->unique_id]['official_price_symbol'] = $mv['official_price']['currency_symbol']; + $vehList[$v->unique_id]['official_price_text'] = $mv['official_price']['currency_symbol'].number_format($mv['official_price']['amount'], 2, '.', ','); + } + } + + $accList = []; + $accIndex = 1; + foreach ($item->accounts as $k => $v) { + $accList[] = [ + 'index' => '收款'.$k + 1, + 'remit_country_name_snapshot' => $v->remit_country_name_snapshot, + 'remit_date' => date('Y-m-d', $v->remit_date), + 'currency_name_snapshot' => $v->currency_name_snapshot, + 'account_name_snapshot' => $v->account_name_snapshot, + 'remitter_name' => $v->remitter_name, + 'amount' => $v->amount, + ]; + $accIndex += 1; + } + + $cifTotalAmount = 0.00; + if ($item->moneyValues) { + $cifTotalAmountSysboml = SysCurrency::where('currency_code', $item->moneyValues->currency)->value('currency_symbol'); + $cifTotalAmount = $cifTotalAmountSysboml.sprintf("%.2f", $item->moneyValues->amount, $item->moneyValues->exchange_rate); + } + + $veh = [ + 'order_no' => $item->order_no, + 'salesperson_snapshot' => $item->salesperson_snapshot, + // 'sign_img' => $item->salesperson->sign_img, + 'sign_img_url' => (!empty($item->salesperson->sign_img))?(new \app\common\service\OssService)->generatePrivateUrl($item->salesperson->sign_img):'', + 'created_at' => date('Y-m-d', strtotime($item->created_at)), + + 'pi_no' => $item->pi_no, + 'biz_date' => date('Y-m-d', $item->biz_date), + 'clearance_country_snapshot' => $item->clearance_country_snapshot, + 'customer_source_snapshot' => $item->customer_source_snapshot, + 'customer_snapshot' => $item->customer_snapshot, + 'customer_country_snapshot' => $item->customer_country_snapshot, + + // 订单项 + 'veh_list' => $vehList, + + 'contract_exchange_rate' => $item->contract_exchange_rate, + 'cif_total_amount' => $item->cif_total_amount, + + 'declarant_name' => $item->declarant_name, + 'eta_ship_date' => date('Y-m-d', $item->eta_ship_date), + 'shipping_method_snapshot' => $item->shipping_method_snapshot, + 'domestic_port_snapshot' => $item->domestic_port_snapshot, + 'trade_term_snapshot' => $item->trade_term_snapshot, + 'clearance_port_snapshot' => $item->clearance_port_snapshot, + 'latest_arrival_date' => date('Y-m-d', $item->latest_arrival_date), + + // 账户 + 'account_list' => $accList, + 'acc_index' => $accIndex, + 'cif_total_amount' => $cifTotalAmount + ]; + + return $veh; + } +} \ No newline at end of file diff --git a/app/common/service/SysAccountService.php b/app/common/service/SysAccountService.php new file mode 100644 index 0000000..b9c59ad --- /dev/null +++ b/app/common/service/SysAccountService.php @@ -0,0 +1,28 @@ +logic = (new SysAccountLogic()); + } + + /** + * 获取系统list + */ + public function getList() + { + return $this->logic->getList(); + } +} \ No newline at end of file diff --git a/app/common/service/SysCurrencyService.php b/app/common/service/SysCurrencyService.php new file mode 100644 index 0000000..f0108d0 --- /dev/null +++ b/app/common/service/SysCurrencyService.php @@ -0,0 +1,25 @@ +logic = (new SysCurrencyLogic()); + } + + /** + * 获取系统list + */ + public function getList() + { + return $this->logic->getList(); + } +} \ No newline at end of file diff --git a/app/common/service/SysDictItemService.php b/app/common/service/SysDictItemService.php new file mode 100644 index 0000000..c1e51ed --- /dev/null +++ b/app/common/service/SysDictItemService.php @@ -0,0 +1,42 @@ +logic = (new SysDictItemLogic()); + } + + /** + * 获取系统list + */ + public function getList($params) + { + if (empty($params['dict_id']) && empty($params['dict_code'])) + throw new BusinessException('未明确要查询的字典组'); + $where = []; + if(isset($params['dict_id']) && $params['dict_id']) + $where[] = ['dict_id', '=', $params['dict_id']]; + if(isset($params['dict_code']) && $params['dict_code']){ + $dict = (new SysDictLogic)->getDictByCode($params['dict_code']); + if (empty($dict['id'])) + throw new BusinessException('字典组code错误'); + $where[] = ['dict_id', '=', $dict['id']]; + } + + return $this->logic->getList($where); + } +} \ No newline at end of file diff --git a/app/common/service/SysDictService.php b/app/common/service/SysDictService.php new file mode 100644 index 0000000..4918017 --- /dev/null +++ b/app/common/service/SysDictService.php @@ -0,0 +1,52 @@ +logic = (new SysDictLogic()); + } + + /** + * 获取系统list + */ + public function getList() + { + return $this->logic->getList(); + } + + + + public static function pluckValuesByCode(string $dictCode): array + { + return SysDict::where('dict_code', $dictCode) + ->where('status', 1) + ->first()?->items() + ->where('status', 1) + ->orderBy('sort') + ->pluck('item_value') + ->toArray() ?? []; + } + + public static function pluckKeyValueByCode(string $dictCode): array + { + return SysDict::where('dict_code', $dictCode) + ->where('status', 1) + ->first()?->items() + ->where('status', 1) + ->orderBy('sort') + ->pluck('item_value', 'item_label') + ->toArray() ?? []; + } +} \ No newline at end of file diff --git a/app/common/service/SysPortService.php b/app/common/service/SysPortService.php new file mode 100644 index 0000000..530fcbf --- /dev/null +++ b/app/common/service/SysPortService.php @@ -0,0 +1,36 @@ +logic = (new SysPortLogic()); + } + + /** + * 获取系统list + */ + public function getList($params) + { + $where = []; + if(isset($params['scene']) && $params['scene']) { + if($params['scene'] == 'domestic'){ + $where['country_id'] = 45; + }else{ + $where[] = ['country_id', '<>', 45]; + } + } + return $this->logic->getList($where); + } +} \ No newline at end of file diff --git a/app/common/service/UploadService.php b/app/common/service/UploadService.php new file mode 100644 index 0000000..43684cf --- /dev/null +++ b/app/common/service/UploadService.php @@ -0,0 +1,177 @@ +findFile(['file_path' => $candidatePath]); + if (!$existing) { + break; + } + // 若存在同路径同名,且类型也相同,则追加序号 + if (isset($existing->file_type) && strtolower($existing->file_type) === strtolower($mime)) { + $suffix = ' (' . $counter . ')'; + $candidateName = $nameBody . $suffix . ($nameExt !== '' ? ('.' . $nameExt) : ''); + $counter++; + continue; + } + // 类型不同则允许同名,结束循环 + break; + } + + $objectKey = $dir . $candidateName; + + $ossService = new OssService(); + $ossService->uploadImage($filePath, $objectKey); + $privateUrl = $ossService->generatePrivateUrl($objectKey); + + // 记录文件信息到数据库 + $uploadedFilesService = new UploadedFilesService(); + $fileInfo = [ + 'file_name' => basename($objectKey), + 'file_path' => $objectKey, + 'file_size' => $size, + 'file_type' => $mime, + 'original_name' => $originalName, + ]; + $recordResult = $uploadedFilesService->recordFileInfo($fileInfo, null, $scene, $category); + + $data = [ + 'original_name' => $originalName, + 'file_name' => $objectKey, + 'file_url' => $privateUrl, + 'file_id' => $recordResult['id'] ?? null, + 'partition' => $scene, + 'category' => $category, + ]; + + return $data; + } catch (\Throwable $e) { + Log::error($e->getMessage(),$param); + throw new BusinessException($e->getMessage()); + } + } + + /** + * 删除 OSS 上的文件 + * + * @param array $param + * - object_key: OSS 对象键名-oss路径(必需) + * - file_id: 数据库文件记录ID(可选,用于删除数据库记录) + * @return bool 删除是否成功 + * + * 示例 + * $uploadService = new UploadService(); + * + * 只删除 OSS 文件 + * $uploadService->deleteOssFile([ + * 'object_key' => 'vehicle_album/documents/20241201/abc123.pdf' + * ]); + * + * 同时删除 OSS 文件和数据库记录 + * $uploadService->deleteOssFile([ + * 'object_key' => 'vehicle_album/images/20241201/abc123.jpg', + * 'file_id' => 123 + * ]); + */ + public function deleteOssFile($objectKey,$fileId = null) + { + try { + + if (empty($objectKey)) { + Log::error('OSS对象键名不能为空', $objectKey); + throw new BusinessException('OSS对象键名不能为空'); + } + + $uploadedFilesService = new UploadedFilesService(); + // 如果提供了文件ID,同时删除数据库记录 + if ($fileId) { + $uploadedFilesService->deleteFile($fileId); + }else{ + $uploadedFilesService = new UploadedFilesService(); + if (empty($fileId)) { + $where = ['file_path' => $objectKey]; + $file = $uploadedFilesService->getFile($where); + $fileId = $file->id; + } + if(!empty($fileId)){ + $uploadedFilesService->deleteFile($fileId); + } + } + + // 删除 OSS 上的文件 + $ossService = new OssService(); + $ossService->deleteFile($objectKey); + + Log::info('OSS文件删除成功', ['object_key' => $objectKey, 'file_id' => $fileId]); + return true; + } catch (\Throwable $e) { + Log::error('删除OSS文件失败: ' . $e->getMessage()); + throw new BusinessException('删除OSS文件失败: ' . $e->getMessage()); + } + } +} \ No newline at end of file diff --git a/app/common/service/UploadedFilesService.php b/app/common/service/UploadedFilesService.php new file mode 100644 index 0000000..ed01db2 --- /dev/null +++ b/app/common/service/UploadedFilesService.php @@ -0,0 +1,238 @@ + [ + 'path' => 'images', + 'max_size' => 5 * 1024 * 1024, // 5MB + 'allowed_types' => ['image/jpeg', 'image/png', 'image/gif', 'image/webp'], + 'extensions' => ['jpg', 'jpeg', 'png', 'gif', 'webp'] + ], + 'document' => [ + 'path' => 'documents', + 'max_size' => 10 * 1024 * 1024, // 10MB + 'allowed_types' => ['application/pdf', 'application/msword', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'], + 'extensions' => ['pdf', 'doc', 'docx'] + ], + 'audio' => [ + 'path' => 'audios', + 'max_size' => 20 * 1024 * 1024, // 20MB + 'allowed_types' => ['audio/mp3', 'audio/wav', 'audio/ogg'], + 'extensions' => ['mp3', 'wav', 'ogg'] + ], + 'other' => [ + 'path' => 'others', + 'max_size' => 5 * 1024 * 1024, // 5MB + 'allowed_types' => [], + 'extensions' => [] + ] + ]; + + /** + * 记录文件信息到数据库 + * + * @param array $fileInfo 文件信息 + * @param Request|null $request 请求对象(可选) + * @param string $partition 分区名称 + * @param string $category 文件分类 + * @return array + * @throws BusinessException + */ + public function recordFileInfo(array $fileInfo, ?Request $request = null, string $partition = 'other', string $category = 'general'): array + { + + // 验证必需的文件信息 + $requiredFields = ['file_name', 'file_path', 'file_size', 'file_type']; + foreach ($requiredFields as $field) { + if (!isset($fileInfo[$field])) { + throw new BusinessException("缺少必需的文件信息字段: {$field}"); + } + } + + // 获取文件扩展名 + $fileExtension = pathinfo($fileInfo['file_name'], PATHINFO_EXTENSION); + + // 记录到数据库 + $uploadIp = $request ? $request->getRealIp() : '0.0.0.0'; + $userAgent = $request ? $request->header('User-Agent', '') : ''; + + $fileData = [ + 'file_name' => $fileInfo['file_name'], + 'file_path' => $fileInfo['file_path'], + 'file_size' => $fileInfo['file_size'], + 'file_type' => $fileInfo['file_type'], + 'category' => $category, + 'original_name' => $fileInfo['original_name'] ?? '', + 'extension' => $fileExtension, + 'partition' => $partition, + 'upload_ip' => $uploadIp, + 'user_agent' => $userAgent, + ]; + + $uploadedFile = $this->logicCreate($fileData); + + return [ + 'id' => $uploadedFile->id, + 'file_name' => $fileInfo['file_name'], + 'file_path' => $fileInfo['file_path'], + 'file_url' => getenv('MAIN_DOMAIN') . $fileInfo['file_path'], + 'file_size' => $fileInfo['file_size'], + 'file_type' => $fileInfo['file_type'], + 'category' => $category, + 'partition' => $partition, + 'original_name' => $fileInfo['original_name'] ?? '', + 'extension' => $fileExtension, + ]; + } + + /** + * 获取文件信息 + * + * @param int $fileId + * @return array|null + */ + public function getFileInfo(int $fileId): ?array + { + $file = $this->logicShow(['where' => ['id' => $fileId]]); + + if (!$file) { + return null; + } + + return [ + 'id' => $file->id, + 'file_name' => $file->file_name, + 'file_path' => $file->file_path, + 'file_url' => getenv('MAIN_DOMAIN') . $file->file_path, + 'file_size' => $file->file_size, + 'file_type' => $file->file_type, + 'category' => $file->category, + 'partition' => $file->partition ?? 'other', + 'original_name' => $file->original_name ?? '', + 'extension' => $file->extension ?? '', + 'created_at' => $file->created_at, + ]; + } + + /** + * 获取文件 + * + * @param string $path + * @return bool|\Illuminate\Database\Eloquent\Model + */ + public function getFile(array $where) + { + $config['model'] = $this->modelClass; + $config['where'] = $where; + $file = $this->logicShow($config); + + if (!$file) { + return false; + } + + return $file; + } + /** + * 获取文件 + * + * @param string $path + * @return bool|\Illuminate\Database\Eloquent\Model + */ + public function findFile(array $where) + { + $file = UploadedFiles::where(['file_path' => $where['file_path']]); + + if (!$file) { + return false; + } + + return $file; + } + + /** + * 删除文件 + * + * @param int $fileId + * @return bool + */ + public function deleteFile(int $fileId): bool + { + $config['model'] = $this->modelClass; + $config['where'] = ['id' => $fileId]; + $file = $this->logicShow($config); + + if (!$file) { + return false; + } + + // 删除数据库记录 + $config =[ + 'model' => $this->modelClass, + 'where' => ['id' => $fileId], + ]; + return $this->logicDeleteBy($config); + } + + /** + * 获取分区配置 + * + * @return array + */ + public function getPartitionConfig(): array + { + return $this->uploadPartitions; + } + + /** + * 生成文件名 + * + * @param $file + * @return string + */ + protected function generateFileName($file): string + { + $extension = $file->getUploadExtension(); + $timestamp = time(); + $random = substr(md5(uniqid()), 0, 8); + + return $timestamp . '_' . $random . '.' . $extension; + } + + /** + * 格式化文件大小 + * + * @param int $bytes + * @return string + */ + protected function formatFileSize(int $bytes): string + { + $units = ['B', 'KB', 'MB', 'GB']; + $bytes = max($bytes, 0); + $pow = floor(($bytes ? log($bytes) : 0) / log(1024)); + $pow = min($pow, count($units) - 1); + + $bytes /= pow(1024, $pow); + + return round($bytes, 2) . ' ' . $units[$pow]; + } +} \ No newline at end of file diff --git a/app/common/service/UserService.php b/app/common/service/UserService.php index 53507b2..74c12c4 100644 --- a/app/common/service/UserService.php +++ b/app/common/service/UserService.php @@ -52,6 +52,9 @@ class UserService 'sn' => $user->sn, 'corp_id' => $user->corp_id, 'corp_name' => $corp_name, + 'report_to' => $user->report_to, + 'sign_img' => $user->sign_img, + 'sign_img_url' => (!empty($user->sign_img))?(new \app\common\service\OssService)->generatePrivateUrl($user->sign_img):'', ], // 'menu' => $menuTree, // 'permissions' => $permissionCodes @@ -84,7 +87,9 @@ class UserService 'email' => $user->email, 'sex' => $user->sex, 'status' => $user->status, - 'roles' => $user->roles->pluck('name') + 'roles' => $user->roles->pluck('name'), + 'sign_img' => $user->sign_img, + 'sign_img_url' => (!empty($user->sign_img))?$ossservice->generatePrivateUrl($user->sign_img):'', ]; } diff --git a/app/common/service/VehColorService.php b/app/common/service/VehColorService.php new file mode 100644 index 0000000..2060efe --- /dev/null +++ b/app/common/service/VehColorService.php @@ -0,0 +1,33 @@ +logic = (new VehColorLogic()); + } + + /** + * 获取系统list + */ + public function getList($params) + { + if (empty($params['type'])) + throw new BusinessException('类型不能为空'); + $where = []; + if(isset($params['type']) && $params['type']) + $where[] = ['type', '=', $params['type']]; + return $this->logic->getList($where); + } +} \ No newline at end of file diff --git a/app/common/service/VehDictI18nService.php b/app/common/service/VehDictI18nService.php new file mode 100644 index 0000000..1c738d2 --- /dev/null +++ b/app/common/service/VehDictI18nService.php @@ -0,0 +1,75 @@ +validator; + } + + + /** 本进程缓存:[dict][code] => labels array */ + protected static array $labelsCache = []; + + public static function label(string $dict=null, ?string $code=null, string $locale='zh-CN', ?string $fallbackZh=null): ?string + { + if (!$dict || !$code) return $fallbackZh; + + $labels = self::labels($dict, $code); + if (!$labels) return $fallbackZh ?? $code; + + return $labels[$locale] ?? $labels['zh-CN'] ?? $code; + } + + /** 返回 labels 数组,命中缓存 */ + public static function labels(string $dict, string $code): ?array + { + $dict = strtoupper($dict); + $code = (string)$code; + + if (isset(self::$labelsCache[$dict][$code])) { + return self::$labelsCache[$dict][$code]; + } + + $term = DictTerm::where('dict',$dict)->where('code',$code)->first(['labels']); + if (!$term) return null; + + $labels = $term->labels ?? null; + if (!is_array($labels)) return null; + + self::$labelsCache[$dict][$code] = $labels; + return $labels; + } + + /** 校验 code 是否存在于字典(用于必填维度) */ + public static function ensureExists(string $dict, ?string $code, string $fieldNameForError): void + { + if ($code === null || $code === '') { + throw new \InvalidArgumentException("缺少必填字段:{$fieldNameForError}"); + } + $ok = VehDictI18n::where('dict',$dict)->where('code',$code)->where('status', 1)->exists(); + if (!$ok) throw new \InvalidArgumentException("字典不存在或未启用:{$dict}.{$code}"); + } +} \ No newline at end of file diff --git a/app/common/service/VehDictMappingService.php b/app/common/service/VehDictMappingService.php new file mode 100644 index 0000000..67c5288 --- /dev/null +++ b/app/common/service/VehDictMappingService.php @@ -0,0 +1,57 @@ +validator; + } + + + public static function all(): array + { + $rows = VehDictMapping::where('status', 1)->orderBy('sort')->orderBy('id')->get()->toArray(); + $out = []; + foreach ($rows as $r) { + $out[$r['dict']] = [ + 'field' => $r['field'], + 'dict' => $r['dict'], + 'required' => (int)$r['required'] === 1, + ]; + } + return $out; + } + + public static function requiredDicts(): array + { + return array_keys(array_filter(self::all(), fn($x)=>$x['required']===true)); + } + + public static function enabled(string $dict): bool + { + $all = self::all(); + return isset($all[$dict]); + } +} \ No newline at end of file diff --git a/app/common/service/VehModificationService.php b/app/common/service/VehModificationService.php new file mode 100644 index 0000000..31c7119 --- /dev/null +++ b/app/common/service/VehModificationService.php @@ -0,0 +1,28 @@ +logic = (new VehModificationLogic()); + } + + /** + * 获取系统list + */ + public function getList() + { + return $this->logic->getList(); + } +} \ No newline at end of file diff --git a/app/common/service/VehMoneyValueService.php b/app/common/service/VehMoneyValueService.php new file mode 100644 index 0000000..361aa55 --- /dev/null +++ b/app/common/service/VehMoneyValueService.php @@ -0,0 +1,31 @@ +validator; + } +} \ No newline at end of file diff --git a/app/common/service/VehVehicleListingService.php b/app/common/service/VehVehicleListingService.php new file mode 100644 index 0000000..3aee146 --- /dev/null +++ b/app/common/service/VehVehicleListingService.php @@ -0,0 +1,31 @@ +validator; + } +} \ No newline at end of file diff --git a/app/common/service/VehVehicleSkuService.php b/app/common/service/VehVehicleSkuService.php new file mode 100644 index 0000000..0fa8649 --- /dev/null +++ b/app/common/service/VehVehicleSkuService.php @@ -0,0 +1,164 @@ +validator; + } + + + // // 枚举 + // private const MAPS = [ + // 'BODY' => [ + // 'SEDAN' => '三厢轿车', 'SUV'=>'SUV', 'HB'=>'两厢/掀背', 'MPV'=>'MPV', 'PICKUP'=>'皮卡' + // ], + // 'DRIVE' => [ + // 'FWD'=>'前驱', 'RWD'=>'后驱', 'AWD'=>'四驱(AWD)', '4WD'=>'四驱(4WD)' + // ], + // 'MOTOR_LAYOUT' => [ + // 'FWD'=>'前置单电机', 'RWD'=>'后置单电机', 'AWD'=>'双电机四驱', 'DM-I'=>'DM-i', 'DM-P'=>'DM-p' + // ], + // 'FUEL' => [ + // 'GAS'=>'汽油', 'HEV'=>'油电混动', 'PHEV'=>'插电混动', 'EV'=>'纯电', 'EREV'=>'增程' + // ], + // 'TRANS' => [ + // 'MT'=>'手动', 'AT'=>'自动', 'CVT'=>'无级', 'DCT'=>'双离合', '1SPEED'=>'单速' + // ], + // 'RANGE_STD' => [ + // 'CLTC'=>'CLTC', 'WLTP'=>'WLTP', 'EPA'=>'EPA', 'NEDC'=>'NEDC' + // ], + // 'EMISSION' => [ + // 'CHN6'=>'国六', 'EURO6'=>'欧6', 'EPA'=>'美国EPA' + // ], + // ]; + + // // *_code取中文 + // public function label(string $dict, ?string $code): ?string + // { + // if ($code === null || $code === '') return null; + // $dict = strtoupper($dict); + // $code = strtoupper($code); + // return self::MAPS[$dict][$code] ?? $code; + // } + + // // 静态映射把 *_code → 中文 + // private function backfill(array $d): array + // { + // $d['body_type'] = $this->label('BODY', $d['body_type_code']); + // $d['drive_type'] = $this->label('DRIVE', $d['drive_type_code']); + // $d['motor_layout'] = $this->label('MOTOR_LAYOUT', $d['motor_layout_code']); + // $d['fuel_type'] = $this->label('FUEL', $d['fuel_type_code']); + // $d['transmission'] = $this->label('TRANS', $d['transmission_code']); + // $d['range_std'] = $this->label('RANGE_STD', $d['range_std_code']); + // $d['emission_std'] = $this->label('EMISSION', $d['emission_std_code']); + // return $d; + // } + + protected function buildQueryConfig(Request $request, string $scene = 'default'): array + { + $p = $request->all(); + + $config = []; + $config['model'] = $this->modelClass; + switch ($scene) { + case 'default': + $config['where']['is_active'] = 1; + $config['order']['id'] = 'desc'; + break; + case 'index': + $config['where']['is_active'] = 1; + $config['order']['id'] = 'desc'; + break; + case 'show': + $config['where']['id'] = $p['id']; + $config['where']['is_active'] = 1; + break; + case 'store': + break; + case 'update': + $config['where']['id'] = $p['id']; + break; + case 'destroy': + $config['where']['id'] = $p['id']; + break; + } + + return $config; + } + + public function beforeServiceStore($data, Request $request) + { + // $data = $this->backfill($data); + if (empty($data['model_code'])) { + $data['model_code'] = ModelNaming::buildModelCode($data); + } + if (empty($data['model_name'])) { + $data['model_name'] = ModelNaming::buildModelName($data); + } + $data['sku_code'] = SkuCodeBuilder::build($data); + $data['corp_id'] = 0; + $data['created_by'] = $request->user->uid; + $data['updated_by'] = $request->user->uid; + // 指纹 + $data['fingerprint'] = Fingerprint::makeFromArray($data); + return $data; + } + + protected function afterServiceStore($res) + { + + return $res; + } + + + public function beforeServiceUpdate($data, Request $request) + { + unset($data['created_at']); + unset($data['updated_at']); + // $data = $this->backfill($data); + if (empty($data['model_code'])) { + $data['model_code'] = ModelNaming::buildModelCode($data); + } + if (empty($data['model_name'])) { + $data['model_name'] = ModelNaming::buildModelName($data); + } + // 填充系统字段 + $data['updated_by'] = $request->user->uid; + // 指纹 + $data['fingerprint'] = Fingerprint::makeFromArray($data); + return $data; + } + + protected function afterServiceUpdate($res) + { + + return $res; + } +} \ No newline at end of file diff --git a/app/common/service/VehVehicleUnitService.php b/app/common/service/VehVehicleUnitService.php new file mode 100644 index 0000000..4af3ea4 --- /dev/null +++ b/app/common/service/VehVehicleUnitService.php @@ -0,0 +1,31 @@ +validator; + } +} \ No newline at end of file diff --git a/app/common/traits/DataTransformer.php b/app/common/traits/DataTransformer.php index 4f3abac..adde8e6 100644 --- a/app/common/traits/DataTransformer.php +++ b/app/common/traits/DataTransformer.php @@ -50,6 +50,7 @@ trait DataTransformer return [ 'error' => true, 'message' => $e->getMessage(), + // 'message' => $e->getMessage()."\n".$e->getLine()."\n".$e->getFile(), 'code' => $e->getCode(), ]; } diff --git a/app/common/traits/HasMoneyJournal.php b/app/common/traits/HasMoneyJournal.php new file mode 100644 index 0000000..5d2c92d --- /dev/null +++ b/app/common/traits/HasMoneyJournal.php @@ -0,0 +1,85 @@ +hasMany(VehMoneyValue::class, 'object_id') + ->where('object_scene', static::moneyObjectScene()); + } + + /** 取某费项的“最新记录”(按时间/id 倒序) */ + public function moneyLatest(string $field): ?VehMoneyValue + { + return $this->moneyJournal() + ->where('field', $field) + ->orderByDesc('created_at') + ->orderByDesc('id') + ->first(); + } + + /** 追加一行金额 */ + public function addMoney(string $field, string $currency, $amount, ?string $rate = null, ?string $remark = null): VehMoneyValue + { + $now = time(); + /** @var VehMoneyValue $row */ + $row = $this->moneyJournal()->create([ + 'object_scene' => static::moneyObjectScene(), + 'object_id' => $this->id, + 'field' => $field, + 'currency' => strtoupper($currency), + 'amount' => (string)$amount, + 'exchange_rate' => $rate, + 'remark' => $remark, + 'created_at' => $now, + 'updated_at' => $now, + ]); + return $row; + } + + /** 批量追加:['unit_price'=>['currency'=>'USD','amount'=>'2199.00','rate'=>'7.2','remark'=>'...'], ...] */ + public function addMoneyBatch(array $payload): array + { + return Db::transaction(function () use ($payload) { + $out = []; + foreach ($payload as $field => $cfg) { + $out[$field] = $this->addMoney( + $field, + $cfg['currency'] ?? 'CNY', + $cfg['amount'] ?? '0.00', + $cfg['rate'] ?? null, + $cfg['remark'] ?? null + ); + } + return $out; + }); + } + + /** 把若干费项按各自汇率换到结算币并求和(无 rate 的跳过) */ + public function sumToSettlement(array $fields, int $scale = 2): string + { + $sum = '0.00'; + foreach ($fields as $f) { + $row = $this->moneyLatest($f); + if ($row) { + $v = $row->amountInSettlement($scale); + if ($v !== null) $sum = bcadd($sum, $v, $scale); + } + } + return $sum; + } +} \ No newline at end of file diff --git a/app/common/traits/crud/CrudServiceTrait.php b/app/common/traits/crud/CrudServiceTrait.php index dd4536f..eb563fe 100644 --- a/app/common/traits/crud/CrudServiceTrait.php +++ b/app/common/traits/crud/CrudServiceTrait.php @@ -27,63 +27,61 @@ trait CrudServiceTrait } /** 分页列表 */ - public function serviceList(Request $request) + public function serviceList(Request $request, string $scene = 'index') { - if (method_exists($this, 'complexQuery')) { - $result = $this->complexQuery($request->get()); - } else { - $config = $this->buildQueryConfig($request, 'index'); + $result = $this->cqyProcess(ucfirst($scene), $request); + if ($result === false) { + $config = $this->buildQueryConfig($request, $scene); $limit = (int)$request->get('size', 15); $page = (int)$request->get('current', 1); $result = $this->logicList($config, $limit, $page); } - return $this->postProcess('List', $result); + return $this->postProcess(ucfirst($scene), $result); } /** 全量列表 */ - public function serviceListAll(Request $request) + public function serviceListAll(Request $request, string $scene = 'default') { - if (method_exists($this, 'complexQuery')) { - $result = $this->complexQuery($request->get()); - } else { - $config = $this->buildQueryConfig($request, 'default'); + $result = $this->cqyProcess(ucfirst($scene), $request); + if ($result === false) { + $config = $this->buildQueryConfig($request, $scene); $result = $this->logicListAll($config); } - return $this->postProcess('ListAll', $result); + return $this->postProcess(ucfirst($scene), $result); } /** 查看单条 */ - public function serviceShow(Request $request) + public function serviceShow(Request $request, string $scene = 'show') { - $config = $this->buildQueryConfig($request, 'show'); + $config = $this->buildQueryConfig($request, $scene); $item = $this->logicShow($config); - return $this->postProcess('Show', $item); + return $this->postProcess(ucfirst($scene), $item); } /** 创建 */ - public function serviceStore(Request $request, array $data = []) + public function serviceStore(Request $request, array $data = [], string $scene = 'store') { - $data = $this->preProcess('Store', $data, $request); + $data = $this->preProcess(ucfirst($scene), $data, $request); $dl = $this->getDataLimitFromRequest($request); $created = $this->logicCreate($data, $dl); - return $this->postProcess('Store', $created); + return $this->postProcess(ucfirst($scene), $created); } /** 更新 */ - public function serviceUpdate(Request $request, array $data = []) + public function serviceUpdate(Request $request, array $data = [], string $scene = 'update') { - $data = $this->preProcess('Update', $data, $request); - $config = $this->buildQueryConfig($request, 'update'); + $data = $this->preProcess(ucfirst($scene), $data, $request); + $config = $this->buildQueryConfig($request, $scene); $updated = $this->logicUpdateBy($config, $data); - return $this->postProcess('Update', $data); + return $this->postProcess(ucfirst($scene), $data); } /** 删除 */ - public function serviceDestroy(Request $request) + public function serviceDestroy(Request $request, string $scene = 'destroy') { - $config = $this->buildQueryConfig($request, 'destroy'); + $config = $this->buildQueryConfig($request, $scene); $deleted = $this->logicDeleteBy($config); - return $this->postProcess('Destroy', $deleted); + return $this->postProcess(ucfirst($scene), $deleted); } /** ----------------- 公共辅助方法 ----------------- */ @@ -182,13 +180,18 @@ trait CrudServiceTrait : $data; } - - /** - * 处理复杂查询 - * 覆盖此方法,实现复杂查询逻辑 + /** + * 复杂/自定义查询 + * + * @param string $op 场景 + * @param mixed $data 原始返回值 + * @return mixed 处理后的返回值 */ - // protected function complexQuery(array $params): array - // { - // throw new BusinessException('请在子类实现 complexQuery'); - // } + protected function cqyProcess(string $op, Request $request): mixed + { + $hook = "complexQuery{$op}"; + return method_exists($this, $hook) + ? $this->{$hook}($request) + : false; + } } \ No newline at end of file diff --git a/app/common/traits/crud/CrudServiceTrait_bak.php b/app/common/traits/crud/CrudServiceTrait_bak.php new file mode 100644 index 0000000..4b2e012 --- /dev/null +++ b/app/common/traits/crud/CrudServiceTrait_bak.php @@ -0,0 +1,194 @@ +validator; + } + + /** 分页列表 */ + public function serviceList(Request $request) + { + if (method_exists($this, 'complexQuery')) { + $result = $this->complexQuery($request->get()); + } else { + $config = $this->buildQueryConfig($request, 'index'); + $limit = (int)$request->get('size', 15); + $page = (int)$request->get('current', 1); + $result = $this->logicList($config, $limit, $page); + } + return $this->postProcess('List', $result); + } + + /** 全量列表 */ + public function serviceListAll(Request $request) + { + if (method_exists($this, 'complexQuery')) { + $result = $this->complexQuery($request->get()); + } else { + $config = $this->buildQueryConfig($request, 'default'); + $result = $this->logicListAll($config); + } + return $this->postProcess('ListAll', $result); + } + + /** 查看单条 */ + public function serviceShow(Request $request) + { + $config = $this->buildQueryConfig($request, 'show'); + $item = $this->logicShow($config); + return $this->postProcess('Show', $item); + } + + /** 创建 */ + public function serviceStore(Request $request, array $data = []) + { + $data = $this->preProcess('Store', $data, $request); + $dl = $this->getDataLimitFromRequest($request); + $created = $this->logicCreate($data, $dl); + return $this->postProcess('Store', $created); + } + + /** 更新 */ + public function serviceUpdate(Request $request, array $data = []) + { + $data = $this->preProcess('Update', $data, $request); + $config = $this->buildQueryConfig($request, 'update'); + $updated = $this->logicUpdateBy($config, $data); + return $this->postProcess('Update', $data); + } + + /** 删除 */ + public function serviceDestroy(Request $request) + { + $config = $this->buildQueryConfig($request, 'destroy'); + $deleted = $this->logicDeleteBy($config); + return $this->postProcess('Destroy', $deleted); + } + + /** ----------------- 公共辅助方法 ----------------- */ + + /** + * 构造 CrudUtil 查询配置 + */ + protected function buildQueryConfig(Request $request, string $scene = 'default'): array + { + $p = $request->all(); + + $config = []; + $config['model'] = $this->modelClass; + switch ($scene) { + case 'default': + $config['where']['status'] = 1; + $config['order']['sort'] = 'desc'; + $config['order']['id'] = 'desc'; + break; + case 'index': + $config['where']['status'] = 1; + $config['order']['sort'] = 'desc'; + $config['order']['id'] = 'desc'; + break; + case 'show': + $config['where']['id'] = $p['id']; + $config['where']['status'] = 1; + break; + case 'store': + break; + case 'update': + $config['where']['id'] = $p['id']; + break; + case 'destroy': + $config['where']['id'] = $p['id']; + break; + } + + return $config; + } + + /** 解析 withFields 字符串 "rel:field,field;..." */ + protected function parseWithFields(string $str): array + { + if (!$str) { + return []; + } + $out = []; + foreach (explode(';', $str) as $pair) { + [$rel, $flds] = explode(':', $pair); + $out[$rel] = explode(',', $flds); + } + return $out; + } + + /** 从请求读取 dataLimitType, dataLimitField */ + protected function getDataLimitFromRequest(Request $req): array + { + $type = $req->get('dataLimitType'); + if (!$type) { + return []; + } + return [ + 'type' => $type, + 'field' => $req->get('dataLimitField', 'admin_id'), + ]; + } + + /** + * $data 前置处理 + * + * @param array $data + * @param Request $request + * @return array + */ + protected function preProcess(string $op, mixed $data, Request $request): mixed + { + $hook = "beforeService{$op}"; + return method_exists($this, $hook) + ? $this->{$hook}($data, $request) + : $data; + } + + /** + * 根据操作类型,调用对应的后置钩子 + * + * @param string $op 操作:List, Show, Store, Update, Destroy + * @param mixed $data 原始返回值 + * @return mixed 处理后的返回值 + */ + protected function postProcess(string $op, mixed $data): mixed + { + $hook = "afterService{$op}"; + return method_exists($this, $hook) + ? $this->{$hook}($data) + : $data; + } + + + /** + * 处理复杂查询 + * 覆盖此方法,实现复杂查询逻辑 + */ + // protected function complexQuery(array $params): array + // { + // throw new BusinessException('请在子类实现 complexQuery'); + // } +} \ No newline at end of file diff --git a/app/common/utils/MigrationResources.php b/app/common/utils/MigrationResources.php new file mode 100644 index 0000000..ce2f473 --- /dev/null +++ b/app/common/utils/MigrationResources.php @@ -0,0 +1,194 @@ +where($conditions); + + // 按排序字段排序 + $resources = $query->orderBy('sort')->orderBy('id')->get(); + // var_dump($resources->count()); + // 转换为数组 + $resourceList = []; + foreach ($resources as $resource) { + $resourceList[] = (array) $resource; + } + + // 构建树状结构 + return $this->buildTree($resourceList); + } + + /** + * 构建树状结构 + * + * @param array $resources 资源列表 + * @param int $parentId 父级ID + * @return array + */ + private function buildTree(array $resources, int $parentId = 0): array + { + $tree = []; + + foreach ($resources as $resource) { + if ($resource['parent_id'] == $parentId) { + // 递归获取子节点 + $children = $this->buildTree($resources, $resource['id']); + + // 如果有子节点,则添加到children字段 + if (!empty($children)) { + $resource['children'] = $children; + } + + $tree[] = $resource; + } + } + + return $tree; + } + + /** + * 使用引用方式构建树(性能优化版) + * + * @param array $conditions 筛选条件 + * @return array + */ + public function getFullResourceTreeOptimized($table = 'resources', array $conditions = []): array + { + // 查询所有资源数据 + $query = Db::table($table)->where($conditions); + + // 按排序字段排序 + $resources = $query->orderBy('sort')->orderBy('id')->get(); + + // 构建ID映射和引用关系 + $resourceMap = []; + $tree = []; + + // 初始化所有节点 + foreach ($resources as $resource) { + $resourceArray = (array) $resource; + $resourceMap[$resourceArray['id']] = $resourceArray; + } + + // 构建父子关系 + foreach ($resourceMap as $id => &$node) { + if ($node['parent_id'] == 0) { + // 根节点 + $tree[] = &$node; + } else { + // 子节点 + if (isset($resourceMap[$node['parent_id']])) { + if (!isset($resourceMap[$node['parent_id']]['children'])) { + $resourceMap[$node['parent_id']]['children'] = []; + } + $resourceMap[$node['parent_id']]['children'][] = &$node; + } else { + // 父节点不存在,作为根节点处理 + $tree[] = &$node; + } + } + } + unset($node); + + return $tree; + } + + /** + * 获取扁平化的资源列表 + * + * @param array $conditions 筛选条件 + * @return array + */ + public function getResourceList($table = 'resources', array $conditions = []): array + { + $query = Db::table($table)->where($conditions); + + // 按排序字段排序 + $resources = $query->orderBy('sort')->orderBy('id')->get(); + + // 转换为数组 + $resourceList = []; + foreach ($resources as $resource) { + $resourceList[] = (array) $resource; + } + + return $resourceList; + } + + + /** + * 递归插入资源树 + */ + private function insertResourceTree($table, $tree, $parentId, $keys = []) + { + $keys = ['system_id','name','desc','type','code','parent_id','url','icon','open_type','link', + 'is_hide','is_hide_tab','no_auth','is_home','instance','keep_alive','sort','status']; + foreach ($tree as $item) { + // 保存子节点 + $children = $item['children'] ?? []; + + // 过滤字段 + $filteredItem = []; + foreach ($item as $k => $v) { + if (in_array($k, $keys)) { + $filteredItem[$k] = $v; + } + } + + // 设置parent_id + $filteredItem['parent_id'] = $parentId; + + // 插入并获取新ID + $newId = Db::table($table)->insertGetId($filteredItem); + echo "插入节点: {$filteredItem['name']}, 新ID: $newId\n"; + + // 递归处理子节点 + if (!empty($children)) { + $this->insertResourceTree($table, $children, $newId, $keys); + } + } + } + + + public function do(string $from = 'resources_20250916_bak', string $to = 'resources_yuanshi', array $where = []) + { + try { + // 开启事务 + Db::beginTransaction(); + + // 查询迁移资源的树状结果集 + $list = $this->getFullResourceTreeOptimized($from, $where); + + // 入库字段 + $keys = ['system_id','name','desc','type','code','parent_id','url','icon','open_type','link', + 'is_hide','is_hide_tab','no_auth','is_home','instance','keep_alive','sort','status']; + + // 使用递归处理树结构 + $this->insertResourceTree($to, $list, 0, $keys); + + // 提交事务 + Db::commit(); + + echo "数据插入成功"; + + } catch (\Exception $e) { + // 回滚事务 + Db::rollBack(); + + echo "数据插入失败: " . $e->getMessage() . " 在文件 " . $e->getFile() . " 第 " . $e->getLine() . " 行"; + } + } +} \ No newline at end of file diff --git a/app/common/utils/approval/service/ApprovalFlowService.php b/app/common/utils/approval/service/ApprovalFlowService.php new file mode 100644 index 0000000..ad6ba9c --- /dev/null +++ b/app/common/utils/approval/service/ApprovalFlowService.php @@ -0,0 +1,1135 @@ +where('type', $flowType)->where('is_active', 1)->first(); + if (!$flow) throw new BusinessException("审批流程未找到或已停用: {$flowType}"); + + $steps = json_decode($flow->steps, true); + if (!is_array($steps) || empty($steps)) throw new BusinessException("流程步骤配置错误"); + + // 找首个有效步骤索引(考虑条件) + $firstIndex = $this->getNextStepIndex($steps, $formData, -1); + if ($firstIndex === null) throw new BusinessException("未找到可执行的审批步骤"); + $firstStep = $steps[$firstIndex]; + + $now = date('Y-m-d H:i:s'); + $deadlineAt = null; + if (($flow->timeout_hours ?? 0) > 0) { + $deadlineAt = date('Y-m-d H:i:s', time() + $flow->timeout_hours * 3600); + } + + $instanceId = Db::table('approval_instances')->insertGetId([ + 'flow_type' => $flowType, + 'biz_type' => $bizType, + 'biz_id' => $bizId, + 'form_data' => json_encode($formData, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), + 'initiator_id' => $initiatorId, + 'status' => 'processing', + 'current_step' => $firstIndex, + 'deadline_at' => $deadlineAt, + 'created_at' => $now, + 'updated_at' => $now, + ]); + + $instance = (object)[ + 'id' => $instanceId, + 'flow_type' => $flowType, + 'form_data' => json_encode($formData, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), + 'initiator_id' => $initiatorId, + 'current_step' => $firstIndex, + 'status' => 'processing', + 'flow_type' => $flowType, + 'biz_type' => $bizType, + 'biz_id' => $bizId, + ]; + + // 解析首步审批人(支持 role_id 或 roles.name) + $approverIds = $this->resolveApprovers($firstStep, $instance, true); + foreach ($approverIds as $uid) { + $this->insertApprovalRecord($instanceId, $firstIndex, $uid); + } + + $this->sendNotification($approverIds, $instance, 'pending', false); + $this->notifyCCs($firstStep, $instance, 'pending'); + + return ['instance_id' => $instanceId]; + }); + } + + /** 执行审批(幂等友好) */ + public function approve(int $instanceId, int $approverId, string $action, string $comment = '') + { + if (!in_array($action, ['approve','reject'], true)) { + throw new BusinessException("无效的审批操作"); + } + + return Db::transaction(function () use ($instanceId, $approverId, $action, $comment) { + $instance = Db::table('approval_instances')->where('id', $instanceId)->lockForUpdate()->first(); + if (!$instance) throw new BusinessException("审批实例未找到"); + if ($instance->status !== 'processing') { + // return ['status' => 'noop', 'reason' => 'instance_not_processing', 'instance_status' => $instance->status]; + // throw new BusinessException("实例未处理"); + return ['status' => 'noop', 'msg' => '实例未处理']; + } + + $flow = Db::table('approval_flows')->where('type', $instance->flow_type)->first(); + if (!$flow) throw new BusinessException("审批流程定义未找到"); + + $steps = json_decode($flow->steps, true); + $idx = (int)$instance->current_step; + $step = $steps[$idx] ?? null; + if (!$step) throw new BusinessException("当前步骤配置错误"); + + // 找到该用户在本步的 pending 记录(加锁) + $pendingRecord = Db::table('approval_records') + ->where('instance_id', $instanceId) + ->where('approver_id', $approverId) + ->where('step_index', $idx) + ->where('status', 'pending') + ->lockForUpdate() + ->first(); + + if (!$pendingRecord) { + // 已处理过? + $anyRecord = Db::table('approval_records') + ->where('instance_id', $instanceId) + ->where('approver_id', $approverId) + ->where('step_index', $idx) + ->first(); + if ($anyRecord && $anyRecord->status !== 'pending') { + // return ['status' => 'noop', 'reason' => 'already_handled', 'record_status' => $anyRecord->status]; + // throw new BusinessException("已处理"); + return ['status' => 'noop', 'msg' => '已处理']; + } + // 步骤已推进 + if ((int)$instance->current_step !== $idx) { + // return ['status' => 'noop', 'reason' => 'not_current_step', 'current_step' => (int)$instance->current_step]; + // throw new BusinessException("非当前步骤"); + return ['status' => 'noop', 'msg' => '非当前审批']; + } + // return ['status' => 'noop', 'reason' => 'no_pending_record']; + return ['status' => 'noop', 'msg' => '无待处理记录']; + } + + $now = date('Y-m-d H:i:s'); + + // 1) 更新当前人的审批记录 + Db::table('approval_records')->where('id', $pendingRecord->id)->update([ + 'action' => $action, + 'status' => ($action === 'approve') ? 'approved' : 'rejected', + 'comment' => $comment, + 'approved_at' => $now, + 'updated_at' => $now, + ]); + + // 异步生成pdf文件 9.11 add + if ($action === 'approve') { + \Webman\RedisQueue\Redis::send('stock-order-pdf', ['id'=>$instance->biz_id]); + } + + // 2) 聚合本步结论(any / all) + $typeRaw = strtolower($step['type'] ?? 'all'); + $type = in_array($typeRaw, ['any','all']) ? $typeRaw : 'all'; + $outcome = $this->getStepOutcome($instanceId, $idx, $type); // 'pending' | 'approve' | 'reject' + + if ($outcome === 'pending') { + // return ['status' => 'step_pending']; + return ['status' => 'step_pending', 'msg' => '审批待定']; + } + + // 3) 有结论:关闭本步其余 pending + $this->closeOtherPending($instanceId, $idx, [$approverId]); + + // 4) 拒绝 => 结束 + if ($outcome === 'reject') { + Db::table('approval_instances')->where('id', $instanceId)->update([ + 'status' => 'rejected', + 'finished_at' => $now, + 'updated_at' => $now, + ]); + $this->sendNotification([$instance->initiator_id], $instance, 'reject', false); + // return ['status' => 'completed', 'result' => 'reject']; + return ['status' => 'completed', 'msg' => '已拒绝']; + } + + // 5) 通过 => 进入下一步或结束 + $formData = json_decode($instance->form_data, true) ?: []; + $nextIndex = $this->getNextStepIndex($steps, $formData, $idx); + + if ($nextIndex === null) { + Db::table('approval_instances')->where('id', $instanceId)->update([ + 'status' => 'approved', + 'finished_at' => $now, + 'updated_at' => $now, + ]); + $this->sendNotification([$instance->initiator_id], $instance, 'approve', false); + // return ['status' => 'completed', 'result' => 'approve']; + return ['status' => 'completed', 'msg' => '已同意']; + } + + // 乐观推进 current_step + $affected = Db::table('approval_instances') + ->where('id', $instanceId) + ->where('current_step', $idx) + ->where('status', 'processing') + ->update(['current_step' => $nextIndex, 'updated_at' => $now]); + if (!$affected) { + // return ['status' => 'noop', 'reason' => 'concurrent_progressed']; + return ['status' => 'noop', 'msg' => '并发进展']; + } + + // 生成下一步待办 + 通知 + $nextStep = $steps[$nextIndex]; + $nextApprovers = $this->resolveApprovers($nextStep, $instance, true); + foreach ($nextApprovers as $uid) { + $this->insertApprovalRecord($instanceId, $nextIndex, $uid); + } + $this->sendNotification($nextApprovers, $instance, 'pending', false); + $this->notifyCCs($nextStep, $instance, 'pending'); + + // return ['status' => 'next_step', 'step_name' => $nextStep['name'] ?? ('Step '.$nextIndex)]; + return ['status' => 'next_step', 'msg' => '进行下一步:'.$nextStep['name'] ?? ('Step '.$nextIndex)]; + }); + } + + /** 撤回(仅发起人在无已处理记录前可撤回) */ + public function revoke(int $instanceId, int $initiatorId, string $reason = '') + { + return Db::transaction(function () use ($instanceId, $initiatorId, $reason) { + $instance = Db::table('approval_instances')->where('id', $instanceId)->lockForUpdate()->first(); + if (!$instance) throw new BusinessException('审批实例未找到'); + if ((int)$instance->initiator_id !== (int)$initiatorId) throw new BusinessException('无权撤回'); + if ($instance->status !== 'processing') throw new BusinessException('流程已结束或不可撤回'); + + $handled = Db::table('approval_records') + ->where('instance_id', $instanceId) + ->whereIn('status', ['approved','rejected','timeout','skipped']) + ->count() > 0; + if ($handled) throw new BusinessException('当前进度不可撤回'); + + $now = date('Y-m-d H:i:s'); + Db::table('approval_instances')->where('id', $instanceId)->update([ + 'status' => 'revoked', + 'finished_at' => $now, + 'updated_at' => $now, + ]); + + Db::table('approval_records') + ->where('instance_id', $instanceId) + ->where('status', 'pending') + ->update(['status' => 'skipped', 'approved_at' => $now, 'updated_at' => $now]); + + $this->sendNotification([$initiatorId], $instance, 'revoked', false); + return ['status' => 'completed', 'result' => 'revoked']; + }); + } + + /** 流程图数据(把 skipped/timeout 视作已完结的中性状态) */ + public function getFlowDiagramData(int $instanceId): array + { + $instance = Db::table('approval_instances')->find($instanceId); + if (!$instance) return []; + + $flow = Db::table('approval_flows')->where('type', $instance->flow_type)->first(); + if (!$flow) return []; + + $steps = json_decode($flow->steps, true) ?: []; + $records = Db::table('approval_records') + ->where('instance_id', $instanceId) + ->orderBy('step_index')->orderByRaw('COALESCE(approved_at, created_at) asc') + ->get() + ->groupBy('step_index'); + + $formData = json_decode($instance->form_data, true) ?: []; + $data = []; + foreach ($steps as $i => $step) { + $actions = ($records->get($i) ?? collect())->toArray(); + + if (!empty($actions)) { + $cntApproved = 0; $cntRejected = 0; $cntPending = 0; + foreach ($actions as $a) { + if ($a->status === 'approved') $cntApproved++; + elseif ($a->status === 'rejected') $cntRejected++; + elseif ($a->status === 'pending') $cntPending++; + // skipped/timeout 不阻断完结显示 + } + if ($cntRejected > 0) $status = 'rejected'; + elseif ($cntPending > 0) $status = 'processing'; + elseif ($cntApproved > 0) $status = 'approved'; + else $status = 'approved'; // 全是 skipped/timeout 也视为已过 + } else { + if ($i < (int)$instance->current_step) { + if (isset($step['condition']) && !$this->evaluateCondition($step['condition'], $formData)) { + $status = 'skipped'; + } else { + $status = 'approved'; + } + } elseif ($i === (int)$instance->current_step && $instance->status === 'processing') { + $status = 'processing'; + } else { + $status = 'pending'; + } + } + + $data[] = [ + 'index' => $i, + 'name' => $step['name'] ?? ('Step '.$i), + 'type' => $step['type'] ?? 'all', + 'status' => $status, + 'actions' => $actions, + ]; + } + return $data; + } + + /** 定时任务:处理超时(实例级) */ + public function handleTimeouts() + { + $now = date('Y-m-d H:i:s'); + $expired = Db::table('approval_instances') + ->where('status', 'processing') + ->whereNotNull('deadline_at') + ->where('deadline_at', '<=', $now) + ->get(); + + foreach ($expired as $instance) { + Db::transaction(function () use ($instance, $now) { + $flow = Db::table('approval_flows')->where('type', $instance->flow_type)->first(); + if (!$flow) return; + + $final = ($flow->timeout_action === 'auto_approve') ? 'approved' : 'rejected'; + $finalAction = ($flow->timeout_action === 'auto_approve') ? 'approve' : 'reject'; + + Db::table('approval_instances')->where('id', $instance->id)->update([ + 'status' => $final, + 'finished_at' => $now, + 'updated_at' => $now, + ]); + + Db::table('approval_records') + ->where('instance_id', $instance->id) + ->where('step_index', $instance->current_step) + ->where('status', 'pending') + ->update(['status' => 'timeout', 'approved_at' => $now, 'updated_at' => $now]); + + $this->notifyInitiator($instance, $finalAction); + }); + } + } + + // ---------- 内部工具 ---------- + + /** 解析审批人(role/user/initiator_leader/field_user/dept_head) */ + private function resolveApprovers(array $step, object $instance, bool $avoidSelf = true): array + { + $resolved = []; + $formData = json_decode($instance->form_data ?? '{}', true) ?: []; + + $confs = $step['approvers'] ?? []; + foreach ($confs as $cfg) { + $typ = $cfg['type'] ?? ''; + $val = $cfg['value'] ?? null; + $ids = []; + + switch ($typ) { + case 'role': + $sys = isset($cfg['system_id']) ? (int)$cfg['system_id'] : null; + $includeGlobal = array_key_exists('include_global', $cfg) ? (bool)$cfg['include_global'] : true; + $ids = $this->getUserIdsByRole($val, $sys, true, $includeGlobal); + break; + case 'user': + $ids = [(int)$val]; + break; + case 'initiator_leader': + $leaderId = $this->getLeaderIdByUserId((int)$instance->initiator_id); + if ($leaderId) $ids = [(int)$leaderId]; + break; + case 'field_user': + if (isset($formData[$val])) $ids = [(int)$formData[$val]]; + break; + case 'dept_head': + $head = $this->getDeptHeadId((int)$val); + if ($head) $ids = [(int)$head]; + break; + default: + throw new BusinessException("不支持的审批人类型: {$typ}"); + } + $resolved = array_merge($resolved, $ids); + } + + $resolved = array_unique(array_filter(array_map('intval', $resolved))); + if ($avoidSelf) { + // $resolved = array_values(array_filter($resolved, fn($id) => $id !== (int)$instance->initiator_id)); + $resolved = array_values($resolved); + } + if (empty($resolved)) { + throw new BusinessException("未解析到任何审批人"); + } + return $resolved; + } + + /** 通知抄送人 */ + private function notifyCCs(array $step, object $instance, string $action = 'pending'): void + { + $ccConfig = $step['cc'] ?? []; + if (empty($ccConfig)) return; + + $formData = json_decode($instance->form_data ?? '{}', true) ?: []; + $uids = []; + + foreach ($ccConfig as $cc) { + $typ = $cc['type'] ?? ''; + $val = $cc['value'] ?? null; + + switch ($typ) { + case 'role': + $uids = array_merge($uids, $this->getUserIdsByRole($val, null, true, true)); + break; + case 'user': $uids[] = (int)$val; break; + case 'initiator_leader': + $leaderId = $this->getLeaderIdByUserId((int)$instance->initiator_id); + if ($leaderId) $uids[] = (int)$leaderId; + break; + case 'field_user': + if (isset($formData[$val])) $uids[] = (int)$formData[$val]; + break; + case 'dept_head': + $head = $this->getDeptHeadId((int)$val); + if ($head) $uids[] = (int)$head; + break; + default: break; + } + } + + $uids = array_unique(array_filter(array_map('intval', $uids))); + if (empty($uids)) return; + + $now = date('Y-m-d H:i:s'); + foreach ($uids as $uid) { + $exists = Db::table('approval_ccs') + ->where('instance_id', $instance->id) + ->where('user_id', $uid) + ->count() > 0; + if (!$exists) { + Db::table('approval_ccs')->insert([ + 'instance_id' => $instance->id, + 'user_id' => $uid, + 'notified_at' => $now, + 'created_at' => $now, + 'updated_at' => $now, + ]); + } + } + + $this->sendNotification($uids, $instance, $action, true); + } + + /** 条件求值(简单表达式) */ + private function evaluateCondition(?string $condition, array $formData): bool + { + if (!$condition) return true; + $pattern = '/^(\w+)\s*(==|!=|>|<|>=|<=)\s*(.+)$/'; + if (!preg_match($pattern, $condition, $m)) { + throw new BusinessException("条件格式错误: {$condition}"); + } + $field = $m[1]; $op = $m[2]; $value = trim($m[3], "\"'"); + $actual = $formData[$field] ?? null; + if (is_numeric($value) && is_numeric($actual)) { $value=(float)$value; $actual=(float)$actual; } + return match ($op) { + '==' => $actual == $value, + '!=' => $actual != $value, + '>' => $actual > $value, + '<' => $actual < $value, + '>=' => $actual >= $value, + '<=' => $actual <= $value, + default => throw new BusinessException("不支持的操作符: {$op}"), + }; + } + + /** 获取下一个有效步骤索引(考虑 condition) */ + private function getNextStepIndex(array $steps, array $formData, int $currentStepIndex): ?int + { + for ($i = $currentStepIndex + 1; $i < count($steps); $i++) { + $s = $steps[$i]; + if (!isset($s['condition']) || $this->evaluateCondition($s['condition'], $formData)) { + return $i; + } + } + return null; + } + + /** + * 聚合本步骤结论(按类型) + * 返回:'pending' | 'approve' | 'reject' + * + * any:有 reject => reject;否则有 approve => approve;否则 pending + * all:有 reject => reject;有 pending => pending;否则(全 approved 或 approved+skipped/timeout)=> approve + */ + private function getStepOutcome(int $instanceId, int $stepIndex, string $type): string + { + $records = Db::table('approval_records') + ->where('instance_id', $instanceId) + ->where('step_index', $stepIndex) + ->get(); + + $pending = 0; $approved = 0; $rejected = 0; + foreach ($records as $r) { + if ($r->status === 'pending') { $pending++; } + elseif ($r->status === 'approved'){ $approved++; } + elseif ($r->status === 'rejected'){ $rejected++; } + // skipped/timeout 不影响聚合结论 + } + + $type = strtolower($type); + if ($type === 'any') { + if ($rejected > 0) return 'reject'; + if ($approved > 0) return 'approve'; + return 'pending'; + } + // all + if ($rejected > 0) return 'reject'; + if ($pending > 0) return 'pending'; + return $approved > 0 ? 'approve' : 'pending'; + } + + /** 关闭本步其余待办为 skipped */ + private function closeOtherPending(int $instanceId, int $stepIndex, array $keepApproverIds = []): void + { + $now = date('Y-m-d H:i:s'); + $q = Db::table('approval_records') + ->where('instance_id', $instanceId) + ->where('step_index', $stepIndex) + ->where('status', 'pending'); + + if (!empty($keepApproverIds)) $q->whereNotIn('approver_id', $keepApproverIds); + + $q->update(['status'=>'skipped','approved_at'=>$now,'updated_at'=>$now]); + } + + /** 创建待办(去重) */ + private function insertApprovalRecord(int $instanceId, int $stepIndex, int $approverId): void + { + $now = date('Y-m-d H:i:s'); + $exists = Db::table('approval_records') + ->where('instance_id', $instanceId) + ->where('step_index', $stepIndex) + ->where('approver_id', $approverId) + ->count() > 0; + if (!$exists) { + Db::table('approval_records')->insert([ + 'instance_id' => $instanceId, + 'approver_id' => $approverId, + 'step_index' => $stepIndex, + 'action' => null, + 'status' => 'pending', + 'comment' => null, + 'approved_at' => null, + 'created_at' => $now, + 'updated_at' => $now, + ]); + } + } + + /** 发送通知(接你自己的消息系统即可) */ + private function sendNotification(array $userIds, object $instance, string $action, bool $isCC): void + { + $map = [ + 'pending' => '待审批', + 'approve' => '已通过', + 'reject' => '已拒绝', + 'revoked' => '已撤回' + ]; + $prefix = $isCC ? '【抄送】' : '【审批】'; + $text = $map[$action] ?? $action; + + $approvalType = array_flip(\app\common\service\SysDictService::pluckKeyValueByCode('approval_type')); + + switch ($instance->flow_type) { + case 'stock_orders': // 备货单 + $stockOrder = \app\common\model\StockOrders::find($instance->biz_id); + $title = $prefix."备货单:{$stockOrder->order_no}"; + $flowType = $approvalType[$instance->flow_type]; + $content = "{$prefix}你有一条审批 {$text}:类型 {$flowType},业务单号 {$stockOrder->order_no}"; + break; + case 'stock_orders_nullify': // 备货单作废 + break; + } + + noticePush([ + 'title' => $title, + 'content' => $content, + 'recipient' => $userIds, + 'email' => 1 + ]); + } + + private function notifyInitiator(object $instance, string $result): void + { + $this->sendNotification([(int)$instance->initiator_id], $instance, $result, false); + } + + // ---------- 数据访问(roles / user_roles 结构) ---------- + + /** + * 从角色取用户ID + * @param mixed $roleIdOrName 数字 => role_id;字符串 => roles.name + * @param int|null $systemId 限定 roles.system_id;null 不限定 + * @param bool $onlyActive 仅取 status=1 角色,默认 true + * @param bool $includeGlobal 是否包含全局角色(system_id=0),默认 true + */ + private function getUserIdsByRole($roleIdOrName, ?int $systemId = null, bool $onlyActive = true, bool $includeGlobal = true): array + { + if (is_int($roleIdOrName) || (is_string($roleIdOrName) && ctype_digit($roleIdOrName))) { + $roleId = (int)$roleIdOrName; + $needJoin = $onlyActive || !is_null($systemId) || $includeGlobal === false; + + if (!$needJoin) { + $rows = Db::table('user_roles')->where('role_id', $roleId)->select('user_id')->get(); + } else { + $q = Db::table('user_roles as ur') + ->join('roles as r', 'ur.role_id', '=', 'r.id') + ->where('ur.role_id', $roleId) + ->select('ur.user_id'); + if ($onlyActive) $q->where('r.status', 1); + if (!is_null($systemId)) { + $includeGlobal ? $q->whereIn('r.system_id', [0, $systemId]) : $q->where('r.system_id', $systemId); + } + $rows = $q->get(); + } + } else { + $name = (string)$roleIdOrName; + $q = Db::table('user_roles as ur') + ->join('roles as r', 'ur.role_id', '=', 'r.id') + ->where('r.name', $name) + ->select('ur.user_id'); + if ($onlyActive) $q->where('r.status', 1); + if (!is_null($systemId)) { + $includeGlobal ? $q->whereIn('r.system_id', [0, $systemId]) : $q->where('r.system_id', $systemId); + } + $rows = $q->get(); + } + + $ids = []; + foreach ($rows as $row) $ids[] = (int)$row->user_id; + return array_values(array_unique($ids)); + } + + private function getLeaderIdByUserId(int $userId): ?int + { + $id = Db::table('user')->where('id', $userId)->value('report_to'); + return $id ? (int)$id : null; + } + + private function getDeptHeadId(int $deptId): ?int + { + $id = Db::table('departments')->where('id', $deptId)->value('head_user_id'); + return $id ? (int)$id : null; + } + + + /** + * 查询某个审批实例的审批记录(附带步骤名/类型,可选带出审批人姓名) + * + * @param int $instanceId + * @param string|null $userNameColumn 可选:如 'name' 或 'nickname'。传 null 则不 join 用户表 + * @return array{ + * instance: array, + * steps: array, + * records: array, + * summary: array{approved:int,rejected:int,pending:int,skipped:int,timeout:int,total:int} + * } + */ + public function getInstanceRecords(int $instanceId, ?string $userNameColumn = null): array + { + // 1) 实例 + 流程步骤 + $instance = Db::table('approval_instances')->where('id', $instanceId)->first(); + if (!$instance) { + throw new BusinessException('审批实例未找到'); + } + $flow = Db::table('approval_flows')->where('type', $instance->flow_type)->first(); + if (!$flow) { + throw new BusinessException('审批流程定义未找到'); + } + $steps = json_decode($flow->steps, true) ?: []; + + // 2) 记录查询(可选带出审批人姓名) + if ($userNameColumn) { + // 注意:确保 user 表存在该列 + $records = Db::table('approval_records as ar') + ->leftJoin('user as u', 'ar.approver_id', '=', 'u.id') + ->where('ar.instance_id', $instanceId) + ->orderBy('ar.step_index') + ->orderByRaw('COALESCE(ar.approved_at, ar.created_at) asc') + ->select( + 'ar.*', + Db::raw("u.`{$userNameColumn}` as approver_name") + ) + ->get() + ->toArray(); + } else { + $records = Db::table('approval_records as ar') + ->where('ar.instance_id', $instanceId) + ->orderBy('ar.step_index') + ->orderByRaw('COALESCE(ar.approved_at, ar.created_at) asc') + ->select('ar.*') + ->get() + ->toArray(); + } + + // 3) 附加步骤名/类型;统计汇总 + $summary = ['approved'=>0,'rejected'=>0,'pending'=>0,'skipped'=>0,'timeout'=>0,'total'=>0]; + foreach ($records as &$r) { + $idx = (int)$r->step_index; + $meta = $steps[$idx] ?? []; + $r->step_name = $meta['name'] ?? ('Step '.$idx); + $r->step_type = $meta['type'] ?? 'all'; + + if (isset($summary[$r->status])) { + $summary[$r->status]++; + } + $summary['total']++; + } + unset($r); + + return [ + 'instance' => [ + 'id' => (int)$instance->id, + 'flow_type' => $instance->flow_type, + 'biz_type' => $instance->biz_type, + 'biz_id' => $instance->biz_id, + 'status' => $instance->status, + 'current_step' => (int)$instance->current_step, + 'created_at' => $instance->created_at, + 'updated_at' => $instance->updated_at, + 'finished_at' => $instance->finished_at, + ], + 'steps' => array_map(function($s){ + return [ + 'name' => $s['name'] ?? '', + 'type' => $s['type'] ?? 'all', + 'condition' => $s['condition'] ?? null, + ]; + }, $steps), + 'records' => array_map(function($r){ + // 转成数组(Illuminate 返回的是 stdClass) + return [ + 'id' => (int)$r->id, + 'instance_id' => (int)$r->instance_id, + 'approver_id' => (int)$r->approver_id, + 'step_index' => (int)$r->step_index, + 'action' => $r->action, + 'status' => $r->status, + 'comment' => $r->comment, + 'approved_at' => $r->approved_at, + 'created_at' => $r->created_at, + 'updated_at' => $r->updated_at, + 'approver_name'=> property_exists($r, 'approver_name') ? $r->approver_name : null, + 'step_name' => $r->step_name, + 'step_type' => $r->step_type, + ]; + }, $records), + 'summary' => $summary, + ]; + } + + /** + * 极简:审批实例的时间轴(只返回每一步的总状态) + * + * @param int $instanceId + * @return array + */ + public function getSimpleTimeline1(int $instanceId): array + { + // 基础数据 + $instance = Db::table('approval_instances')->where('id', $instanceId)->first(); + if (!$instance) throw new BusinessException('审批实例未找到'); + $flow = Db::table('approval_flows')->where('type', $instance->flow_type)->first(); + if (!$flow) throw new BusinessException('审批流程定义未找到'); + + $steps = json_decode($flow->steps, true) ?: []; + $formData = json_decode($instance->form_data ?? '{}', true) ?: []; + $currentStep = (int)$instance->current_step; + + // 读出所有记录并分组 + $records = Db::table('approval_records as ar') + ->where('ar.instance_id', $instanceId) + ->orderBy('ar.step_index') + ->orderByRaw('COALESCE(ar.approved_at, ar.created_at) asc') + ->select('ar.*') + ->get() + ->toArray(); + + $byStep = []; + foreach ($records as $r) { + $idx = (int)$r->step_index; + $byStep[$idx][] = $r; + } + + $timeline = []; + + foreach ($steps as $i => $meta) { + $name = $meta['name'] ?? ('Step '.$i); + $typeRaw = strtolower($meta['type'] ?? 'all'); + $type = in_array($typeRaw, ['any','all']) ? $typeRaw : 'all'; + $condStr = $meta['condition'] ?? null; + $condOk = $condStr ? $this->evaluateCondition($condStr, $formData) : true; + + $recs = $byStep[$i] ?? []; + $hasRecs = !empty($recs); + + // 默认状态 + $status = 'pending'; + + if ($hasRecs) { + // 统计是否全部 timeout(且无 approve/reject/pending) + $cnt = ['pending'=>0,'approved'=>0,'rejected'=>0,'timeout'=>0]; + foreach ($recs as $rr) { + if (isset($cnt[$rr->status])) $cnt[$rr->status]++; + } + + // 用已有的聚合规则拿到结论 + $outcome = $this->getStepOutcome($instanceId, $i, $type); // 'pending'|'approve'|'reject' + + if ($outcome === 'reject') { + $status = 'rejected'; + } elseif ($outcome === 'approve') { + $status = 'approved'; + } else { + // 没有结论,再看是不是纯超时 + if ($cnt['timeout'] > 0 && $cnt['approved'] === 0 && $cnt['rejected'] === 0 && $cnt['pending'] === 0) { + $status = 'timeout'; + } else { + $status = 'processing'; + } + } + } else { + // 无记录:根据位置与条件判断展示 + if ($i < $currentStep) { + $status = $condOk ? 'approved' : 'skipped'; + } elseif ($i === $currentStep && $instance->status === 'processing') { + // 理论上当前步应有记录;防御性展示为 processing + $status = 'processing'; + } else { + $status = $condOk ? 'pending' : 'skipped'; + } + } + + $timeline[] = [ + 'index' => (int)$i, + 'name' => $name, + 'type' => $type, // any | all + 'status' => $status, // approved | rejected | processing | pending | skipped | timeout + ]; + } + + return $timeline; + } + + + /** + * 极简时间轴(含每步时间 & 申请人信息) + * + * @param int $instanceId + * @param string|null $userNameColumn 申请人姓名列(如 'name' / 'nickname')。不需要就传 null + * @param string $initiatorPlacement 'prepend' | 'attach' + * - 'prepend':在最前插入一条“发起申请”块(推荐) + * - 'attach' :把申请人信息并到第一步的返回项里(不新增块) + * + * 返回:按显示顺序的数组,每项: + * { + * index:int // 步骤索引;若为“发起申请”块则为 -1 + * name:string // 步骤名或“发起申请” + * type:string // any|all 或 '-'(发起块) + * status:string // approved|rejected|processing|pending|skipped|timeout|submitted(发起块) + * started_at:?string + * finished_at:?string + * duration_seconds:?int + * // 仅发起块或 attach 时的第一步会包含: + * initiator_id?:int + * initiator_name?:?string + * } + */ + public function getSimpleTimeline(int $instanceId, ?string $userNameColumn = null, string $initiatorPlacement = 'prepend'): array + { + // 基础数据 + $instance = Db::table('approval_instances')->where('id', $instanceId)->first(); + if (!$instance) throw new BusinessException('审批实例未找到'); + + $flow = Db::table('approval_flows')->where('type', $instance->flow_type)->first(); + if (!$flow) throw new BusinessException('审批流程定义未找到'); + + $steps = json_decode($flow->steps, true) ?: []; + $formData = json_decode($instance->form_data ?? '{}', true) ?: []; + $curStep = (int)$instance->current_step; + + // 申请人 + $initiatorId = (int)$instance->initiator_id; + $initiatorName = null; + if ($userNameColumn) { + $initiatorName = Db::table('user')->where('id', $initiatorId)->value($userNameColumn); + } + + // 审批记录 + $records = Db::table('approval_records as ar') + ->where('ar.instance_id', $instanceId) + ->orderBy('ar.step_index') + ->orderByRaw('COALESCE(ar.approved_at, ar.created_at) asc') + ->select('ar.*') + ->get() + ->toArray(); + + $byStep = []; + foreach ($records as $r) { + $idx = (int)$r->step_index; + $byStep[$idx][] = $r; + } + + $timeline = []; + + // 可选:先插入“发起申请”块(index = -1) + if ($initiatorPlacement === 'prepend') { + $appliedAt = $instance->created_at ?: null; + $timeline[] = [ + 'index' => -1, + 'name' => '发起申请', + 'type' => '-', + 'status' => 'submitted', + 'started_at' => $appliedAt, + 'finished_at' => $appliedAt, + 'duration_seconds' => 0, + 'initiator_id' => $initiatorId, + 'initiator_name' => $initiatorName, + ]; + } + + // 每一步 + foreach ($steps as $i => $meta) { + $name = $meta['name'] ?? ('Step '.$i); + $typeRaw = strtolower($meta['type'] ?? 'all'); + $type = in_array($typeRaw, ['any','all']) ? $typeRaw : 'all'; + $condStr = $meta['condition'] ?? null; + $condOk = $condStr ? $this->evaluateCondition($condStr, $formData) : true; + + $recs = $byStep[$i] ?? []; + $hasRecs = !empty($recs); + + $status = 'pending'; + $startedAt = null; + $finishedAt = null; + $durationSec = null; + + if ($hasRecs) { + // 统计时间 + $minCreated = null; + $minApproveAt = null; $maxApproveAt = null; + $minRejectAt = null; + $maxTimeoutAt = null; + $cnt = ['pending'=>0,'approved'=>0,'rejected'=>0,'timeout'=>0]; + + foreach ($recs as $rr) { + if ($rr->created_at && (!$minCreated || $rr->created_at < $minCreated)) $minCreated = $rr->created_at; + if ($rr->approved_at) { + if ($rr->status === 'approved') { + if (!$minApproveAt || $rr->approved_at < $minApproveAt) $minApproveAt = $rr->approved_at; + if (!$maxApproveAt || $rr->approved_at > $maxApproveAt) $maxApproveAt = $rr->approved_at; + } elseif ($rr->status === 'rejected') { + if (!$minRejectAt || $rr->approved_at < $minRejectAt) $minRejectAt = $rr->approved_at; + } elseif ($rr->status === 'timeout') { + if (!$maxTimeoutAt || $rr->approved_at > $maxTimeoutAt) $maxTimeoutAt = $rr->approved_at; + } + } + if (isset($cnt[$rr->status])) $cnt[$rr->status]++; + } + + $startedAt = $minCreated; + + // 总结果 + $outcome = $this->getStepOutcome($instanceId, $i, $type); // 'pending'|'approve'|'reject' + if ($outcome === 'reject') { + $status = 'rejected'; + $finishedAt = $minRejectAt; + } elseif ($outcome === 'approve') { + $status = 'approved'; + $finishedAt = ($type === 'any') ? $minApproveAt : $maxApproveAt; + } else { + if ($cnt['timeout'] > 0 && $cnt['approved'] === 0 && $cnt['rejected'] === 0 && $cnt['pending'] === 0) { + $status = 'timeout'; + $finishedAt = $maxTimeoutAt; + } else { + $status = 'processing'; + } + } + + if ($startedAt && $finishedAt) { + $durationSec = max(0, strtotime($finishedAt) - strtotime($startedAt)); + } + } else { + // 无记录:根据位置与条件展示 + if ($i < $curStep) { + $status = $condOk ? 'approved' : 'skipped'; + } elseif ($i === $curStep && $instance->status === 'processing') { + $status = 'processing'; + } else { + $status = $condOk ? 'pending' : 'skipped'; + } + } + + $row = [ + 'index' => (int)$i, + 'name' => $name, + 'type' => $type, + 'status' => $status, + 'started_at' => $startedAt, + 'finished_at' => $finishedAt, + 'duration_seconds' => $durationSec, + ]; + + // 如果需要把申请人信息“并到第一步”而不是独立一块 + if ($initiatorPlacement === 'attach' && empty($timeline)) { + $row['initiator_id'] = $initiatorId; + $row['initiator_name'] = $initiatorName; + } + + $timeline[] = $row; + } + + return $timeline; + } + + + /** + * 按业务ID获取“最新进展”的极简状态 + * - 已完结:直接返回实例最终状态 + * - 进行中:返回当前步骤(索引、名称、类型)与当前待处理审批者列表(pending) + * + * @param string $bizId + * @param string|null $bizType 可选限定业务类型 + * @param string|null $userNameColumn 可选:审批人姓名字段(如 user.name / nickname) + * @return array{ + * instance_id:int, flow_type:string, biz_type:string, biz_id:string, + * status:string, // 实例状态:approved|rejected|revoked|processing + * finished_at:?string, updated_at:?string, + * step?: array{ + * index:int, name:string, type:string, // any|all + * status:string, // processing|timeout(进行中场景) + * pending_approvers: array + * } + * } + */ + public function getBriefStatusByBiz(string $bizId, ?string $bizType = null, ?string $userNameColumn = null): array + { + // 1) 最新实例 + $q = Db::table('approval_instances')->where('biz_id', $bizId); + if ($bizType !== null && $bizType !== '') { + $q->where('biz_type', $bizType); + } + $instance = $q->orderByDesc('id')->first(); + if (!$instance) { + // throw new BusinessException('未找到对应的审批实例'); + return []; + } + + $base = [ + 'instance_id' => (int)$instance->id, + 'flow_type' => $instance->flow_type, + 'biz_type' => $instance->biz_type, + 'biz_id' => $instance->biz_id, + 'status' => $instance->status, + 'finished_at' => $instance->finished_at, + 'updated_at' => $instance->updated_at, + ]; + + // 2) 已完结:直接返回 + if ($instance->status !== 'processing') { + return $base; + } + + // 3) 进行中:取当前步骤 + 当前 pending 审批者 + $flow = Db::table('approval_flows')->where('type', $instance->flow_type)->first(); + if (!$flow) { + // 容错:没有流程定义 + return $base; + } + $steps = json_decode($flow->steps, true) ?: []; + $idx = (int)$instance->current_step; + $meta = $steps[$idx] ?? []; + + $typeRaw = strtolower($meta['type'] ?? 'all'); + $type = in_array($typeRaw, ['any','all']) ? $typeRaw : 'all'; + + // 当前步的所有记录 + $recsQ = Db::table('approval_records')->where('instance_id', $instance->id)->where('step_index', $idx); + + // 待处理人(只列出 pending) + if ($userNameColumn) { + $pendingRows = Db::table('approval_records as ar') + ->leftJoin('user as u', 'ar.approver_id', '=', 'u.id') + ->where('ar.instance_id', $instance->id) + ->where('ar.step_index', $idx) + ->where('ar.status', 'pending') + ->select('ar.approver_id', Db::raw("u.`{$userNameColumn}` as approver_name")) + ->get() + ->toArray(); + } else { + $pendingRows = Db::table('approval_records') + ->where('instance_id', $instance->id) + ->where('step_index', $idx) + ->where('status', 'pending') + ->select('approver_id') + ->get() + ->toArray(); + } + + $pendingApprovers = []; + foreach ($pendingRows as $r) { + $row = ['approver_id' => (int)$r->approver_id]; + if ($userNameColumn && property_exists($r, 'approver_name')) { + $row['approver_name'] = $r->approver_name; + } + $pendingApprovers[] = $row; + } + + // 当前步的聚合状态(进行中通常返回 processing;若全被超时任务置为 timeout,则返回 timeout) + // 先快速统计是否纯 timeout + $stats = Db::table('approval_records') + ->where('instance_id', $instance->id) + ->where('step_index', $idx) + ->selectRaw("SUM(CASE WHEN status='pending' THEN 1 ELSE 0 END) as c_pending") + ->selectRaw("SUM(CASE WHEN status='approved' THEN 1 ELSE 0 END) as c_approved") + ->selectRaw("SUM(CASE WHEN status='rejected' THEN 1 ELSE 0 END) as c_rejected") + ->selectRaw("SUM(CASE WHEN status='timeout' THEN 1 ELSE 0 END) as c_timeout") + ->first(); + + $stepStatus = 'processing'; + if ($stats && (int)$stats->c_timeout > 0 && (int)$stats->c_pending === 0 && (int)$stats->c_approved === 0 && (int)$stats->c_rejected === 0) { + $stepStatus = 'timeout'; + } + + $base['step'] = [ + 'index' => $idx, + 'name' => $meta['name'] ?? ('Step '.$idx), + 'type' => $type, // any|all + 'status' => $stepStatus, // processing|timeout + 'pending_approvers' => $pendingApprovers, // 当前正在等待的审批者 + ]; + + return $base; + } + + +} \ No newline at end of file diff --git a/app/common/utils/email/Emails.php b/app/common/utils/email/Emails.php index e910504..0742274 100644 --- a/app/common/utils/email/Emails.php +++ b/app/common/utils/email/Emails.php @@ -436,7 +436,7 @@ class Emails // 初始化 UID 轮询器 $uidRotator = new \app\common\utils\RedisUidRotator('email_parsed_result_20250804'); - $uidRotator->setUids([64, 63, 83, 93, 108, 132, 145, 150, 143, 156, 157, 158], false); + $uidRotator->setUids([64, 63, 93, 108, 132, 145, 150, 143, 156, 157, 158], false); EmailParsedResult::where('status', 0) ->chunkById($batchSize, function ($records) use ($delaySeconds, $throttleAfter, $throttleSeconds, $enterUser, $channelInfo, $uidRotator) { @@ -454,7 +454,7 @@ class Emails // 获取分配的用户信息 $recipientInfo = User::where('id', $assignedUid)->where('status', 1)->first(); if (!$recipientInfo) { - flg("用户ID=" . $assignedUid . " 不存在或已离职跳过分配,需要手动移除 UID"); + flg("用户ID=" . $assignedUid . " 已离职 跳过自动分配,需要手动移除 UID"); $assignedUid = $uidRotator->next(); $recipientInfo = User::where('id', $assignedUid)->where('status', 1)->first(); diff --git a/app/common/utils/pdf/controller/PdfController.php b/app/common/utils/pdf/controller/PdfController.php new file mode 100644 index 0000000..b647628 --- /dev/null +++ b/app/common/utils/pdf/controller/PdfController.php @@ -0,0 +1,76 @@ + '备货单', + 'order_no' => 'SO' . date('YmdHis'), + 'customer' => 'Magic Bridge Auto FZE', + ]); + + $driver = new MpdfDriver(); + $pdf = $driver->htmlToPdf($html, [ + // 若 HTML 里用到相对资源(img/css),给个基准目录 + 'asset_base' => public_path('assets/pdf'), + // 也可以单独传 CSS 文件(推荐把 CSS 放在 public/assets/pdf) + 'css_files' => [ + public_path('assets/pdf/stock-order.css'), + ], + 'header_html' => '
Company • Stock Order
', + 'footer_html' => '
{PAGENO} / {nbpg}
', + 'watermark_text' => '', // 'DRAFT' + ]); + + return new Response(200, [ + 'Content-Type' => 'application/pdf', + 'Content-Disposition' => 'inline; filename="stock-order.pdf"', + ], $pdf); + } + + // ② 直接下载 + public function download(Request $request): Response + { + $html = '

发票

客户:永生花

'; + $pdf = (new MpdfDriver())->htmlToPdf($html); + return new Response(200, [ + 'Content-Type' => 'application/pdf', + 'Content-Disposition' => 'attachment; filename="invoice.pdf"', + ], $pdf); + } + + // ③ 保存到本地(或上传 OSS 前先落盘) + public function save(Request $request): Response + { + $tpl = base_path('resource/templates/invoice.html'); + $html = PdfHelper::loadTemplate($tpl, [ + 'invoice_no' => 'INV' . date('YmdHis'), + 'amount' => '12345.67', + ]); + + $pdf = (new MpdfDriver())->htmlToPdf($html, [ + 'css_files' => [ public_path('assets/pdf/invoice.css') ], + 'asset_base'=> public_path('assets/pdf'), + ]); + + $dir = runtime_path('pdf'); + if (!is_dir($dir)) mkdir($dir, 0777, true); + $path = $dir . '/invoice-' . date('YmdHis') . '.pdf'; + file_put_contents($path, $pdf); + + return json(['path' => $path]); + } +} diff --git a/app/common/utils/pdf/service/MpdfDriver.php b/app/common/utils/pdf/service/MpdfDriver.php new file mode 100644 index 0000000..d59427a --- /dev/null +++ b/app/common/utils/pdf/service/MpdfDriver.php @@ -0,0 +1,118 @@ + 'A4', + 'orientation' => 'L', + 'margin_top' => 12, + 'margin_bottom' => 12, + 'margin_left' => 10, + 'margin_right' => 10, + 'tempDir' => runtime_path('mpdf'), + 'header_html' => '', + 'footer_html' => '', + 'watermark_text'=> '', + 'css_files' => [], + 'default_font' => 'notosanssc', + 'font_dirs' => [public_path('fonts')], + 'font_data' => [ + 'notosanssc' => ['R'=>'NotoSansSC-Regular.ttf','B'=>'NotoSansSC-Bold.ttf'], + ], + 'asset_base' => '', + 'enable_html5_parser' => true, + ], $options); + + $defaultConfig = (new ConfigVariables())->getDefaults(); + $defaultFontConfig = (new FontVariables())->getDefaults(); + + $fontDirs = array_merge($defaultConfig['fontDir'], $opts['font_dirs']); + $fontData = array_merge($defaultFontConfig['fontdata'], $opts['font_data']); + + $mpdf = new Mpdf([ + 'format' => $opts['format'], + 'orientation' => $opts['orientation'], + 'margin_left' => $opts['margin_left'], + 'margin_right' => $opts['margin_right'], + 'margin_top' => $opts['margin_top'], + 'margin_bottom' => $opts['margin_bottom'], + 'tempDir' => $opts['tempDir'], + 'fontDir' => $fontDirs, + 'fontdata' => $fontData, + 'default_font' => $opts['default_font'], + 'enable_html5_parser' => $opts['enable_html5_parser'], + ]); + + if ($opts['header_html']) $mpdf->SetHTMLHeader($opts['header_html']); + if ($opts['footer_html']) $mpdf->SetHTMLFooter($opts['footer_html']); + + if (!empty($opts['watermark_text'])) { + $mpdf->SetWatermarkText($opts['watermark_text']); + $mpdf->showWatermarkText = true; + $mpdf->watermarkTextAlpha = 0.08; + } + + // 资源绝对化(如果你有相对路径的图片等) + $html = PdfHelper::absolutizeAssetUrls($html, $opts['asset_base']); + + // === 关键改动:把 抽出来,作为 HEADER_CSS 写入 === + [$inlineCss, $cleanHtml] = $this->extractCssAndCleanHtml($html); + + // 先写外部 css_files + if (!empty($opts['css_files'])) { + foreach ($opts['css_files'] as $cssFile) { + if (is_file($cssFile)) { + $mpdf->WriteHTML(file_get_contents($cssFile), HTMLParserMode::HEADER_CSS); + } + } + } + // 再写内联 的内容拼成一段 CSS,并把这些 ,DOTALL 支持跨行 + if (preg_match_all('/]*>(.*?)<\/style>/is', $html, $m)) { + foreach ($m[1] as $block) { + // 去掉 HTML 注释,以防注释把 CSS 包起来 + $block = preg_replace('//', '', $block); + $cssParts[] = trim($block); + } + // 从 HTML 删除所有 + + +
+ +
+

合塑实业-汽车事业部-备货单(客户下定)

+
+ 备货单号: + 业务员: +
+
+ + + + + + + + + + + + + + + + + + + + + +
阿里订单号/PI号: 日期: 关目的国: 客户来源: 客户名称: 客户所在国:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 0): ?> + + + + + + + + + + + + + + + + + + +
编号车型数量/台颜色/外内饰改装官方指导价/二手挂牌价优惠单价总价利润
+ + 0): ?> + () + +
+ 合同汇率: + + 合计到岸价: +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
清关人: 全款收齐日: 预计发运日期: 出运方式: 国内收货港: 贸易条款/清关目的港: /提车日期: 最迟到港时间:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
计划收款信息序号汇款国日期币种账户汇款人金额
+ + +
+ +
+ +
+ 业务员签字: + + + + + +
+ +
+ 备发货负责人签字: + + + + + +
+
+ + +
+
+ 组长签字: + + + + + +
+ +
+ 日期: + +
+
+ +
+
+ + +
+ + \ No newline at end of file diff --git a/app/common/utils/pdf/template/stockOrder.stub b/app/common/utils/pdf/template/stockOrder.stub new file mode 100644 index 0000000..a11bfa1 --- /dev/null +++ b/app/common/utils/pdf/template/stockOrder.stub @@ -0,0 +1,319 @@ + + + + + 合塑实业-汽车事业部-备货单(客户下定) + + + +
+ +
+

合塑实业-汽车事业部-备货单(客户下定)

+
+ 备货单号: + 业务员: +
+
+ + + + + + + + + + + + + + + + + + + + + +
阿里订单号/PI号: 日期: 关目的国: 客户来源: 客户名称: 客户所在国:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 0): ?> + + + + + + + + + + + + + + + + + + +
编号车型数量/台颜色/外内饰改装官方指导价/二手挂牌价优惠单价总价利润
+ + 0): ?> + () + +
+ 合同汇率: + + 合计到岸价: +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
清关人: 全款收齐日: 预计发运日期: 出运方式: 国内收货港: 贸易条款/清关目的港: /提车日期: 最迟到港时间:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
计划收款信息序号汇款国日期币种账户汇款人金额
+ + +
+ +
+ +
+ 业务员签字: + + + + + +
+ +
+ 备发货负责人签字: + + + + + +
+
+ + +
+
+ 组长签字: + + + + + +
+ +
+ 日期: + +
+
+ +
+
+ + +
+ + \ No newline at end of file diff --git a/app/common/utils/pdf/template/stockOrder_bak.stub b/app/common/utils/pdf/template/stockOrder_bak.stub new file mode 100644 index 0000000..6b93ecc --- /dev/null +++ b/app/common/utils/pdf/template/stockOrder_bak.stub @@ -0,0 +1,255 @@ + + + + + 合塑实业-汽车事业部-备货单(客户下定) + + + +
+ +
+

合塑实业-汽车事业部-备货单(客户下定)

+
+ 备货单号: + 业务员: +
+
+ + + + + + + + + + + + + + + + + + + + + +
阿里订单号/PI号: 日期: 关目的国: 客户来源: 客户名称: 客户所在国:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 0): ?> + + + + + + + + + + + + + + + +
编号车型数量/台颜色/外内饰改装单价总价官方指导价/二手挂牌价优惠/二手减价
+ 合同汇率: + + 合计到岸价: +
+ + + + + + + + + + + + + + + + + + + + + +
清关人: 预计发运日期: 出运方式: 国内收货港: 贸易条款/清关目的港: /最迟到港时间:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
计划收款信息序号汇款国日期币种账户汇款人金额
+ +
+
+
+ 下单人: + +
+
+
下单时间:
+
+ +
+ + \ No newline at end of file diff --git a/app/common/utils/scaffold/Generate.php b/app/common/utils/scaffold/Generate.php index eb82025..2ed10a0 100644 --- a/app/common/utils/scaffold/Generate.php +++ b/app/common/utils/scaffold/Generate.php @@ -156,11 +156,11 @@ class Generate protected function deleteFileIfExists($path) { if (file_exists($path)) { - "文件: {$path} 已存在,核对后手动删除。\n"; - return; + // "文件: {$path} 已存在,核对后手动删除。\n"; + // return; - // unlink($path); - // echo "删除已存在文件: {$path} \n"; + unlink($path); + echo "删除已存在文件: {$path} \n"; } } diff --git a/app/common/utils/translation/config/trans.php b/app/common/utils/translation/config/trans.php new file mode 100644 index 0000000..83b9708 --- /dev/null +++ b/app/common/utils/translation/config/trans.php @@ -0,0 +1,34 @@ + [ + 'ticket_categorie' => [ + 'translation_model' => \app\ticket\model\CategorieTranslation::class, + 'foreign_key' => 'categorie_id', + ], + 'sys_currency' => [ + 'translation_model' => app\common\model\SysCurrencyTranslation::class, + 'foreign_key' => 'currency_id', + ], + 'sys_dict' => [ + 'translation_model' => app\common\model\SysDictTranslation::class, + 'foreign_key' => 'dict_id', + ], + 'sys_dict_item' => [ + 'translation_model' => app\common\model\SysDictItemTranslation::class, + 'foreign_key' => 'dict_item_id', + ], + 'sys_port_translation' => [ + 'translation_model' => app\common\model\SysPortTranslation::class, + 'foreign_key' => 'port_id', + ], + 'veh_color_translation' => [ + 'translation_model' => app\common\model\VehColorTranslation::class, + 'foreign_key' => 'color_id', + ], + ], + + // 默认支持的目标语种列表 + 'locales' => ['zh','en','ru','ar'], +]; diff --git a/app/queue/translation/jobs/TranslationJob.php b/app/common/utils/translation/jobs/TranslationJob.php similarity index 87% rename from app/queue/translation/jobs/TranslationJob.php rename to app/common/utils/translation/jobs/TranslationJob.php index eb5e704..ca584e2 100644 --- a/app/queue/translation/jobs/TranslationJob.php +++ b/app/common/utils/translation/jobs/TranslationJob.php @@ -1,8 +1,8 @@ config = getConfig('/queue/translation/config/trans.php'); + $this->config = getConfig('/common/utils/translation/config/trans.php'); +// $this->config = getConfig('/queue/translation/config/trans.php'); $this->payload = $payload; } diff --git a/app/queue/translation/queue/Translation.php b/app/common/utils/translation/queue/Translation.php similarity index 93% rename from app/queue/translation/queue/Translation.php rename to app/common/utils/translation/queue/Translation.php index 0b5841a..e9935f6 100644 --- a/app/queue/translation/queue/Translation.php +++ b/app/common/utils/translation/queue/Translation.php @@ -1,10 +1,10 @@ config = getConfig('/queue/translation/config/trans.php'); + $this->config = getConfig('/common/utils/translation/config/trans.php'); +// $this->config = getConfig('/queue/translation/config/trans.php'); } /** @@ -23,8 +24,9 @@ class TranslationService * @param string $sourceLocale 源语种,如 'zh' * @param array $sourceFields 源语种字段 ['name'=>…,'condition_desc'=>…] * @param array $clientFields 前端已传翻译 ['en'=>[...], …] + * @param array $intactFields 不需要翻译的字段 */ - public function queueTranslations(string $entityType, int $entityId, string $sourceLocale, array $sourceFields, array $clientFields = []): void + public function queueTranslations(string $entityType, int $entityId, string $sourceLocale, array $sourceFields, array $clientFields = [], array $intactFields = []): void { foreach ($this->config['locales'] as $locale) { $payload = [ diff --git a/app/queue/translation/service/TranslatorClient.php b/app/common/utils/translation/service/TranslatorClient.php similarity index 95% rename from app/queue/translation/service/TranslatorClient.php rename to app/common/utils/translation/service/TranslatorClient.php index 3abc1df..ee227da 100644 --- a/app/queue/translation/service/TranslatorClient.php +++ b/app/common/utils/translation/service/TranslatorClient.php @@ -1,5 +1,5 @@ [ + + ], + + 'update' => [ + + ] + ]; +} \ No newline at end of file diff --git a/app/common/validate/ApprovalInstancesValidate.php b/app/common/validate/ApprovalInstancesValidate.php new file mode 100644 index 0000000..f34dbd9 --- /dev/null +++ b/app/common/validate/ApprovalInstancesValidate.php @@ -0,0 +1,29 @@ + [ + + ], + + 'update' => [ + + ] + ]; +} \ No newline at end of file diff --git a/app/common/validate/ApprovalRecordsValidate.php b/app/common/validate/ApprovalRecordsValidate.php new file mode 100644 index 0000000..648e0c6 --- /dev/null +++ b/app/common/validate/ApprovalRecordsValidate.php @@ -0,0 +1,29 @@ + [ + + ], + + 'update' => [ + + ] + ]; +} \ No newline at end of file diff --git a/app/common/validate/StockOrderAccountsValidate.php b/app/common/validate/StockOrderAccountsValidate.php new file mode 100644 index 0000000..8c63773 --- /dev/null +++ b/app/common/validate/StockOrderAccountsValidate.php @@ -0,0 +1,29 @@ + [ + + ], + + 'update' => [ + + ] + ]; +} \ No newline at end of file diff --git a/app/common/validate/StockOrderItemOptionsValidate.php b/app/common/validate/StockOrderItemOptionsValidate.php new file mode 100644 index 0000000..4f3c522 --- /dev/null +++ b/app/common/validate/StockOrderItemOptionsValidate.php @@ -0,0 +1,29 @@ + [ + + ], + + 'update' => [ + + ] + ]; +} \ No newline at end of file diff --git a/app/common/validate/StockOrderItemsValidate.php b/app/common/validate/StockOrderItemsValidate.php new file mode 100644 index 0000000..501e690 --- /dev/null +++ b/app/common/validate/StockOrderItemsValidate.php @@ -0,0 +1,29 @@ + [ + + ], + + 'update' => [ + + ] + ]; +} \ No newline at end of file diff --git a/app/common/validate/StockOrdersValidate.php b/app/common/validate/StockOrdersValidate.php new file mode 100644 index 0000000..78fccf3 --- /dev/null +++ b/app/common/validate/StockOrdersValidate.php @@ -0,0 +1,29 @@ + [ + + ], + + 'update' => [ + + ] + ]; +} \ No newline at end of file diff --git a/app/common/validate/UploadedFilesValidate.php b/app/common/validate/UploadedFilesValidate.php new file mode 100644 index 0000000..339ff2b --- /dev/null +++ b/app/common/validate/UploadedFilesValidate.php @@ -0,0 +1,82 @@ + 'require|max:255', + 'file_path' => 'require|max:500', + 'file_size' => 'require|integer|gt:0', + 'file_type' => 'require|max:100', + 'category' => 'require|max:50', + 'original_name' => 'max:255', + 'extension' => 'max:20', + 'partition' => 'max:50', + 'upload_ip' => 'ip', + 'user_agent' => 'max:500', + ]; + + /** + * 验证消息 + */ + protected $message = [ + 'file_name.require' => '文件名不能为空', + 'file_name.max' => '文件名长度不能超过255个字符', + 'file_path.require' => '文件路径不能为空', + 'file_path.max' => '文件路径长度不能超过500个字符', + 'file_size.require' => '文件大小不能为空', + 'file_size.integer' => '文件大小必须是整数', + 'file_size.gt' => '文件大小必须大于0', + 'file_type.require' => '文件类型不能为空', + 'file_type.max' => '文件类型长度不能超过100个字符', + 'category.require' => '文件分类不能为空', + 'category.max' => '文件分类长度不能超过50个字符', + 'original_name.max' => '原始文件名长度不能超过255个字符', + 'extension.max' => '文件扩展名长度不能超过20个字符', + 'partition.max' => '分区名称长度不能超过50个字符', + 'upload_ip.ip' => '上传IP格式不正确', + 'user_agent.max' => '用户代理长度不能超过500个字符', + ]; + + /** + * 验证场景 + */ + protected $scene = [ + 'create' => ['file_name', 'file_path', 'file_size', 'file_type', 'category'], + 'update' => ['file_name', 'file_path', 'file_size', 'file_type', 'category', 'original_name', 'extension', 'partition'], + 'search' => ['category', 'partition', 'file_type'], + ]; + + /** + * 验证文件上传 + */ + public function sceneUpload() + { + return $this->append('file_name', 'require|max:255') + ->append('file_path', 'require|max:500') + ->append('file_size', 'require|integer|gt:0') + ->append('file_type', 'require|max:100') + ->append('category', 'require|max:50') + ->append('original_name', 'max:255') + ->append('extension', 'max:20') + ->append('partition', 'max:50') + ->append('upload_ip', 'ip') + ->append('user_agent', 'max:500'); + } + + /** + * 验证文件信息更新 + */ + public function sceneUpdateInfo() + { + return $this->append('file_name', 'require|max:255') + ->append('category', 'require|max:50') + ->append('original_name', 'max:255'); + } +} \ No newline at end of file diff --git a/app/common/validate/VehDictI18nValidate.php b/app/common/validate/VehDictI18nValidate.php new file mode 100644 index 0000000..cb65500 --- /dev/null +++ b/app/common/validate/VehDictI18nValidate.php @@ -0,0 +1,29 @@ + [ + + ], + + 'update' => [ + + ] + ]; +} \ No newline at end of file diff --git a/app/common/validate/VehDictMappingValidate.php b/app/common/validate/VehDictMappingValidate.php new file mode 100644 index 0000000..1b4bc7a --- /dev/null +++ b/app/common/validate/VehDictMappingValidate.php @@ -0,0 +1,29 @@ + [ + + ], + + 'update' => [ + + ] + ]; +} \ No newline at end of file diff --git a/app/common/validate/VehMoneyValueValidate.php b/app/common/validate/VehMoneyValueValidate.php new file mode 100644 index 0000000..185ffa4 --- /dev/null +++ b/app/common/validate/VehMoneyValueValidate.php @@ -0,0 +1,29 @@ + [ + + ], + + 'update' => [ + + ] + ]; +} \ No newline at end of file diff --git a/app/common/validate/VehVehicleListingValidate.php b/app/common/validate/VehVehicleListingValidate.php new file mode 100644 index 0000000..ffa9c83 --- /dev/null +++ b/app/common/validate/VehVehicleListingValidate.php @@ -0,0 +1,29 @@ + [ + + ], + + 'update' => [ + + ] + ]; +} \ No newline at end of file diff --git a/app/common/validate/VehVehicleSkuValidate.php b/app/common/validate/VehVehicleSkuValidate.php new file mode 100644 index 0000000..7305e71 --- /dev/null +++ b/app/common/validate/VehVehicleSkuValidate.php @@ -0,0 +1,128 @@ + 'max:40|alphaNum', + 'fingerprint' => 'length:64|alphaNum', + 'brand_code' => 'require|max:32', + 'brand_name' => 'require|max:64', + 'series_code' => 'max:64', + 'series_name' => 'max:128', + 'model_code' => 'max:64', + 'model_name' => 'require|max:128', + 'model_year' => 'require|number|between:1900,2100', + 'trim_code' => 'max:64', + 'trim_level_code' => 'max:64', + 'edition_code' => 'max:64', + 'trim' => 'max:128', + 'trim_level' => 'max:64', + 'edition_name' => 'max:64', + 'body_type_code' => 'max:32', + 'drive_type_code' => 'max:16', + 'motor_layout_code' => 'max:32', + 'fuel_type_code' => 'max:16', + 'engine_disp_l' => 'float|between:0,99.9', + 'engine_turbo' => 'in:0,1', + 'engine_power_kw' => 'number|between:0,2000', + 'battery_kwh' => 'float|between:0,999.99', + 'transmission_code' => 'max:16', + 'gears' => 'number|between:1,20', + 'range_km' => 'number|between:0,2000', + 'range_std_code' => 'max:16', + 'doors' => 'number|between:2,10', + 'seats' => 'number|between:1,20', + 'weight_kg' => 'number|between:0,10000', + 'dimensions_mm' => 'array', + 'color_exterior_code'=> 'max:32', + 'color_exterior_text'=> 'max:64', + 'color_interior_code'=> 'max:32', + 'color_interior_text'=> 'max:64', + 'options_official' => 'array', + 'features' => 'array', + 'emission_std_code' => 'max:16', + 'origin_country' => 'alpha|length:2', + 'currency' => 'require|alpha|length:3', + 'msrp' => 'float|between:0,9999999999.99', + 'is_active' => 'in:0,1', + 'corp_id' => 'require|number', + 'remark' => 'max:255', + 'created_by' => 'number', + 'updated_by' => 'number', + 'created_at' => 'require|number', + 'updated_at' => 'require|number', + ]; + + /** + * 字段别名 + */ + protected $field = [ + 'sku_code' => 'SKU编码', + 'fingerprint' => '规格指纹', + 'brand_code' => '品牌代码', + 'brand_name' => '品牌名称', + 'series_code' => '车系代码', + 'series_name' => '车系名称', + 'model_code' => '车型代码', + 'model_name' => '车型名称', + 'model_year' => '年款', + 'trim_code' => '版型代码', + 'trim_level_code' => '配置等级代码', + 'edition_code' => '特别版代码', + 'trim' => '版型/细分配置', + 'trim_level' => '配置等级', + 'edition_name' => '特别版名称', + 'body_type_code' => '车身类型', + 'drive_type_code' => '驱动方式', + 'motor_layout_code' => '电机布置', + 'fuel_type_code' => '能源类型', + 'engine_disp_l' => '排量(L)', + 'engine_turbo' => '是否涡轮', + 'engine_power_kw' => '最大功率(kW)', + 'battery_kwh' => '电池容量(kWh)', + 'transmission_code' => '变速箱代码', + 'gears' => '挡位数', + 'range_km' => '续航里程', + 'range_std_code' => '续航标准', + 'doors' => '门数', + 'seats' => '座位数', + 'weight_kg' => '整备质量', + 'dimensions_mm' => '车身尺寸', + 'color_exterior_code'=> '外观色代码', + 'color_exterior_text'=> '外观色名称', + 'color_interior_code'=> '内饰色代码', + 'color_interior_text'=> '内饰色名称', + 'options_official' => '官方选装', + 'features' => '功能配置', + 'emission_std_code' => '排放标准', + 'origin_country' => '原产国', + 'currency' => '币种', + 'msrp' => '建议零售价', + 'is_active' => '是否启用', + 'corp_id' => '公司ID', + 'remark' => '备注', + 'created_by' => '创建人', + 'updated_by' => '更新人', + 'created_at' => '创建时间', + 'updated_at' => '更新时间', + ]; + + /** + * 场景定义 + */ + protected $scene = [ + 'store' => [ + 'brand_code', 'brand_name', 'model_year','currency' + ], + 'update' => [ + 'id', 'brand_code','brand_name', 'model_year','currency' + ], + ]; +} \ No newline at end of file diff --git a/app/common/validate/VehVehicleUnitValidate.php b/app/common/validate/VehVehicleUnitValidate.php new file mode 100644 index 0000000..11f91a9 --- /dev/null +++ b/app/common/validate/VehVehicleUnitValidate.php @@ -0,0 +1,29 @@ + [ + + ], + + 'update' => [ + + ] + ]; +} \ No newline at end of file diff --git a/app/controller/IndexController.php b/app/controller/IndexController.php index ba87d98..b25b5a4 100644 --- a/app/controller/IndexController.php +++ b/app/controller/IndexController.php @@ -2,19 +2,13 @@ namespace app\controller; -use app\common\model\InquiryLog; -use app\common\model\User; -use app\common\model\UserRoles; -use app\common\model\UserSystems; -use app\common\service\MessageNotificationsService; -use app\common\traits\DataPermission; -use app\common\utils\email\MailSend; +use app\common\model\Resource; +use app\common\service\SysDictService; +use app\common\service\UploadService; use app\common\utils\scaffold\Generate; use support\Request; use support\Db; use support\Cache; -use app\inquiry\service\InquiryLogService; -use Illuminate\Support\Facades\Mail; class IndexController { @@ -77,26 +71,26 @@ class IndexController { $options = [ 'controller' => [ - 'module' => 'common', - 'name' => 'message_notifications' + 'module' => 'crm', + 'name' => 'approval_flows' ], 'model' => [ - 'module' => 'common', - 'name' => 'message_notifications' + 'module' => 'crm', + 'name' => 'approval_flows' ], 'service' => [ - 'module' => 'common', - 'name' => 'message_notifications' + 'module' => 'crm', + 'name' => 'approval_flows' ], 'logic' => [ - 'module' => 'common', - 'name' => 'message_notifications' + 'module' => 'crm', + 'name' => 'approval_flows' ], 'validate' => [ - 'module' => 'common', - 'name' => 'message_notifications' + 'module' => 'crm', + 'name' => 'approval_flows' ], - 'table' => 'message_notifications' + 'table' => 'approval_flows' ]; $result = (new Generate($options))->doGenerate(); @@ -104,45 +98,13 @@ class IndexController } - public function test() + public function test(Request $request) { + // 菜单迁移 + // $where[] = ['id', '>', 141]; + // $list = (new \app\common\utils\MigrationResources())->do('resources_20250916_bak', 'resources_yuanshi', $where); - // $data = [ - // 'sender_id' => 0, - // 'sender_type' => 'system', - // 'title' => '测试消息 - 标题', - // 'content' => '测试消息 - 内容', - // 'scene' => 1, - // 'recipient' => [1], - // ]; - - // $data['recipient'] = [1, 74]; - // // // 同步推送 - // // (new MessageNotificationsService())->pushNotice($data); - - - - // // 异步推送 - // \Webman\RedisQueue\Redis::send('notice', $data); - - - $info = InquiryLog::with(['channelInfo', 'countryInfo', 'enter'])->where('id', 1)->where('sync_crm', 0)->first(); - - var_dump(empty($info->recipient)); - - if ($info) { - var_dump(11); - } else { - var_dump(22); - } - - - if ($info !== NULL) { - var_dump(33); - } else { - var_dump(44); - } - - return json(['code' => 0, 'msg' => 'success']); + $list = (new \app\common\utils\MigrationResources())->getFullResourceTreeOptimized('resources_20250916_bak', []); + var_dump($list); } } \ No newline at end of file diff --git a/app/crm/controller/ApprovalFlowsController.php b/app/crm/controller/ApprovalFlowsController.php new file mode 100644 index 0000000..c97fa87 --- /dev/null +++ b/app/crm/controller/ApprovalFlowsController.php @@ -0,0 +1,33 @@ +service = new ApprovalFlowsService(); + } + + /** + * 列表页数据格式化 + * + * @param array $data + * @return array + */ + // protected function indexFormat($data) + // { + // if (isset($data['created_at'])) + // $data['created_at'] = date('Y-m-d H:i:s', $data['created_at']); + // if (isset($data['updated_at'])) + // $data['updated_at'] = date('Y-m-d H:i:s', $data['updated_at']); + // return $data; + // } +} \ No newline at end of file diff --git a/app/crm/controller/ApprovalInstancesController.php b/app/crm/controller/ApprovalInstancesController.php new file mode 100644 index 0000000..3643489 --- /dev/null +++ b/app/crm/controller/ApprovalInstancesController.php @@ -0,0 +1,33 @@ +service = new ApprovalInstancesService(); + } + + /** + * 列表页数据格式化 + * + * @param array $data + * @return array + */ + // protected function indexFormat($data) + // { + // if (isset($data['created_at'])) + // $data['created_at'] = date('Y-m-d H:i:s', $data['created_at']); + // if (isset($data['updated_at'])) + // $data['updated_at'] = date('Y-m-d H:i:s', $data['updated_at']); + // return $data; + // } +} \ No newline at end of file diff --git a/app/crm/controller/ApprovalRecordsController.php b/app/crm/controller/ApprovalRecordsController.php new file mode 100644 index 0000000..ca68d9f --- /dev/null +++ b/app/crm/controller/ApprovalRecordsController.php @@ -0,0 +1,143 @@ +service = new ApprovalRecordsService(); + } + + /** + * 列表页数据格式化 + * + * @param array $data + * @return array + */ + protected function indexFormat($data) + { + if (isset($data['records']) && count($data['records']) > 0) { + foreach ($data['records'] as $key => &$record) { + $result = [ + 'id' => $record['id'], + 'flows_name' => $record['approvalInstance']['flows']['name'], + + 'instance_id' => $record['instance_id'], + + 'biz_id' => $record['approvalInstance']['biz_id'], + 'biz_desc' => '备货单【'.$record['approvalInstance']['bizInfo']['order_no'].'】', + 'biz_type' => $record['approvalInstance']['flow_type'], + + 'initiator_name' => $record['approvalInstance']['initiator']['name'], + 'initiator_avatar' => $record['approvalInstance']['initiator']['avatar'], + + 'approver_name' => $record['approver']['name'], + 'approver_avatar' => $record['approver']['avatar'], + + 'step_index' => $record['step_index'], + 'action' => $record['action'], + 'action_desc' => SysDictItem::where('item_value', $record['action'])->where('status', 1)->value('item_label'), + 'status' => $record['status'], + 'status_desc' => SysDictItem::where('item_value', $record['status'])->where('status', 1)->value('item_label'), + 'comment' => $record['comment'], + 'approved_at' => $record['approved_at'], + + 'finished_at' => $record['approvalInstance']['finished_at'], + 'deadline_at' => $record['approvalInstance']['deadline_at'], + + 'created_at' => $record['created_at'] + ]; + $record = $result; + } + } + return $data; + } + + + public function approve(Request $request) + { + try { + $res = $this->service->serviceApprove($request); + return $this->success([], $res['msg']); + } catch (\Throwable $e) { + return $this->error($e->getMessage()); + } + } + + + public function logs(Request $request) + { + try { + $instanceId = $request->get('instance_id'); + $appro = new \app\common\utils\approval\service\ApprovalFlowService(); + $res = $appro->getSimpleTimeline($instanceId, 'name', 'prepend'); + return $this->success($res); + } catch (\Throwable $e) { + return $this->error($e->getMessage()); + } + } + + + + public function todo(Request $request) + { + try { + $raw = $this->service->serviceList($request, 'todo'); + $data = $this->transformData($raw, method_exists($this, 'todoFormat') + ? [$this, 'todoFormat'] + : null); + return $this->success($data); + } catch (\Throwable $e) { + return $this->error($e->getMessage()); + } + } + + protected function todoFormat($data) + { + if (isset($data['records']) && count($data['records']) > 0) { + foreach ($data['records'] as $key => &$record) { + $result = [ + 'id' => $record['id'], + 'flows_name' => $record['approvalInstance']['flows']['name'], + + 'instance_id' => $record['instance_id'], + + 'biz_id' => $record['approvalInstance']['biz_id'], + 'biz_desc' => '备货单号【'.$record['approvalInstance']['bizInfo']['order_no'].'】', + 'biz_type' => $record['approvalInstance']['flow_type'], + + 'initiator_name' => $record['approvalInstance']['initiator']['name'], + 'initiator_avatar' => $record['approvalInstance']['initiator']['avatar'], + + 'approver_name' => $record['approver']['name'], + 'approver_avatar' => $record['approver']['avatar'], + + 'step_index' => $record['step_index'], + 'action' => $record['action'], + 'action_desc' => SysDictItem::where('item_value', $record['action'])->where('status', 1)->value('item_label'), + 'status' => $record['status'], + 'status_desc' => SysDictItem::where('item_value', $record['status'])->where('status', 1)->value('item_label'), + 'comment' => $record['comment'], + 'approved_at' => $record['approved_at'], + + 'finished_at' => $record['approvalInstance']['finished_at'], + 'deadline_at' => $record['approvalInstance']['deadline_at'], + + 'created_at' => $record['created_at'] + ]; + $record = $result; + } + } + return $data; + } +} \ No newline at end of file diff --git a/app/crm/controller/CustomerController.php b/app/crm/controller/CustomerController.php index 88f6b6f..e10222c 100644 --- a/app/crm/controller/CustomerController.php +++ b/app/crm/controller/CustomerController.php @@ -205,6 +205,25 @@ class CustomerController extends BaseController } } + /** + * 客户下拉 + * @param Request $request + * @return \support\Response + */ + public function preset(Request $request) + { + try { + $param = $request->all(); + $param['user_id'] = $request->user->uid; + $param['corp_abbr'] = $request->basic_data['corp_abbr']; + $result = (new CustomerService())->preset($param); + + return $this->success($result); + } catch (\Throwable $e) { + return $this->error($e->getMessage()); + } + } + /** * 客户恢复 * @param Request $request @@ -327,7 +346,7 @@ class CustomerController extends BaseController 'address' => Util::getParam($request, 'address', ''), 'sex' => Util::getParam($request, 'sex', 0), 'obtain' => time(), - 'company' => Util::getParam($request, 'company', 0), + 'company' => Util::getParam($request, 'company', ''), ]; if (Util::isEmpty($id)) { diff --git a/app/crm/controller/StockOrderAccountsController.php b/app/crm/controller/StockOrderAccountsController.php new file mode 100644 index 0000000..4fe4338 --- /dev/null +++ b/app/crm/controller/StockOrderAccountsController.php @@ -0,0 +1,33 @@ +service = new StockOrderAccountsService(); + } + + /** + * 列表页数据格式化 + * + * @param array $data + * @return array + */ + // protected function indexFormat($data) + // { + // if (isset($data['created_at'])) + // $data['created_at'] = date('Y-m-d H:i:s', $data['created_at']); + // if (isset($data['updated_at'])) + // $data['updated_at'] = date('Y-m-d H:i:s', $data['updated_at']); + // return $data; + // } +} \ No newline at end of file diff --git a/app/crm/controller/StockOrderItemOptionsController.php b/app/crm/controller/StockOrderItemOptionsController.php new file mode 100644 index 0000000..e14daa4 --- /dev/null +++ b/app/crm/controller/StockOrderItemOptionsController.php @@ -0,0 +1,33 @@ +service = new StockOrderItemOptionsService(); + } + + /** + * 列表页数据格式化 + * + * @param array $data + * @return array + */ + // protected function indexFormat($data) + // { + // if (isset($data['created_at'])) + // $data['created_at'] = date('Y-m-d H:i:s', $data['created_at']); + // if (isset($data['updated_at'])) + // $data['updated_at'] = date('Y-m-d H:i:s', $data['updated_at']); + // return $data; + // } +} \ No newline at end of file diff --git a/app/crm/controller/StockOrderItemsController.php b/app/crm/controller/StockOrderItemsController.php new file mode 100644 index 0000000..8173f75 --- /dev/null +++ b/app/crm/controller/StockOrderItemsController.php @@ -0,0 +1,33 @@ +service = new StockOrderItemsService(); + } + + /** + * 列表页数据格式化 + * + * @param array $data + * @return array + */ + // protected function indexFormat($data) + // { + // if (isset($data['created_at'])) + // $data['created_at'] = date('Y-m-d H:i:s', $data['created_at']); + // if (isset($data['updated_at'])) + // $data['updated_at'] = date('Y-m-d H:i:s', $data['updated_at']); + // return $data; + // } +} \ No newline at end of file diff --git a/app/crm/controller/StockOrdersController.php b/app/crm/controller/StockOrdersController.php new file mode 100644 index 0000000..77f8586 --- /dev/null +++ b/app/crm/controller/StockOrdersController.php @@ -0,0 +1,295 @@ +service = new StockOrdersService(); + } + + /** + * 列表页数据格式化 + * + * @param array $data + * @return array + */ + protected function indexFormat($data) + { + if (isset($data['created_at'])) $data['created_at'] = date('Y-m-d H:i:s', $data['created_at']); + if (isset($data['updated_at'])) $data['updated_at'] = date('Y-m-d H:i:s', $data['updated_at']); + if (isset($data['eta_ship_date'])) $data['eta_ship_date'] = date('Y-m-d', $data['eta_ship_date']); + if (isset($data['biz_date'])) $data['biz_date'] = date('Y-m-d', $data['biz_date']); + if (isset($data['latest_arrival_date'])) $data['latest_arrival_date'] = date('Y-m-d', $data['latest_arrival_date']); + + if (isset($data['id'])) { + $appro = new \app\common\utils\approval\service\ApprovalFlowService(); + $appro = $appro->getBriefStatusByBiz($data['id'], null, 'name'); + $map = ['processing'=>'审批中','pending'=>'审批中','approved'=>'已通过','rejected'=>'已拒绝','revoked'=>'已撤回']; + if ($appro) { + $data['appro_status'] = [ + 'status' => $appro['status'], + 'status_desc' => $map[$appro['status']], + 'name' => isset($appro['step'])?$appro['step']['name']:'' + ]; + } + } + return $data; + } + + + protected function showFormat($data) + { + if (isset($data['created_at'])) $data['created_at'] = date('Y-m-d H:i:s', $data['created_at']); + if (isset($data['updated_at'])) $data['updated_at'] = date('Y-m-d H:i:s', $data['updated_at']); + if (isset($data['eta_ship_date'])) $data['eta_ship_date'] = date('Y-m-d', $data['eta_ship_date']); + if (isset($data['biz_date']) && $data['biz_date'] > 0) $data['biz_date'] = date('Y-m-d', $data['biz_date']); + if (isset($data['latest_arrival_date']) && $data['latest_arrival_date'] > 0) $data['latest_arrival_date'] = date('Y-m-d', $data['latest_arrival_date']); + if (isset($data['pick_up_date']) && $data['pick_up_date'] > 0) $data['pick_up_date'] = date('Y-m-d', $data['pick_up_date']); + if (isset($data['collect_all_date']) && $data['collect_all_date'] > 0) $data['collect_all_date'] = date('Y-m-d', $data['collect_all_date']); + + if (!empty($data['contract'])) { + $data['contract_url'] = (new \app\common\service\OssService)->generatePrivateUrl($data['contract']); + } + + if (count($data['moneyValues']) > 0) { + foreach ($data['moneyValues'] as $mvs) { + $data[$mvs['field']] = [ + 'exchange_rate' => $mvs['exchange_rate'], + 'amount' => $mvs['amount'], + 'currency' => $mvs['currency'], + 'id' => $mvs['id'] + ]; + } + } + + foreach ($data['items'] as &$item) { + foreach ($item['moneyValues'] as $mv) { + $item[$mv['field']] = [ + 'exchange_rate' => $mv['exchange_rate'], + 'amount' => $mv['amount'], + 'currency' => $mv['currency'], + 'id' => $mv['id'] + ]; + } + unset($item['moneyValues']); + + foreach ($item['options'] as $opt) { + $item['modify_list'][] = [ + "id" => $opt['id'], + "order_item_id" => $opt['order_item_id'], + "option_id" => $opt['option_id'], + "option_name_snapshot" => $opt['option_name_snapshot'], + "option_price" => $opt['option_price'] + ]; + } + unset($item['options']); + } + unset($item); + + + foreach ($data['accounts'] as $acc) { + $data['account_list'][] = [ + "id" => $acc['id'], + "order_id" => $acc['order_id'], + "seq_no" => $acc['seq_no'], + "remit_country_id" => $acc['remit_country_id'], + "remit_country_name_snapshot" => $acc['remit_country_name_snapshot'], + "remit_date" => date('Y-m-d', $acc['remit_date']), + "currency_code" => $acc['currency_code'], + "currency_name_snapshot" => $acc['currency_name_snapshot'], + "account_id" => $acc['account_id'], + "account_name_snapshot" => $acc['account_name_snapshot'], + "remitter_name" => $acc['remitter_name'], + "amount" => $acc['amount'] + ]; + } + unset($data['accounts']); + + $data['veh_list'] = $data['items']; + unset($data['items']); + return $data; + } + + + protected function printFormat($data) + { + $data['file_url_full'] = (!empty($data['file_url'])) ? (new \app\common\service\OssService)->generatePrivateUrl($data['file_url']) : ''; + return $data; + } + + /** + * 获取打印数据 + */ + public function print(Request $request) + { + try { + $raw = $this->service->servicePrint($request); + $data = $this->transformData($raw, method_exists($this, 'printFormat') + ? [$this, 'printFormat'] + : null); + return $this->success($data); + } catch (\Throwable $e) { + return $this->error($e->getMessage()); + } + } + + public function detail(Request $request) + { + try { + $raw = $this->service->serviceDetail($request); + $data = $this->transformData($raw, method_exists($this, 'detailFormat') + ? [$this, 'detailFormat'] + : null); + return $this->success($data); + } catch (\Throwable $e) { + return $this->error($e->getMessage()); + } + } + + protected function detailFormat($data) + { + // if (!isset($data['profit_total'])) $data['profit_total'] = 0.00; + return $data; + } + + /** + * 启动审批 + */ + public function approval(Request $request) + { + try { + $raw = $this->service->serviceApproval($request); + $data = $this->transformData($raw, method_exists($this, 'approvalFormat') + ? [$this, 'approvalFormat'] + : null); + return $this->success($data); + } catch (\Throwable $e) { + return $this->error($e->getMessage()); + } + } + + /** + * 最终审批 + */ + public function ultimateApproval(Request $request) + { + try { + $raw = $this->service->serviceUltimateApproval($request); + $data = $this->transformData($raw, method_exists($this, 'approvalFormat') + ? [$this, 'approvalFormat'] + : null); + return $this->success($data); + } catch (\Throwable $e) { + return $this->error($e->getMessage()); + } + } + + public function desc(Request $request) + { + + $data = [ + 'id' => '', //'主键', + 'order_no' => '', //'备货单编号(业务唯一)', + 'pi_no' => '', //'PI号'/'阿里订单号', + 'biz_date' => '', //'单据日期(时间戳)', + 'salesperson_id' => '', //'业务员ID(关联数据)', + 'salesperson_snapshot' => '', //'业务员姓名快照', + 'clearance_country_id' => '', //'清关目的国ID(关联数据)', + 'clearance_country_snapshot' => '', //'清关目的国名称快照', + // 'ali_order_no' => '', //'阿里订单号', + 'customer_source_id' => '', //'客户来源ID(关联数据)', + 'customer_source_snapshot' => '', //'客户来源名称快照', + 'customer_id' => '', //'客户ID(关联数据)', + 'customer_snapshot' => '', //'客户名称快照', + 'customer_country_id' => '', //'客户所在国ID(关联数据)', + 'customer_country_snapshot' => '', //'客户所在国名称快照', + 'declarant_name' => '', //'清关人(文本)', + 'eta_ship_date' => '', //'预计发运日(时间戳)', + 'shipping_method' => '', //'出运方式(如海运/空运/铁路等)', + 'cif_total_amount' => '', //'0.00' COMMENT '合计到岸价(CIF)', + 'domestic_port_id' => '', //'国内收货港ID(关联数据)', + 'domestic_port_snapshot' => '', //'国内收货港名称快照', + 'trade_term_id' => '', //'贸易条款ID(关联数据,EXW/FOB/CIF等)', + 'trade_term_snapshot' => '', //'贸易条款名称快照', + 'clearance_port_id' => '', //'清关目的港ID(关联数据)', + 'clearance_port_snapshot' => '', //'清关目的港名称快照', + 'latest_arrival_date' => '', //'最迟到港时间(时间戳)', + 'contract_exchange_rate' => '', //'合同汇率(下单锁定)', + 'settlement_currency' => '', //'CNY' COMMENT '结算币种', + 'remark' => '', //'备注', + // 'real_veh' => '', //'0' COMMENT '实车ID', + 'is_final' => '', //'0' COMMENT '是否终版1是0否', + 'status' => '', //'draft' COMMENT '单据状态:draft/submitted/approved/void/reversed等', + ]; + + // (?<=\x60 )[^\r\n']*(?=') + + $items = [ + // 'id' => '', // '主键', + // 'order_id' => '', // '所属备货单ID', + // 'product_code' => '', // '商品编号(可作外部编码)', + // 'sku_snapshot' => '', // '商品/车型名称快照', + // 'unit_price_value_id' => '', // '单价', + // 'handling_fee_value_id' => '', // '操作费', + // 'fob_transport_fee_value_id' => '', // 'FOB内陆运费', + // 'port_misc_fee_value_id' => '', // '港杂费', + // 'retrofit_fee_value_id' => '', // '改装费', + // 'total_price_value_id' => '', // '合计(按订单结算币计算)', + + 'vin' => '', // '车架号', + 'sku_id' => '', // '商品/车型ID(关联数据)', + 'model_snapshot' => '', // '车型(若与sku一致可复用;此处为显示快照)', + 'quantity' => '', // '1.000000' COMMENT '数量', + 'color_interior_id' => '', // '颜色内饰ID(关联数据)', + 'color_interior_snapshot' => '', // '颜色内饰名称快照', + 'color_exterior_id' => '', // '颜色外饰ID(关联数据)', + 'color_exterior_snapshot' => '', // '颜色外饰名称快照', + 'remark' => '', // '备注', + 'unit_price' => '', // '0.000000' COMMENT '单价(含/不含税按业务口径约定)', + 'handling_fee' => '', // '0.000000' COMMENT '手续费(按单车)', + 'fob_transport_fee' => '', // '0.000000' COMMENT 'FOB运输费(按单车)', + 'port_misc_fee' => '', // '0.000000' COMMENT '港杂(按单车)', + 'retrofit_fee' => '', // '0.000000' COMMENT '改装费(按单车)', + 'total_price' => '', // '0.000000' COMMENT '总价(quantity*unit_price,写时落地)', + 'official_price' => '', // 官方指导价 + 'discount_price' => '', // 优惠价 + + + 'modify_list' => [ + // 'order_item_id' => '', // '所属明细ID', + 'option_id' => '', // '配置项ID(关联数据)', + 'option_name_snapshot' => '', // '配置项名称快照', + 'option_price' => '', // '配置项价格(可选,写时快照)', + ] + ]; + + + $account = [ + 'order_id' => '', // '所属备货单ID', + // 'seq_no' => '', // '1' COMMENT '账户序号(行号)', + 'remit_country_id' => '', // '汇款国ID(关联数据)', + 'remit_country_name_snapshot' => '', // '汇款国名称快照', + 'remit_date' => '', // '汇款日期(时间戳)', + 'currency_code' => '', // 'USD' COMMENT '币种代码(如USD/CNY/AED等)', + 'currency_name_snapshot' => '', // '币种名称快照(可选)', + 'account_id' => '', // '账户ID(关联数据)', + 'account_name_snapshot' => '', // '账户名称/开户名快照', + 'remitter_name' => '', // '汇款人', + 'amount' => '', // '0.00' COMMENT '金额', + ]; + } +} \ No newline at end of file diff --git a/app/crm/model/Customer.php b/app/crm/model/Customer.php index 326eb72..92c070d 100644 --- a/app/crm/model/Customer.php +++ b/app/crm/model/Customer.php @@ -3,6 +3,7 @@ namespace app\crm\model; +use app\common\model\Country; use app\crm\utils\Util; use config\constants\Constants; use DateTimeInterface; @@ -106,4 +107,9 @@ class Customer extends Model return $this->save($options); }); } + + public function countryInfo() + { + return $this->hasOne(Country::class, 'id', 'country_id'); + } } \ No newline at end of file diff --git a/app/crm/service/CustomerService.php b/app/crm/service/CustomerService.php index c20ceab..c08b21d 100644 --- a/app/crm/service/CustomerService.php +++ b/app/crm/service/CustomerService.php @@ -164,6 +164,39 @@ class CustomerService return $customer; } + public function preset($params) + { + $keyword = $params['keyword'] ?? ''; + $where = []; + $user_id =$params['user_id']; + + $where[] = ['status', 1]; + $where[] = ['user_id', $user_id]; + + if (!empty($countryId)){ + $where[] = ['country_id', $countryId]; + } + + $query = Customer::select('id','code','sn','corp_id','email','phone','WhatsApp','contacts','company')->where($where); + + if (!Util::isEmpty($keyword)) { + $query->where(function ($query) use ($keyword) { + $query->where('contacts', 'like', "%{$keyword}%") + ->orWhere('phone', 'like', "%{$keyword}%") + ->orWhere('email', 'like', "%{$keyword}%") + ->orWhere('WhatsApp', 'like', "%{$keyword}%") + ->orWhere('code', 'like', "%{$keyword}%"); + }); + } + $customer = $query->orderBy('follow_last','desc')->get(); + + foreach ($customer as &$item) { + } + unset($item); + + return $customer; + } + /** 分页列表 */ public function serviceList(Request $request) diff --git a/app/crm/service/StockOrdersService.php b/app/crm/service/StockOrdersService.php new file mode 100644 index 0000000..9026af3 --- /dev/null +++ b/app/crm/service/StockOrdersService.php @@ -0,0 +1,762 @@ +validator; + } + + + protected function buildQueryConfig(Request $request, string $scene = 'index'): array + { + $p = $request->all(); + + $config = []; + $config['model'] = $this->modelClass; + switch ($scene) { + case 'index': + + // ====== ====== 筛选 ====== ====== + // 业务id + if (isset($p['salesperson_id']) && $p['salesperson_id']) { + $config['where']['salesperson_id'] = $p['salesperson_id']; + } + // 初、终版 + $config['where']['is_final'] = $p['is_final'] ?? 0; + // 关键字 + if (isset($p['keyword']) && $p['keyword']) { + $k = '%' . $p['keyword'] . '%'; + foreach (['pi_no', 'order_no', 'customer_snapshot'] as $col) { + $config['orWhere'][] = [$col, 'like', $k]; + } + } + + $config['order']['id'] = 'desc'; + + $config['where']['salesperson_id'] = $request->user->uid; + break; + case 'print': + case 'approval': + $config['where']['id'] = $p['id']; + break; + case 'detail': + case 'show': + $config['where']['id'] = $p['id']; + + $config['with'][] = 'items'; + $config['with'][] = 'items.options'; + $config['with'][] = 'items.moneyValues'; + $config['with'][] = 'accounts'; + $config['with'][] = 'moneyValues'; + $config['with'][] = 'salesperson'; + break; + case 'store': + break; + case 'update': + $config['where']['id'] = $p['id']; + break; + case 'destroy': + $config['where']['id'] = $p['id']; + break; + } + + return $config; + } + + public function beforeServiceStore($data, Request $request) + { + $userId = $request->user->uid; + + $data['order_no'] = generateStockNo(); + $data['biz_date'] = strtotime($data['biz_date']); + $data['eta_ship_date'] = strtotime($data['eta_ship_date']); + $data['latest_arrival_date'] = strtotime($data['latest_arrival_date']); + $data['salesperson_id'] = $userId; + $data['salesperson_snapshot'] = User::where('id', $userId)->value('name') ?? ''; + $data['approval_status'] = $data['approval_status'] == 1 ? 1 : 0; + + // if ($data['is_final'] == 1) { + $data['pick_up_date'] = $data['pick_up_date'] ? strtotime($data['pick_up_date']) : 0; + $data['collect_all_date'] = $data['collect_all_date'] ? strtotime($data['collect_all_date']) : 0; + // } + return $data; + } + + protected function afterServiceStore($res) + { + // 处理原单 + if ($res->is_final == 1 && $res->object_id > 0) { + $order = StockOrders::find($res->object_id); + $order->is_use = 1; + $order->object_id = $res->id; + if (!$order->save()) { + flg('备货单【'.$order->order_no.'】创建终版成功,但更新原单失败'); + } + } + // 启动审批 + if ($res->approval_status == 1) { + $this->startApproval($res); + } + return $res; + } + + /** + * 启动审批 + */ + public function startApproval($item) + { + // 启动审批 + $appro = new \app\common\utils\approval\service\ApprovalFlowService(); + $appro->start('stock_orders', 'stock_orders', $item->id, $item->toArray(), $item->salesperson_id); + // 异步生成pdf文件 + \Webman\RedisQueue\Redis::send('stock-order-pdf', ['id'=>$item->id]); + } + + /** + * 创建PDF文件 + */ + public function createdPdfFile($id) + { + $config = []; + $config['model'] = $this->modelClass; + $config['where']['id'] = $id; + $config['with'][] = 'items'; + $config['with'][] = 'items.options'; + $config['with'][] = 'items.moneyValues'; + $config['with'][] = 'accounts'; + $config['with'][] = 'moneyValues'; + $config['with'][] = 'salesperson'; + $item = $this->logicShow($config); + $pdfData = $this->pdfData($item); + + // 渲染模板为最终HTML + $tplPath = app_path('common/utils/pdf/template/stockOrder.stub'); + $html = \app\common\utils\pdf\service\PdfHelper::render($tplPath, ['data' => $pdfData]); + + // 生成PDF + $driver = new \app\common\utils\pdf\service\MpdfDriver(); + $pdfBin = $driver->htmlToPdf($html); + + // 存储 + $dir = public_path('pdf').'/'; + $fileName = ($pdfData['order_no'] ?? date('YmdHis')) . '.pdf'; + + // 本地保存 + \app\common\utils\pdf\service\PdfStorage::saveBinary( + $pdfBin, + $dir, + $fileName + ); + + // 上传云端 + $param = [ + 'path' => $dir . $fileName, + 'scene' => 'stock_orders', + 'category' => 'pdf', + 'original_name' => $fileName + ]; + $up = new \app\common\service\UploadService(); + $res = $up->uploadLocalFile($param); + + @unlink($dir . $fileName); + + // 更新文件地址 + $item->file_url = $res['file_name']; + $item->save(); + + return $res; + } + + public function pdfData($item) + { + $vehList = []; + $index = 0; + $totalProfit = 0; // 总利润 + + // $priceFields = [ + // 'official_price', // '官方指导价', + // 'discount_price', // '优惠', + // 'listing_price', // '二手挂牌价价', + // 'reduction_price', // '优惠', + + // 'handling_fee', // '手续费', + // 'retrofit_fee', // '改装费', + // 'fob_transport_fee', // 'FOB运输费', + // 'port_misc_fee', // '港杂', + // 'ocean_freight', // '海运费', + + // 'unit_price', // '单价', + // 'total_price', // '总价', + // // 'profit', // '利润' + // ]; + + foreach ($item->items as $k => $v) { + $currencyInfo = SysCurrency::where('currency_code', $item->settlement_currency)->first(['currency_symbol', 'currency_name'])->toArray(); + + // 存在则数量加1 + if (count($vehList) > 0 && isset($vehList[$v->unique_id])) { + $vehList[$v->unique_id]['num'] += 1; + $vehList[$v->unique_id]['total_price']['currency_info'] = $currencyInfo['currency_symbol'].number_format($vehList[$v->unique_id]['total_price']['amount']*$vehList[$v->unique_id]['num'], 2, '.', ','); + $vehList[$v->unique_id]['profit']['currency_info'] = $currencyInfo['currency_symbol'].number_format($vehList[$v->unique_id]['profit']['amount']*$vehList[$v->unique_id]['num'], 2, '.', ','); + continue; + } + + $index += 1; + + // 处理金额 (每次循环重新初始化) + $mv = []; + foreach ($v->moneyValues as $money) { + $mv[$money->field] = [ + 'currency' => $money->currency, + 'amount' => $money->amount, + 'rate' => $money->exchange_rate, + 'currency_info' => SysCurrency::where('currency_code', $money->currency)->value('currency_symbol').number_format($money->amount, 2, '.', ',') + ]; + } + + // 处理改装 (每次循环重新初始化) + $mfy = ''; + foreach ($v->options as $my) { + $mfy .= $my->option_name_snapshot.' '; + } + + // 计算单项利润并累加到总利润 + $itemProfit = $this->calculateProfit($mv, $item); + $totalProfit += $itemProfit; + + // 处理结果集 + $vehListItem = [ + 'index' => $index, + 'model_snapshot' => $v->model_snapshot, + 'color_interior_snapshot' => $v->color_interior_snapshot, + 'color_exterior_snapshot' => $v->color_exterior_snapshot, + 'num' => 1, + 'modify' => $mfy, + 'mileage' => $v->mileage + ]; + + $vehList[$v->unique_id] = $vehListItem + $mv; + } + + $mvs = []; + foreach ($item->moneyValues as $moneys) { + $mvs[$moneys->field] = [ + 'currency' => $moneys->currency, + 'amount' => $moneys->amount, + 'rate' => $moneys->exchange_rate, + 'currency_info' => SysCurrency::where('currency_code', $moneys->currency)->value('currency_symbol').number_format($moneys->amount, 2, '.', ','), + ]; + } + + $accList = []; + foreach ($item->accounts as $k => $v) { + $accList[] = [ + 'index' => '收款'.($k + 1), + 'remit_country_name_snapshot' => $v->remit_country_name_snapshot, + 'remit_date' => date('Y-m-d', $v->remit_date), + 'currency_name_snapshot' => $v->currency_name_snapshot, + 'account_name_snapshot' => $v->account_name_snapshot, + 'remitter_name' => $v->remitter_name, + 'amount' => $v->amount, + ]; + } + + + + $veh = [ + 'order_no' => $item->order_no, + 'salesperson_snapshot' => $item->salesperson_snapshot, + 'sign_img_url' => (!empty($item->salesperson->sign_img)) ? + (new \app\common\service\OssService)->generatePrivateUrl($item->salesperson->sign_img) : '', + 'created_at' => date('Y-m-d', strtotime($item->created_at)), + + 'pi_no' => $item->pi_no, + 'biz_date' => date('Y-m-d', $item->biz_date), + 'clearance_country_snapshot' => $item->clearance_country_snapshot, + 'customer_source_snapshot' => $item->customer_source_snapshot, + 'customer_snapshot' => $item->customer_snapshot, + 'customer_country_snapshot' => $item->customer_country_snapshot, + + // 订单项 + 'veh_list' => array_values($vehList), // 重新索引数组 + + 'contract_exchange_rate' => $item->contract_exchange_rate, + 'cif_total_amount' => $mvs['cif_total_amount'], + 'profit_total' => $mvs['profit_total'], + + 'declarant_name' => $item->declarant_name, + 'eta_ship_date' => date('Y-m-d', $item->eta_ship_date), + 'shipping_method_snapshot' => $item->shipping_method_snapshot, + 'domestic_port_snapshot' => $item->domestic_port_snapshot, + 'trade_term_snapshot' => $item->trade_term_snapshot, + 'clearance_port_snapshot' => $item->clearance_port_snapshot, + 'latest_arrival_date' => date('Y-m-d', $item->latest_arrival_date), + 'pick_up_date' => date('Y-m-d', $item->pick_up_date), + 'collect_all_date' => date('Y-m-d', $item->collect_all_date), + 'is_final' => $item->is_final, + + // 账户 + 'account_list' => $accList, + 'acc_index' => count($accList)+1, + 'currency_symbol' => $item->settlement_currency, + 'sign_imgs' => $this->queryAppro($item->id), + 'contract_url' => (!empty($item->contract)) ? (new \app\common\service\OssService)->generatePrivateUrl($item->contract) : '' + ]; + + return $veh; + } + + + public function queryAppro($bizId) + { + $instances = ApprovalInstances::where('biz_id', $bizId)->where('biz_type', 'stock_orders')->first(); + if (!$instances) { + return []; + } + + $records = ApprovalRecords::where('instance_id', $instances->id)->where('action', 'approve')->orderBy('step_index', 'asc')->pluck('approver_id')->toArray(); + if (count($records) == 0) { + return []; + } + + $signImgs = []; + foreach ($records as $k => $v) { + $signImg = User::where('id', $v)->value('sign_img'); + $signImgs[$k] = (!empty($signImg))?(new \app\common\service\OssService)->generatePrivateUrl($signImg):''; + } + + return $signImgs; + } + + /** + * 打印数据 + */ + public function servicePrint(Request $request) + { + $config = $this->buildQueryConfig($request, 'show'); + $item = $this->logicShow($config); + return $this->postProcess('Print', $item); + } + + public function serviceDetail(Request $request) + { + $config = $this->buildQueryConfig($request, 'detail'); + $item = $this->logicShow($config); + $pdfData = $this->pdfData($item); + return $this->postProcess('Detail', $pdfData); + } + + /** + * 审批 + */ + public function serviceApproval(Request $request) + { + $config = $this->buildQueryConfig($request, 'approval'); + $this->logicUpdateBy($config, [ + 'approval_status' => 1 + ]); + $item = $this->logicShow($config); + $this->startApproval($item); + return $this->postProcess('Approval', $item); + } + + /** + * 最终审批 + */ + public function serviceUltimateApproval(Request $request) + { + $config = $this->buildQueryConfig($request, 'detail'); + $item = $this->logicShow($config); + foreach ($item->items as $k => $v) { + if ($v->vin === '') { + throw new BusinessException('存在未填写vin的车辆,无法完成提交审核'); + } + } + $this->logicUpdateBy($config, [ + 'approval_status' => 1 + ]); + $this->startApproval($item); + return $this->postProcess('UltimateApproval', $item); + } + + /** + * 创建 + * + * @param Request $request + * @param array $data + * @return mixed + */ + public function serviceStore(Request $request, array $data = []) + { + // 关联表较多,增加事务处理 + return Db::transaction(function () use ($request, $data) { + $data = $this->preProcess('Store', $data, $request); + $dl = $this->getDataLimitFromRequest($request); + $created = $this->logicCreate($data, $dl); + + // 处理车辆和账户信息 + $this->processOrderItems($created, $data); + + return $this->postProcess('Store', $created); + }); + } + + protected function onlyFillableData(Model|string $model, array $data, array $extraWhitelist = []): array + { + // 兼容传类名或实例 + $m = \is_string($model) ? new $model : $model; + // 1) 优先使用 fillable + $keys = $m->getFillable(); + // 2) 若没配置 fillable(常见:用 guarded),回退到完整表字段,避免误伤 + if (empty($keys)) { + $keys = $m->getConnection() + ->getSchemaBuilder() + ->getColumnListing($m->getTable()); + } + // 3) 允许额外白名单(如虚拟字段、JSON映射字段) + if ($extraWhitelist) { + $keys = array_merge($keys, $extraWhitelist); + } + return array_intersect_key($data, array_flip($keys)); + } + + /** + * 更新前处理 + * + * @param array $data + * @param Request $request + * @return array + */ + public function beforeServiceUpdate($data, Request $request) + { + $data['biz_date'] = strtotime($data['biz_date']); + $data['eta_ship_date'] = strtotime($data['eta_ship_date']); + $data['latest_arrival_date'] = strtotime($data['latest_arrival_date']); + $data['approval_status'] = $data['approval_status'] == 1 ? 1 : 0; + + $data['pick_up_date'] = $data['pick_up_date'] ? strtotime($data['pick_up_date']) : 0; + $data['collect_all_date'] = $data['collect_all_date'] ? strtotime($data['collect_all_date']) : 0; + return $data; + } + + /** + * 更新后处理 + * + * @param mixed $res + * @return mixed + */ + protected function afterServiceUpdate($res) + { + // 启动审批 + if ($res->approval_status == 1) { + $this->startApproval($res); + } + return $res; + } + + /** + * 更新 + * + * @param Request $request + * @param array $data + * @return mixed + */ + public function serviceUpdate(Request $request, array $data = []) + { + // 关联表较多,增加事务处理 + return Db::transaction(function () use ($request, $data) { + $data = $this->preProcess('Update', $data, $request); + $config = $this->buildQueryConfig($request, 'update'); + // 过滤掉非库字段 + $dbData = $this->onlyFillableData($config['model'], $data); + // 更新主表 + $updated = $this->logicUpdateBy($config, $dbData); + // 获取更新后的数据 + $order = $this->logicShow($config); + // 处理车辆和账户信息 + $this->processOrderItems($order, $data, true); + return $this->postProcess('Update', $order); + }); + } + + /** + * 处理订单项(车辆和账户) + * @param StockOrders $order 订单对象 + * @param array $data 数据 + * @param bool $isUpdate 是否为更新操作 + */ + private function processOrderItems($order, array $data, bool $isUpdate = false): void + { + // 如果是更新操作,先删除原有的关联数据 + if ($isUpdate) { + $this->clearExistingOrderItems($order->id); + } + + // 处理车辆项 + $this->processVehicleItems($order, $data['veh_list'] ?? []); + + // 处理账户项 + $this->processAccountItems($order, $data['account_list'] ?? []); + } + + /** + * (更新时)清除已存在的订单项 + * @param int $orderId 订单ID + */ + private function clearExistingOrderItems(int $orderId): void + { + // 删除车辆改装项 + StockOrderItemOptions::whereHas('item', function ($query) use ($orderId) { + $query->where('order_id', $orderId); + })->delete(); + + // 删除车辆项及其相关金额记录 + $orderItems = StockOrderItems::where('order_id', $orderId)->get(); + foreach ($orderItems as $item) { + // 删除车辆项的金额记录 + Db::table('veh_money_value') + ->where('object_scene', 'stock_order_items') + ->where('object_id', $item->id) + ->delete(); + } + + // 删除车辆项 + StockOrderItems::where('order_id', $orderId)->delete(); + + // 删除订单本身的金额记录 + Db::table('veh_money_value') + ->where('object_scene', 'stock_orders') + ->where('object_id', $orderId) + ->delete(); + + // 删除账户项 + StockOrderAccounts::where('order_id', $orderId)->delete(); + } + + /** + * 处理车辆项 + */ + private function processVehicleItems($order, array $vehList): void + { + $cifTotalAmount = 0.00; + // $cifTotalAmountRate = 0; + // $cifTotalAmountCurrency = 'CNY'; + + $profitTotalAmount = 0.00; + foreach ($vehList as $veh) { + $veh['order_id'] = $order->id; + $veh['unique_id'] = $this->generateVehicleUniqueId($veh); + + $item = StockOrderItems::create($veh); + + // 处理金额信息 + $this->processVehiclePrices($item, $veh); + + // 计算利润 + $profit = $this->calculateProfit($veh, $order); + $item->addMoney('profit', $order->settlement_currency, $profit, $order->contract_exchange_rate, '利润'); + + // 累计到岸价 + $cifTotalAmount += $veh['total_price']['amount'] ?? 0; + $profitTotalAmount += $profit; + + // if (isset($veh['retrofit_fee']['rate'])) { + // $cifTotalAmountRate = $veh['retrofit_fee']['rate']; + // } + // if (isset($veh['retrofit_fee']['currency'])) { + // $cifTotalAmountCurrency = $veh['retrofit_fee']['currency']; + // } + + // 处理改装项 + $this->processModificationItems($item->id, $veh['modify_list'] ?? []); + } + + // 保存合计到岸价 + $order->addMoney('cif_total_amount', $order->settlement_currency, $cifTotalAmount, $order->contract_exchange_rate, '合计到岸价'); + // 利润 + $order->addMoney('profit_total', $order->settlement_currency, $profitTotalAmount, $order->contract_exchange_rate, '总利润'); + } + + /** + * 生成车辆唯一标识 + */ + private function generateVehicleUniqueId(array $veh): string + { + $uniqueIdStr = ""; + $uniqueIdStr .= $veh['sku_id'] ?? ''; + $uniqueIdStr .= $veh['model_snapshot'] ?? ''; + $uniqueIdStr .= $veh['color_interior_code'] ?? ''; + $uniqueIdStr .= $veh['color_exterior_code'] ?? ''; + $uniqueIdStr .= $veh['mileage'] ?? ''; + $uniqueIdStr .= json_encode($veh['modify_list'] ?? []); + + return md5($uniqueIdStr); + } + + /** + * 处理车辆价格信息 + */ + private function processVehiclePrices($item, array $veh): void + { + // 基础费用 + $priceFields = [ + 'unit_price' => '单价', + 'total_price' => '总价', + 'handling_fee' => '手续费', + 'fob_transport_fee' => 'FOB运输费', + 'port_misc_fee' => '港杂', + 'retrofit_fee' => '改装费', + 'ocean_freight' => '海运费' + ]; + + foreach ($priceFields as $field => $description) { + if (!empty($veh[$field]['amount'])) { + $item->addMoney( + $field, + $veh[$field]['currency'], + $veh[$field]['amount'], + $veh[$field]['rate'], + $description + ); + } + } + + // 根据里程处理新车或二手车价格 + if (!empty($veh['mileage']) && $veh['mileage'] > 0) { + // 二手车 + if (!empty($veh['listing_price']['amount'])) { + $item->addMoney( + 'listing_price', + $veh['listing_price']['currency'], + $veh['listing_price']['amount'], + $veh['listing_price']['rate'], + '二手挂牌价' + ); + } + + if (!empty($veh['reduction_price']['amount'])) { + $item->addMoney( + 'reduction_price', + $veh['reduction_price']['currency'], + $veh['reduction_price']['amount'], + $veh['reduction_price']['rate'], + '二手减价' + ); + } + } else { + // 新车 + if (!empty($veh['official_price']['amount'])) { + $item->addMoney( + 'official_price', + $veh['official_price']['currency'], + $veh['official_price']['amount'], + $veh['official_price']['rate'], + '官方指导价' + ); + } + + if (!empty($veh['discount_price']['amount'])) { + $item->addMoney( + 'discount_price', + $veh['discount_price']['currency'], + $veh['discount_price']['amount'], + $veh['discount_price']['rate'], + '优惠价' + ); + } + } + } + + /** + * 计算利润 + */ + private function calculateProfit(array $veh, $order): float + { + $unitPrice = (float)($veh['unit_price']['amount'] ?? 0); + + if (!empty($veh['mileage']) && $veh['mileage'] > 0) { + // 二手车 + $ttPrice = ($veh['listing_price']['amount'] ?? 0) / ($order->contract_exchange_rate ?: 1); + $ddPrice = $veh['reduction_price']['amount'] ?? 0; + } else { + // 新车 + $ttPrice = ($veh['official_price']['amount'] ?? 0) / ($order->contract_exchange_rate ?: 1); + $ddPrice = $veh['discount_price']['amount'] ?? 0; + } + + $handlingFee = (float)($veh['handling_fee']['amount'] ?? 0); + $fobTransportFee = (float)($veh['fob_transport_fee']['amount'] ?? 0); + $portMiscFee = (float)($veh['port_misc_fee']['amount'] ?? 0); + $retrofitFee = (float)($veh['retrofit_fee']['amount'] ?? 0); + $oceanFreight = (float)($veh['ocean_freight']['amount'] ?? 0); + + return $unitPrice - $ttPrice - $ddPrice - $handlingFee - $fobTransportFee - $portMiscFee - $retrofitFee - $oceanFreight; + } + + /** + * 处理改装项 + */ + private function processModificationItems(int $orderItemId, array $modifyList): void + { + if (empty($modifyList)) { + return; + } + + foreach ($modifyList as $modify) { + $modify['order_item_id'] = $orderItemId; + StockOrderItemOptions::create($modify); + } + } + + /** + * 处理账户项 + */ + private function processAccountItems($order, array $accountList): void + { + if (empty($accountList)) { + return; + } + + foreach ($accountList as $account) { + $account['order_id'] = $order->id; + if (!empty($account['remit_date'])) { + $account['remit_date'] = strtotime($account['remit_date']); + } + StockOrderAccounts::create($account); + } + } +} \ No newline at end of file diff --git a/app/crm/service/StockOrdersService_bak.php b/app/crm/service/StockOrdersService_bak.php new file mode 100644 index 0000000..6e1f287 --- /dev/null +++ b/app/crm/service/StockOrdersService_bak.php @@ -0,0 +1,444 @@ +validator; + } + + + protected function buildQueryConfig(Request $request, string $scene = 'index'): array + { + $p = $request->all(); + + $config = []; + $config['model'] = $this->modelClass; + switch ($scene) { + case 'index': + + // ====== ====== 筛选 ====== ====== + // 业务id + if (isset($p['salesperson_id']) && $p['salesperson_id']) { + $config['where']['salesperson_id'] = $p['salesperson_id']; + } + // 初、终版 + $config['where']['is_final'] = $p['is_final'] ?? 0; + // 关键字 + if (isset($p['keyword']) && $p['keyword']) { + $k = '%' . $p['keyword'] . '%'; + foreach (['pi_no', 'order_no', 'customer_snapshot'] as $col) { + $config['orWhere'][] = [$col, 'like', $k]; + } + } + + $config['order']['id'] = 'desc'; + + $config['where']['salesperson_id'] = $request->user->uid; + break; + case 'print': + $config['where']['id'] = $p['id']; + break; + case 'show': + $config['where']['id'] = $p['id']; + + $config['with'][] = 'items'; + $config['with'][] = 'items.options'; + $config['with'][] = 'items.moneyValues'; + $config['with'][] = 'accounts'; + $config['with'][] = 'moneyValues'; + $config['with'][] = 'salesperson'; + break; + case 'store': + break; + case 'update': + $config['where']['id'] = $p['id']; + break; + case 'destroy': + $config['where']['id'] = $p['id']; + break; + } + + return $config; + } + + + public function serviceStore(Request $request, array $data = []) + { + // 关联表较多,增加事务处理 + return Db::transaction(function () use ($request, $data) { + $data = $this->preProcess('Store', $data, $request); + $dl = $this->getDataLimitFromRequest($request); + $created = $this->logicCreate($data, $dl); // 表1 + + // 车辆 + $cifTotalAmount = 0; + $cifTotalAmountRate = 0; + $cifTotalAmountCurrency = 'CNY'; + // $profit = 0; + foreach ($data['veh_list'] as $veh) { + $veh['order_id'] = $created->id; + + // 唯一标识 + $uniqueIdStr = ""; + $uniqueIdStr .= $veh['sku_id']; + $uniqueIdStr .= $veh['model_snapshot']; + $uniqueIdStr .= $veh['color_interior_code']; + $uniqueIdStr .= $veh['color_exterior_code']; + $uniqueIdStr .= $veh['mileage']; + $uniqueIdStr .= json($veh['modify_list']); + $veh['unique_id'] = md5($uniqueIdStr); + $item = StockOrderItems::create($veh); // 表2 + + // 金额 // 表3 + // $item->addMoney('official_price', $veh['official_price']['currency'], $veh['official_price']['amount'], $veh['official_price']['rate'], '官方指导价'); + // $item->addMoney('discount_price', $veh['discount_price']['currency'], $veh['discount_price']['amount'], $veh['discount_price']['rate'], '优惠价'); + if ($veh['unit_price']['amount']) { + $item->addMoney('unit_price', $veh['unit_price']['currency'], $veh['unit_price']['amount'], $veh['unit_price']['rate'], '单价'); + } + if ($veh['total_price']['amount']) { + $item->addMoney('total_price', $veh['total_price']['currency'], $veh['total_price']['amount'], $veh['total_price']['rate'], '总价'); + } + if ($veh['handling_fee']['amount']) { + $item->addMoney('handling_fee', $veh['handling_fee']['currency'], $veh['handling_fee']['amount'], $veh['handling_fee']['rate'], '手续费'); + } + if ($veh['fob_transport_fee']['amount']) { + $item->addMoney('fob_transport_fee', $veh['fob_transport_fee']['currency'], $veh['fob_transport_fee']['amount'], $veh['fob_transport_fee']['rate'], 'FOB运输费'); + } + if ($veh['port_misc_fee']['amount']) { + $item->addMoney('port_misc_fee', $veh['port_misc_fee']['currency'], $veh['port_misc_fee']['amount'], $veh['port_misc_fee']['rate'], '港杂'); + } + if ($veh['retrofit_fee']['amount']) { + $item->addMoney('retrofit_fee', $veh['retrofit_fee']['currency'], $veh['retrofit_fee']['amount'], $veh['retrofit_fee']['rate'], '改装费'); + } + if ($veh['ocean_freight']['amount']) { + $item->addMoney('ocean_freight', $veh['ocean_freight']['currency'], $veh['ocean_freight']['amount'], $veh['ocean_freight']['rate'], '海运费'); + } + + // 二手车 or 新车 + if ($veh['mileage'] > 0) { + if ($veh['listing_price']['amount']) { + $item->addMoney('listing_price', $veh['listing_price']['currency'], $veh['listing_price']['amount'], $veh['listing_price']['rate'], '二手挂牌价'); + } + if ($veh['reduction_price']['amount']) { + $item->addMoney('reduction_price', $veh['reduction_price']['currency'], $veh['reduction_price']['amount'], $veh['reduction_price']['rate'], '二手减价'); + } + + $ttPrice = ($veh['listing_price']['amount'] / $created->contract_exchange_rate); + $ddPrice = $veh['reduction_price']['amount']; + } else { + if ($veh['official_price']['amount']) { + $item->addMoney('official_price', $veh['official_price']['currency'], $veh['official_price']['amount'], $veh['official_price']['rate'], '官方指导价'); + } + if ($veh['discount_price']['amount'] ) { + $item->addMoney('discount_price', $veh['discount_price']['currency'], $veh['discount_price']['amount'], $veh['discount_price']['rate'], '优惠价'); + } + + $ttPrice = ($veh['official_price']['amount'] / $created->contract_exchange_rate); + $ddPrice = $veh['discount_price']['amount']; + } + + // 利润 + $profit = (float)$veh['unit_price']['amount'] + - (float)$ttPrice + - (float)$ddPrice + - (float)$veh['handling_fee']['amount'] + - (float)$veh['fob_transport_fee']['amount'] + - (float)$veh['port_misc_fee']['amount'] + - (float)$veh['retrofit_fee']['amount'] + - (float)$veh['ocean_freight']['amount']; + $item->addMoney('profit', $veh['unit_price']['currency'], $profit, $veh['unit_price']['rate'], '利润'); + + // cif_total_amount + $cifTotalAmount += $veh['total_price']['amount']; + $cifTotalAmountRate = $veh['retrofit_fee']['rate']; + $cifTotalAmountCurrency = $veh['retrofit_fee']['currency']; + + // 改装 + if (count($veh['modify_list']) > 0) { + foreach ($veh['modify_list'] as $modify) { + $modify['order_item_id'] = $item->id; + StockOrderItemOptions::create($modify); // 表4 + } + } + } + + // 计算&&插入 表1 + $created->addMoney('cif_total_amount', $cifTotalAmountCurrency, $cifTotalAmount, $cifTotalAmountRate, '合计到岸价'); + + // 账户 + if (count($data['account_list']) > 0) { + foreach ($data['account_list'] as $account) { + $account['order_id'] = $created->id; + $account['remit_date'] = strtotime($account['remit_date']); + StockOrderAccounts::create($account); // 表5 + } + } + + return $this->postProcess('Store', $created); + }); + } + + public function beforeServiceStore($data, Request $request) + { + $userId = $request->user->uid; + + $data['order_no'] = generateStockNo(); + $data['biz_date'] = strtotime($data['biz_date']); + $data['eta_ship_date'] = strtotime($data['eta_ship_date']); + $data['latest_arrival_date'] = strtotime($data['latest_arrival_date']); + $data['salesperson_id'] = $userId; + $data['salesperson_snapshot'] = User::where('id', $userId)->value('name') ?? ''; + + return $data; + } + + protected function afterServiceStore($res) + { + // 处理原单 + if ($res->is_final == 1 && $res->object_id > 0) { + $order = StockOrders::find($res->object_id); + $order->is_use = 1; + $order->object_id = $res->id; + if (!$order->save()) { + flg('备货单【'.$order->order_no.'】创建终版成功,但更新原单失败'); + } + } + // 启动审批 + $appro = new \app\common\utils\approval\service\ApprovalFlowService(); + $result = $appro->start('stock_orders', 'stock_orders', $res->id, $res->toArray(), $res->salesperson_id); + var_dump($result); + + // 异步生成pdf文件 + \Webman\RedisQueue\Redis::send('stock-order-pdf', ['id'=>$res->id]); + return $res; + } + + + public function createdPdfFile($id) + { + $config = []; + $config['model'] = $this->modelClass; + $config['where']['id'] = $id; + $config['with'][] = 'items'; + $config['with'][] = 'items.options'; + $config['with'][] = 'items.moneyValues'; + $config['with'][] = 'accounts'; + $config['with'][] = 'moneyValues'; + $config['with'][] = 'salesperson'; + $item = $this->logicShow($config); + $pdfData = $this->pdfData($item); + + // 渲染模板为最终HTML + $tplPath = app_path('common/utils/pdf/template/stockOrder.stub'); + $html = \app\common\utils\pdf\service\PdfHelper::render($tplPath, ['data' => $pdfData]); + + // 生成PDF + $driver = new \app\common\utils\pdf\service\MpdfDriver(); + $pdfBin = $driver->htmlToPdf($html); + + // 存储 + $dir = public_path('pdf').'/'; + $fileName = ($pdfData['order_no'] ?? date('YmdHis')) . '.pdf'; + + // 本地保存 + \app\common\utils\pdf\service\PdfStorage::saveBinary( + $pdfBin, + $dir, + $fileName + ); + + // // 在线预览 + // return new \support\Response(200, [ + // 'Content-Type' => 'application/pdf', + // 'Content-Disposition' => 'inline; filename="stock-order.pdf"', + // ], $pdfBin); + + // $url = [ + // 'file_name' => $fileName, + // 'file_url' => getenv('MAIN_DOMAIN').'/pdf/'.$fileName + // ]; + + // 上传云端 + $param = [ + 'path' => $dir . $fileName, + 'scene' => 'stock_orders', + 'category' => 'pdf', + 'original_name' => $fileName + ]; + $up = new \app\common\service\UploadService(); + $res = $up->uploadLocalFile($param); + + @unlink($dir . $fileName); + + // 更新文件地址 + $item->file_url = $res['file_name']; + $item->save(); + + return $res; + } + + public function servicePrint(Request $request) + { + $config = $this->buildQueryConfig($request, 'show'); + $item = $this->logicShow($config); + return $this->postProcess('Print', $item); + } + + + public function pdfData($item) + { + $vehList = []; + $index = 0; + foreach ($item->items as $k => $v) { + // 存在则数量加1 + if (count($vehList) > 0 && isset($vehList[$v->unique_id])) { + $vehList[$v->unique_id]['num'] += 1; + $vehList[$v->unique_id]['total_price'] += $vehList[$v->unique_id]['unit_price']; + $vehList[$v->unique_id]['total_price_text'] = $vehList[$v->unique_id]['total_price_symbol'].number_format($vehList[$v->unique_id]['total_price'], 2, '.', ','); + continue; + } + $index += 1; + // 处理金额 + $mv = []; + foreach ($v->moneyValues as $money) { + $mv[$money->field] = [ + 'currency' => $money->currency, + 'amount' => $money->amount, + 'rate' => $money->exchange_rate, + 'currency_symbol' => SysCurrency::where('currency_code', $money->currency)->value('currency_symbol'), + ]; + } + // 处理改装 + $mfy = ''; + foreach ($v->options as $my) { + $mfy .= $my->option_name_snapshot.' '; + } + + // 处理结果集 + $vehList[$v->unique_id] = [ + 'index' => $index, + 'model_snapshot' => $v->model_snapshot, + 'color_interior_snapshot' => $v->color_interior_snapshot, + 'color_exterior_snapshot' => $v->color_exterior_snapshot, + 'num' => 1, + 'modify' => $mfy, + 'mileage' => $v->mileage, + 'unit_price' => $mv['unit_price']['amount'], + 'total_price' => $mv['total_price']['amount'], + 'unit_price_symbol' => $mv['unit_price']['currency_symbol'], + 'total_price_symbol' => $mv['total_price']['currency_symbol'], + 'unit_price_text' => $mv['unit_price']['currency_symbol'].number_format($mv['unit_price']['amount'], 2, '.', ','), + 'total_price_text' => $mv['total_price']['currency_symbol'].number_format($mv['total_price']['amount'], 2, '.', ',') + ]; + + // 根据里程处理 新车or二手车 + if ($v->mileage > 0) { + if (isset($mv['reduction_price'])) { + $vehList[$v->unique_id]['reduction_price'] = $mv['reduction_price']['amount']; + $vehList[$v->unique_id]['reduction_price_symbol'] = $mv['reduction_price']['currency_symbol']; + $vehList[$v->unique_id]['reduction_price_text'] = $mv['reduction_price']['currency_symbol'].number_format($mv['reduction_price']['amount'], 2, '.', ','); + } + $vehList[$v->unique_id]['listing_price'] = $mv['listing_price']['amount']; + $vehList[$v->unique_id]['listing_price_symbol'] = $mv['listing_price']['currency_symbol']; + $vehList[$v->unique_id]['listing_price_text'] = $mv['listing_price']['currency_symbol'].number_format($mv['listing_price']['amount'], 2, '.', ','); + } else { + if (isset($mv['discount_price'])) { + $vehList[$v->unique_id]['discount_price'] = $mv['discount_price']['amount']; + $vehList[$v->unique_id]['discount_price_symbol'] = $mv['discount_price']['currency_symbol']; + $vehList[$v->unique_id]['discount_price_text'] = $mv['discount_price']['currency_symbol'].number_format($mv['discount_price']['amount'], 2, '.', ','); + } + $vehList[$v->unique_id]['official_price'] = $mv['official_price']['amount']; + $vehList[$v->unique_id]['official_price_symbol'] = $mv['official_price']['currency_symbol']; + $vehList[$v->unique_id]['official_price_text'] = $mv['official_price']['currency_symbol'].number_format($mv['official_price']['amount'], 2, '.', ','); + } + } + + $accList = []; + $accIndex = 1; + foreach ($item->accounts as $k => $v) { + $accList[] = [ + 'index' => '收款'.$k + 1, + 'remit_country_name_snapshot' => $v->remit_country_name_snapshot, + 'remit_date' => date('Y-m-d', $v->remit_date), + 'currency_name_snapshot' => $v->currency_name_snapshot, + 'account_name_snapshot' => $v->account_name_snapshot, + 'remitter_name' => $v->remitter_name, + 'amount' => $v->amount, + ]; + $accIndex += 1; + } + + $cifTotalAmount = 0.00; + if ($item->moneyValues) { + $cifTotalAmountSysboml = SysCurrency::where('currency_code', $item->moneyValues->currency)->value('currency_symbol'); + $cifTotalAmount = $cifTotalAmountSysboml.sprintf("%.2f", $item->moneyValues->amount, $item->moneyValues->exchange_rate); + } + + $veh = [ + 'order_no' => $item->order_no, + 'salesperson_snapshot' => $item->salesperson_snapshot, + // 'sign_img' => $item->salesperson->sign_img, + 'sign_img_url' => (!empty($item->salesperson->sign_img))?(new \app\common\service\OssService)->generatePrivateUrl($item->salesperson->sign_img):'', + 'created_at' => date('Y-m-d', strtotime($item->created_at)), + + 'pi_no' => $item->pi_no, + 'biz_date' => date('Y-m-d', $item->biz_date), + 'clearance_country_snapshot' => $item->clearance_country_snapshot, + 'customer_source_snapshot' => $item->customer_source_snapshot, + 'customer_snapshot' => $item->customer_snapshot, + 'customer_country_snapshot' => $item->customer_country_snapshot, + + // 订单项 + 'veh_list' => $vehList, + + 'contract_exchange_rate' => $item->contract_exchange_rate, + 'cif_total_amount' => $item->cif_total_amount, + + 'declarant_name' => $item->declarant_name, + 'eta_ship_date' => date('Y-m-d', $item->eta_ship_date), + 'shipping_method_snapshot' => $item->shipping_method_snapshot, + 'domestic_port_snapshot' => $item->domestic_port_snapshot, + 'trade_term_snapshot' => $item->trade_term_snapshot, + 'clearance_port_snapshot' => $item->clearance_port_snapshot, + 'latest_arrival_date' => date('Y-m-d', $item->latest_arrival_date), + + // 账户 + 'account_list' => $accList, + 'acc_index' => $accIndex, + 'cif_total_amount' => $cifTotalAmount + ]; + + return $veh; + } +} \ No newline at end of file diff --git a/app/crm/validate/ApprovalFlowsValidate.php b/app/crm/validate/ApprovalFlowsValidate.php new file mode 100644 index 0000000..fef36f4 --- /dev/null +++ b/app/crm/validate/ApprovalFlowsValidate.php @@ -0,0 +1,29 @@ + [ + + ], + + 'update' => [ + + ] + ]; +} \ No newline at end of file diff --git a/app/crm/validate/ApprovalInstancesValidate.php b/app/crm/validate/ApprovalInstancesValidate.php new file mode 100644 index 0000000..bbcbad5 --- /dev/null +++ b/app/crm/validate/ApprovalInstancesValidate.php @@ -0,0 +1,29 @@ + [ + + ], + + 'update' => [ + + ] + ]; +} \ No newline at end of file diff --git a/app/crm/validate/ApprovalRecordsValidate.php b/app/crm/validate/ApprovalRecordsValidate.php new file mode 100644 index 0000000..6a91cb6 --- /dev/null +++ b/app/crm/validate/ApprovalRecordsValidate.php @@ -0,0 +1,29 @@ + [ + + ], + + 'update' => [ + + ] + ]; +} \ No newline at end of file diff --git a/app/crmAdmin/controller/StatisticsController.php b/app/crmAdmin/controller/StatisticsController.php new file mode 100644 index 0000000..77db8a0 --- /dev/null +++ b/app/crmAdmin/controller/StatisticsController.php @@ -0,0 +1,188 @@ +service = new CountryService(); + } + + public function whole() + { + $totals = Customer::count('id'); + $channelCount = InquiryChannel::count('id'); + $todayCount = Customer::where('created_at', '>=', strtotime(date('Y-m-d')))->count('id'); + $releaseCount = Customer::distinct('enter_user_id')->count('id'); + $countryCount = Country::count('id'); + $trackCount = Customer::distinct('user_id')->count(); + + $data = [ + 'totals' => $totals, + 'channelCount' => $channelCount, + 'todayCount' => $todayCount, + 'releaseCount' => $releaseCount, + 'countryCount' => $countryCount, + 'trackCount' => $trackCount + ]; + return $this->success($data); + } + + // 各渠道计数 + public function channelCount() + { + $inquiryChannelList = InquiryChannel::all(['id', 'name']); + $channels = []; + foreach ($inquiryChannelList as $key => $val) { + $channels[$val->id] = $val->name; + } + + $list = Customer::select('channel_id', Db::raw('COUNT(*) as totals')) + ->groupBy('channel_id') + ->get()->toArray(); + + $channelCounts = []; + foreach ($list as $vo) { + $channelCounts[] = [ + "name" => $channels[$vo["channel_id"]], + "value" => $vo["totals"] + ]; + } + return $this->success($channelCounts); + } + + // 各国家计数 + public function countryCount() + { + $list = Customer::with("countryInfo") + ->select('country_id', Db::raw('COUNT(*) as totals')) + ->where('country_id','<>',0) + ->groupBy('country_id') + ->get(); + $arr = []; + foreach ($list as $val) { + $arr[] = [ + "value" => $val->totals, + "name" => $val->countryInfo->cn_addr + ]; + } + return $this->success($arr); + } + + // 跟单人计数 + public function trackCount() + { + $list = Customer::with(["user"]) + ->where("user_id", ">", 0) + ->whereNotIn('user_id', [1,5,10,11,13,155,16,17,19,74,75,78,141,147]) + ->select('user_id', Db::raw('COUNT(*) as totals')) + ->groupBy('user_id') + ->get(); + $optionType = []; + $counts = []; + foreach ($list as $val) { + if (isset($val->user) && $val->user) { + $optionType[] = $val->user->name; + $counts[] = $val->totals; + } + } + $arr = [ + "optionType" => $optionType, + "counts" => $counts + ]; + return $this->success($arr); + } + + // 每日录单数 + public function dayCount() + { + $list = Db::table('crm_customer') + ->where('created_at', '>=', Carbon::now()->subDay(7)->timestamp) + ->select( + Db::raw('DATE(FROM_UNIXTIME(created_at)) as date'), + Db::raw('COUNT(*) as order_count') + ) + ->groupBy('date') + ->pluck('order_count', 'date')->toArray(); + $optionType = []; + $counts = []; + foreach ($list as $key=>$val) { + $optionType[] = $key; + $counts[] = $val; + } + $arr = [ + "optionType" => $optionType, + "counts" => $counts + ]; + return $this->success($arr); + } + + public function customerCount(Request $request) + { + $companyId = $request->get('company_id') ?? 0; + + // 1. 计算时间范围 + $now = Carbon::now(); + $sixMonthsAgo = $now->copy()->subMonths(5)->startOfMonth(); // 包含6个月 + var_dump($sixMonthsAgo->timestamp); + + // 2. 获取关联渠道ID + $channelIds = $this->getChannelIdsByCompany($companyId); + + if (empty($channelIds)) { + return collect(); + } + + // 3. 执行查询 + $list = Customer::select([ + DB::raw("DATE_FORMAT(FROM_UNIXTIME(created_at), '%Y-%m') AS month"), + DB::raw('COUNT(*) AS total_count') + ]) + ->whereIn('channel_id', $channelIds) + ->where('created_at', '>', 0) + ->where('created_at', '>=', $sixMonthsAgo->timestamp) // 添加时间限制 + ->groupBy('month') + ->orderBy('month') + ->get(); + + $optionType = []; + $counts = []; + foreach ($list as $item) { + $optionType[] = $item->month; + $counts[] = $item->total_count; + } + $arr = [ + "optionType" => $optionType, + "counts" => $counts + ]; + return $this->success($arr); + } + + /** + * 获取公司与渠道的映射 + */ + private function getChannelIdsByCompany($companyId) + { + $data = [ + 0 => [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11], + 1 => [1, 2, 3, 4, 5, 6, 7, 8], + 3 => [9, 10, 11] + ]; + return $data[$companyId] ?? []; + } +} \ No newline at end of file diff --git a/app/functions.php b/app/functions.php index d9bb411..7ae0d0a 100644 --- a/app/functions.php +++ b/app/functions.php @@ -307,4 +307,84 @@ if (!function_exists('generateMsgId')) { // 拼接:日期时间 + 微秒 + 随机数 return $date . $micro . $rand; } +} + +/** + * 生成sku_code + * 格式:年月日时分秒 + 微秒 + 随机数 + * 例如:20250820175530987654321 + * + * @return string + */ +if (!function_exists('skuCode')) { + function skuCode(): string + { + // 时间部分(精确到微秒) + $time = microtime(true); + $micro = sprintf("%06d", ($time - floor($time)) * 1e6); + $date = date('YmdHis', (int)$time); + + // 随机部分(3位) + $rand = mt_rand(100, 999); + + // 拼接:日期时间 + 微秒 + 随机数 + return $date . $micro . $rand; + } +} + + +/** + * 生成备货单编号(预占式,每天从1开始) + * + * @return string + */ +if (!function_exists('generateStockNo')) { + function generateStockNo(string $prefix = 'BH', int $serialLength = 3): string + { + $date = date('Ymd'); + $key = "stock:order:serial:{$date}"; + + // 原子递增获取当日流水号 + $serial = \support\Redis::incr($key); + + // 首次创建时设置过期时间:到明天凌晨 + 1小时(容错) + if ($serial === 1) { + $tomorrow = strtotime(date('Y-m-d', strtotime('+1 day'))); + $expireSeconds = $tomorrow - time() + 3600; // 多加1小时 + \support\Redis::expire($key, $expireSeconds); + } + + // 格式化流水号(补零) + $paddedSerial = str_pad($serial, $serialLength, '0', STR_PAD_LEFT); + + return $prefix . $date . $paddedSerial; + } +} + +/** + * 发送通知 + * + * @param string $title + * @param string $content + * @param string $url + * @param string $type + * @return void + */ +if (!function_exists('noticePush')) { + function noticePush(array $notice = []) + { + /* + 消息字段: + 'sender_id' //发送人 + 'sender_type' //发送人类型 + 'title' //标题 + 'content' //内容 + 'scene' //场景 + 'recipient' //接收人 + 'push_method' //推送方式 + // 如果需要同时 邮箱推送 增加推送邮箱字段 + 'email' //邮箱 + */ + \Webman\RedisQueue\Redis::send('notice', $notice); + } } \ No newline at end of file diff --git a/app/queue/redis/Notice.php b/app/queue/redis/Notice.php index 3a310c8..f166085 100644 --- a/app/queue/redis/Notice.php +++ b/app/queue/redis/Notice.php @@ -17,10 +17,23 @@ class Notice implements Consumer public function consume($data) { try { - (new \app\common\service\MessageNotificationsService())->pushNotice($data); + // 应用消息 + $service = new \app\common\service\MessageNotificationsService(); + $service->pushNotice($data); + + // 邮件消息 + if (isset($data['email']) && $data['email'] === 1) { + $emails = $service->getUserEmailsById($data['recipient']); + foreach ($emails as $email) { + if (empty($email)) { + continue; + } + \app\common\utils\email\MailSend::send($email, $data['title'], $data['content']); + } + } return true; } catch (\Throwable $th) { - var_dump('4 :'.$th->getFile()."|".$th->getLine()."|".$th->getMessage()); + flg("消息推送异常:".$th->getFile()."|".$th->getLine()."|".$th->getMessage()); throw new BusinessException($th->getFile()."|".$th->getLine()."|".$th->getMessage()); } } diff --git a/app/queue/redis/StockOrderPdf.php b/app/queue/redis/StockOrderPdf.php new file mode 100644 index 0000000..057a761 --- /dev/null +++ b/app/queue/redis/StockOrderPdf.php @@ -0,0 +1,51 @@ +createdPdfFile($data['id']); + + return true; + } catch (\Throwable $th) { + var_dump('4 :'.$th->getFile()."|".$th->getLine()."|".$th->getMessage()); + throw new BusinessException($th->getFile()."|".$th->getLine()."|".$th->getMessage()); + } + } + + // 消费失败回调 + /* + $package = [ + 'id' => 1357277951, // 消息ID + 'time' => 1709170510, // 消息时间 + 'delay' => 0, // 延迟时间 + 'attempts' => 2, // 消费次数 + 'queue' => 'send-mail', // 队列名 + 'data' => ['to' => 'tom@gmail.com', 'content' => 'hello'], // 消息内容 + 'max_attempts' => 5, // 最大重试次数 + 'error' => '错误信息' // 错误信息 + ] + */ + public function onConsumeFailure(\Throwable $e, $package) + { + echo "consume failure\n"; + echo $e->getMessage() . "\n"; + // 无需反序列化 + var_export($package); + } +} \ No newline at end of file diff --git a/app/queue/translation/config/trans.php b/app/queue/translation/config/trans.php deleted file mode 100644 index e2c7702..0000000 --- a/app/queue/translation/config/trans.php +++ /dev/null @@ -1,14 +0,0 @@ - [ - 'ticket_categorie' => [ - 'translation_model' => \app\ticket\model\CategorieTranslation::class, - 'foreign_key' => 'categorie_id', - ], - ], - - // 默认支持的目标语种列表 - 'locales' => ['zh','en','ru','ar'], -]; diff --git a/app/queue/utils/XunfeiFanyi.php b/app/queue/utils/XunfeiFanyi.php index 4fe78ab..e380d21 100644 --- a/app/queue/utils/XunfeiFanyi.php +++ b/app/queue/utils/XunfeiFanyi.php @@ -3,6 +3,8 @@ namespace app\queue\utils; +use support\Log; + class XunfeiFanyi { @@ -117,10 +119,12 @@ digest: %s", "ntrans.xfyun.cn", $date, $digestBase64); return $data["data"]["result"]; } else { $this->error = $data["message"]; + Log::error("XunfeiFanyi 失败: ".$data["message"],[$data]); return false; } } catch (\Throwable $th) { $this->error = $th->getMessage(); + Log::error("XunfeiFanyi 错误: ".$th->getMessage()); return false; } } diff --git a/app/ticket/service/CategorieService.php b/app/ticket/service/CategorieService.php index 67d1d74..dedccd4 100644 --- a/app/ticket/service/CategorieService.php +++ b/app/ticket/service/CategorieService.php @@ -2,7 +2,7 @@ namespace app\ticket\service; -use app\queue\translation\service\TranslationService; +use app\common\utils\translation\service\TranslationService; use app\queue\utils\Translat; use app\ticket\logic\CategorieLogic; use app\ticket\model\CategorieTranslation; diff --git a/composer.json b/composer.json index c474010..65fd08b 100644 --- a/composer.json +++ b/composer.json @@ -36,7 +36,6 @@ "webman/console": "^2.1", "vlucas/phpdotenv": "^5.6", "firebase/php-jwt": "^6.11", - "league/oauth2-server": "^9.2", "topthink/think-validate": "^3.0", "shopwwi/webman-filesystem": "1.1", "shopwwi/filesystem-oss": "^1.0", @@ -46,7 +45,8 @@ "workerman/crontab": "^1.0", "phpoffice/phpspreadsheet": "^4.5", "phpmailer/phpmailer": "^6.10", - "webman/gateway-worker": "^1.0" + "webman/gateway-worker": "^1.0", + "mpdf/mpdf": "^8.2" }, "suggest": { "ext-event": "For better performance. " diff --git a/composer.lock b/composer.lock index 7d41f97..feaeb5a 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "7c70d8c8bda8a5098b8baed463db8839", + "content-hash": "4914e483b0c6fc599378c91d2eab17cf", "packages": [ { "name": "aliyuncs/oss-sdk-php", @@ -265,73 +265,6 @@ ], "time": "2024-11-12T16:29:46+00:00" }, - { - "name": "defuse/php-encryption", - "version": "v2.4.0", - "source": { - "type": "git", - "url": "https://github.com/defuse/php-encryption.git", - "reference": "f53396c2d34225064647a05ca76c1da9d99e5828" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/defuse/php-encryption/zipball/f53396c2d34225064647a05ca76c1da9d99e5828", - "reference": "f53396c2d34225064647a05ca76c1da9d99e5828", - "shasum": "" - }, - "require": { - "ext-openssl": "*", - "paragonie/random_compat": ">= 2", - "php": ">=5.6.0" - }, - "require-dev": { - "phpunit/phpunit": "^5|^6|^7|^8|^9|^10", - "yoast/phpunit-polyfills": "^2.0.0" - }, - "bin": [ - "bin/generate-defuse-key" - ], - "type": "library", - "autoload": { - "psr-4": { - "Defuse\\Crypto\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Taylor Hornby", - "email": "taylor@defuse.ca", - "homepage": "https://defuse.ca/" - }, - { - "name": "Scott Arciszewski", - "email": "info@paragonie.com", - "homepage": "https://paragonie.com" - } - ], - "description": "Secure PHP Encryption Library", - "keywords": [ - "aes", - "authenticated encryption", - "cipher", - "crypto", - "cryptography", - "encrypt", - "encryption", - "openssl", - "security", - "symmetric key cryptography" - ], - "support": { - "issues": "https://github.com/defuse/php-encryption/issues", - "source": "https://github.com/defuse/php-encryption/tree/v2.4.0" - }, - "time": "2023-06-19T06:10:36+00:00" - }, { "name": "doctrine/inflector", "version": "2.0.10", @@ -1931,202 +1864,6 @@ }, "time": "2025-03-19T13:51:03+00:00" }, - { - "name": "lcobucci/clock", - "version": "3.3.1", - "source": { - "type": "git", - "url": "https://github.com/lcobucci/clock.git", - "reference": "db3713a61addfffd615b79bf0bc22f0ccc61b86b" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/lcobucci/clock/zipball/db3713a61addfffd615b79bf0bc22f0ccc61b86b", - "reference": "db3713a61addfffd615b79bf0bc22f0ccc61b86b", - "shasum": "" - }, - "require": { - "php": "~8.2.0 || ~8.3.0 || ~8.4.0", - "psr/clock": "^1.0" - }, - "provide": { - "psr/clock-implementation": "1.0" - }, - "require-dev": { - "infection/infection": "^0.29", - "lcobucci/coding-standard": "^11.1.0", - "phpstan/extension-installer": "^1.3.1", - "phpstan/phpstan": "^1.10.25", - "phpstan/phpstan-deprecation-rules": "^1.1.3", - "phpstan/phpstan-phpunit": "^1.3.13", - "phpstan/phpstan-strict-rules": "^1.5.1", - "phpunit/phpunit": "^11.3.6" - }, - "type": "library", - "autoload": { - "psr-4": { - "Lcobucci\\Clock\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Luís Cobucci", - "email": "lcobucci@gmail.com" - } - ], - "description": "Yet another clock abstraction", - "support": { - "issues": "https://github.com/lcobucci/clock/issues", - "source": "https://github.com/lcobucci/clock/tree/3.3.1" - }, - "funding": [ - { - "url": "https://github.com/lcobucci", - "type": "github" - }, - { - "url": "https://www.patreon.com/lcobucci", - "type": "patreon" - } - ], - "time": "2024-09-24T20:45:14+00:00" - }, - { - "name": "lcobucci/jwt", - "version": "5.5.0", - "source": { - "type": "git", - "url": "https://github.com/lcobucci/jwt.git", - "reference": "a835af59b030d3f2967725697cf88300f579088e" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/lcobucci/jwt/zipball/a835af59b030d3f2967725697cf88300f579088e", - "reference": "a835af59b030d3f2967725697cf88300f579088e", - "shasum": "" - }, - "require": { - "ext-openssl": "*", - "ext-sodium": "*", - "php": "~8.2.0 || ~8.3.0 || ~8.4.0", - "psr/clock": "^1.0" - }, - "require-dev": { - "infection/infection": "^0.29", - "lcobucci/clock": "^3.2", - "lcobucci/coding-standard": "^11.0", - "phpbench/phpbench": "^1.2", - "phpstan/extension-installer": "^1.2", - "phpstan/phpstan": "^1.10.7", - "phpstan/phpstan-deprecation-rules": "^1.1.3", - "phpstan/phpstan-phpunit": "^1.3.10", - "phpstan/phpstan-strict-rules": "^1.5.0", - "phpunit/phpunit": "^11.1" - }, - "suggest": { - "lcobucci/clock": ">= 3.2" - }, - "type": "library", - "autoload": { - "psr-4": { - "Lcobucci\\JWT\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Luís Cobucci", - "email": "lcobucci@gmail.com", - "role": "Developer" - } - ], - "description": "A simple library to work with JSON Web Token and JSON Web Signature", - "keywords": [ - "JWS", - "jwt" - ], - "support": { - "issues": "https://github.com/lcobucci/jwt/issues", - "source": "https://github.com/lcobucci/jwt/tree/5.5.0" - }, - "funding": [ - { - "url": "https://github.com/lcobucci", - "type": "github" - }, - { - "url": "https://www.patreon.com/lcobucci", - "type": "patreon" - } - ], - "time": "2025-01-26T21:29:45+00:00" - }, - { - "name": "league/event", - "version": "3.0.3", - "source": { - "type": "git", - "url": "https://github.com/thephpleague/event.git", - "reference": "ec38ff7ea10cad7d99a79ac937fbcffb9334c210" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/thephpleague/event/zipball/ec38ff7ea10cad7d99a79ac937fbcffb9334c210", - "reference": "ec38ff7ea10cad7d99a79ac937fbcffb9334c210", - "shasum": "" - }, - "require": { - "php": ">=7.2.0", - "psr/event-dispatcher": "^1.0" - }, - "provide": { - "psr/event-dispatcher-implementation": "1.0" - }, - "require-dev": { - "friendsofphp/php-cs-fixer": "^2.16", - "phpstan/phpstan": "^0.12.45", - "phpunit/phpunit": "^8.5" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.0-dev" - } - }, - "autoload": { - "psr-4": { - "League\\Event\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Frank de Jonge", - "email": "info@frenky.net" - } - ], - "description": "Event package", - "keywords": [ - "emitter", - "event", - "listener" - ], - "support": { - "issues": "https://github.com/thephpleague/event/issues", - "source": "https://github.com/thephpleague/event/tree/3.0.3" - }, - "time": "2024-09-04T16:06:53+00:00" - }, { "name": "league/flysystem", "version": "3.29.0", @@ -2333,276 +2070,6 @@ ], "time": "2024-01-28T23:22:08+00:00" }, - { - "name": "league/oauth2-server", - "version": "9.2.0", - "source": { - "type": "git", - "url": "https://github.com/thephpleague/oauth2-server.git", - "reference": "00323013403e1a1e0f424affafca56c28b60c22c" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/thephpleague/oauth2-server/zipball/00323013403e1a1e0f424affafca56c28b60c22c", - "reference": "00323013403e1a1e0f424affafca56c28b60c22c", - "shasum": "" - }, - "require": { - "defuse/php-encryption": "^2.4", - "ext-json": "*", - "ext-openssl": "*", - "lcobucci/clock": "^2.3 || ^3.0", - "lcobucci/jwt": "^5.0", - "league/event": "^3.0", - "league/uri": "^7.0", - "php": "~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0", - "psr/http-message": "^2.0", - "psr/http-server-middleware": "^1.0" - }, - "replace": { - "league/oauth2server": "*", - "lncd/oauth2": "*" - }, - "require-dev": { - "laminas/laminas-diactoros": "^3.5", - "php-parallel-lint/php-parallel-lint": "^1.3.2", - "phpstan/extension-installer": "^1.3.1", - "phpstan/phpstan": "^1.12", - "phpstan/phpstan-deprecation-rules": "^1.1.4", - "phpstan/phpstan-phpunit": "^1.3.15", - "phpstan/phpstan-strict-rules": "^1.5.2", - "phpunit/phpunit": "^9.6.21", - "roave/security-advisories": "dev-master", - "slevomat/coding-standard": "^8.14.1", - "squizlabs/php_codesniffer": "^3.8" - }, - "type": "library", - "autoload": { - "psr-4": { - "League\\OAuth2\\Server\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Alex Bilbie", - "email": "hello@alexbilbie.com", - "homepage": "http://www.alexbilbie.com", - "role": "Developer" - }, - { - "name": "Andy Millington", - "email": "andrew@noexceptions.io", - "homepage": "https://www.noexceptions.io", - "role": "Developer" - } - ], - "description": "A lightweight and powerful OAuth 2.0 authorization and resource server library with support for all the core specification grants. This library will allow you to secure your API with OAuth and allow your applications users to approve apps that want to access their data from your API.", - "homepage": "https://oauth2.thephpleague.com/", - "keywords": [ - "Authentication", - "api", - "auth", - "authorisation", - "authorization", - "oauth", - "oauth 2", - "oauth 2.0", - "oauth2", - "protect", - "resource", - "secure", - "server" - ], - "support": { - "issues": "https://github.com/thephpleague/oauth2-server/issues", - "source": "https://github.com/thephpleague/oauth2-server/tree/9.2.0" - }, - "funding": [ - { - "url": "https://github.com/sephster", - "type": "github" - } - ], - "time": "2025-02-15T00:49:10+00:00" - }, - { - "name": "league/uri", - "version": "7.5.1", - "source": { - "type": "git", - "url": "https://github.com/thephpleague/uri.git", - "reference": "81fb5145d2644324614cc532b28efd0215bda430" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/thephpleague/uri/zipball/81fb5145d2644324614cc532b28efd0215bda430", - "reference": "81fb5145d2644324614cc532b28efd0215bda430", - "shasum": "" - }, - "require": { - "league/uri-interfaces": "^7.5", - "php": "^8.1" - }, - "conflict": { - "league/uri-schemes": "^1.0" - }, - "suggest": { - "ext-bcmath": "to improve IPV4 host parsing", - "ext-fileinfo": "to create Data URI from file contennts", - "ext-gmp": "to improve IPV4 host parsing", - "ext-intl": "to handle IDN host with the best performance", - "jeremykendall/php-domain-parser": "to resolve Public Suffix and Top Level Domain", - "league/uri-components": "Needed to easily manipulate URI objects components", - "php-64bit": "to improve IPV4 host parsing", - "symfony/polyfill-intl-idn": "to handle IDN host via the Symfony polyfill if ext-intl is not present" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "7.x-dev" - } - }, - "autoload": { - "psr-4": { - "League\\Uri\\": "" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Ignace Nyamagana Butera", - "email": "nyamsprod@gmail.com", - "homepage": "https://nyamsprod.com" - } - ], - "description": "URI manipulation library", - "homepage": "https://uri.thephpleague.com", - "keywords": [ - "data-uri", - "file-uri", - "ftp", - "hostname", - "http", - "https", - "middleware", - "parse_str", - "parse_url", - "psr-7", - "query-string", - "querystring", - "rfc3986", - "rfc3987", - "rfc6570", - "uri", - "uri-template", - "url", - "ws" - ], - "support": { - "docs": "https://uri.thephpleague.com", - "forum": "https://thephpleague.slack.com", - "issues": "https://github.com/thephpleague/uri-src/issues", - "source": "https://github.com/thephpleague/uri/tree/7.5.1" - }, - "funding": [ - { - "url": "https://github.com/sponsors/nyamsprod", - "type": "github" - } - ], - "time": "2024-12-08T08:40:02+00:00" - }, - { - "name": "league/uri-interfaces", - "version": "7.5.0", - "source": { - "type": "git", - "url": "https://github.com/thephpleague/uri-interfaces.git", - "reference": "08cfc6c4f3d811584fb09c37e2849e6a7f9b0742" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/thephpleague/uri-interfaces/zipball/08cfc6c4f3d811584fb09c37e2849e6a7f9b0742", - "reference": "08cfc6c4f3d811584fb09c37e2849e6a7f9b0742", - "shasum": "" - }, - "require": { - "ext-filter": "*", - "php": "^8.1", - "psr/http-factory": "^1", - "psr/http-message": "^1.1 || ^2.0" - }, - "suggest": { - "ext-bcmath": "to improve IPV4 host parsing", - "ext-gmp": "to improve IPV4 host parsing", - "ext-intl": "to handle IDN host with the best performance", - "php-64bit": "to improve IPV4 host parsing", - "symfony/polyfill-intl-idn": "to handle IDN host via the Symfony polyfill if ext-intl is not present" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "7.x-dev" - } - }, - "autoload": { - "psr-4": { - "League\\Uri\\": "" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Ignace Nyamagana Butera", - "email": "nyamsprod@gmail.com", - "homepage": "https://nyamsprod.com" - } - ], - "description": "Common interfaces and classes for URI representation and interaction", - "homepage": "https://uri.thephpleague.com", - "keywords": [ - "data-uri", - "file-uri", - "ftp", - "hostname", - "http", - "https", - "parse_str", - "parse_url", - "psr-7", - "query-string", - "querystring", - "rfc3986", - "rfc3987", - "rfc6570", - "uri", - "url", - "ws" - ], - "support": { - "docs": "https://uri.thephpleague.com", - "forum": "https://thephpleague.slack.com", - "issues": "https://github.com/thephpleague/uri-src/issues", - "source": "https://github.com/thephpleague/uri-interfaces/tree/7.5.0" - }, - "funding": [ - { - "url": "https://github.com/sponsors/nyamsprod", - "type": "github" - } - ], - "time": "2024-12-08T08:18:47+00:00" - }, { "name": "maennchen/zipstream-php", "version": "3.1.2", @@ -2890,6 +2357,239 @@ ], "time": "2024-11-12T12:43:37+00:00" }, + { + "name": "mpdf/mpdf", + "version": "v8.2.6", + "source": { + "type": "git", + "url": "https://github.com/mpdf/mpdf.git", + "reference": "dd30e3b01061cf8dfe65e7041ab4cc46d8ebdd44" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/mpdf/mpdf/zipball/dd30e3b01061cf8dfe65e7041ab4cc46d8ebdd44", + "reference": "dd30e3b01061cf8dfe65e7041ab4cc46d8ebdd44", + "shasum": "" + }, + "require": { + "ext-gd": "*", + "ext-mbstring": "*", + "mpdf/psr-http-message-shim": "^1.0 || ^2.0", + "mpdf/psr-log-aware-trait": "^2.0 || ^3.0", + "myclabs/deep-copy": "^1.7", + "paragonie/random_compat": "^1.4|^2.0|^9.99.99", + "php": "^5.6 || ^7.0 || ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0", + "psr/http-message": "^1.0 || ^2.0", + "psr/log": "^1.0 || ^2.0 || ^3.0", + "setasign/fpdi": "^2.1" + }, + "require-dev": { + "mockery/mockery": "^1.3.0", + "mpdf/qrcode": "^1.1.0", + "squizlabs/php_codesniffer": "^3.5.0", + "tracy/tracy": "~2.5", + "yoast/phpunit-polyfills": "^1.0" + }, + "suggest": { + "ext-bcmath": "Needed for generation of some types of barcodes", + "ext-xml": "Needed mainly for SVG manipulation", + "ext-zlib": "Needed for compression of embedded resources, such as fonts" + }, + "type": "library", + "autoload": { + "files": [ + "src/functions.php" + ], + "psr-4": { + "Mpdf\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "GPL-2.0-only" + ], + "authors": [ + { + "name": "Matěj Humpál", + "role": "Developer, maintainer" + }, + { + "name": "Ian Back", + "role": "Developer (retired)" + } + ], + "description": "PHP library generating PDF files from UTF-8 encoded HTML", + "homepage": "https://mpdf.github.io", + "keywords": [ + "pdf", + "php", + "utf-8" + ], + "support": { + "docs": "https://mpdf.github.io", + "issues": "https://github.com/mpdf/mpdf/issues", + "source": "https://github.com/mpdf/mpdf" + }, + "funding": [ + { + "url": "https://www.paypal.me/mpdf", + "type": "custom" + } + ], + "time": "2025-08-18T08:51:51+00:00" + }, + { + "name": "mpdf/psr-http-message-shim", + "version": "v2.0.1", + "source": { + "type": "git", + "url": "https://github.com/mpdf/psr-http-message-shim.git", + "reference": "f25a0153d645e234f9db42e5433b16d9b113920f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/mpdf/psr-http-message-shim/zipball/f25a0153d645e234f9db42e5433b16d9b113920f", + "reference": "f25a0153d645e234f9db42e5433b16d9b113920f", + "shasum": "" + }, + "require": { + "psr/http-message": "^2.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Mpdf\\PsrHttpMessageShim\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mark Dorison", + "email": "mark@chromatichq.com" + }, + { + "name": "Kristofer Widholm", + "email": "kristofer@chromatichq.com" + }, + { + "name": "Nigel Cunningham", + "email": "nigel.cunningham@technocrat.com.au" + } + ], + "description": "Shim to allow support of different psr/message versions.", + "support": { + "issues": "https://github.com/mpdf/psr-http-message-shim/issues", + "source": "https://github.com/mpdf/psr-http-message-shim/tree/v2.0.1" + }, + "time": "2023-10-02T14:34:03+00:00" + }, + { + "name": "mpdf/psr-log-aware-trait", + "version": "v3.0.0", + "source": { + "type": "git", + "url": "https://github.com/mpdf/psr-log-aware-trait.git", + "reference": "a633da6065e946cc491e1c962850344bb0bf3e78" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/mpdf/psr-log-aware-trait/zipball/a633da6065e946cc491e1c962850344bb0bf3e78", + "reference": "a633da6065e946cc491e1c962850344bb0bf3e78", + "shasum": "" + }, + "require": { + "psr/log": "^3.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Mpdf\\PsrLogAwareTrait\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mark Dorison", + "email": "mark@chromatichq.com" + }, + { + "name": "Kristofer Widholm", + "email": "kristofer@chromatichq.com" + } + ], + "description": "Trait to allow support of different psr/log versions.", + "support": { + "issues": "https://github.com/mpdf/psr-log-aware-trait/issues", + "source": "https://github.com/mpdf/psr-log-aware-trait/tree/v3.0.0" + }, + "time": "2023-05-03T06:19:36+00:00" + }, + { + "name": "myclabs/deep-copy", + "version": "1.13.4", + "source": { + "type": "git", + "url": "https://github.com/myclabs/DeepCopy.git", + "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/07d290f0c47959fd5eed98c95ee5602db07e0b6a", + "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "conflict": { + "doctrine/collections": "<1.6.8", + "doctrine/common": "<2.13.3 || >=3 <3.2.2" + }, + "require-dev": { + "doctrine/collections": "^1.6.8", + "doctrine/common": "^2.13.3 || ^3.2.2", + "phpspec/prophecy": "^1.10", + "phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13" + }, + "type": "library", + "autoload": { + "files": [ + "src/DeepCopy/deep_copy.php" + ], + "psr-4": { + "DeepCopy\\": "src/DeepCopy/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Create deep copies (clones) of your objects", + "keywords": [ + "clone", + "copy", + "duplicate", + "object", + "object graph" + ], + "support": { + "issues": "https://github.com/myclabs/DeepCopy/issues", + "source": "https://github.com/myclabs/DeepCopy/tree/1.13.4" + }, + "funding": [ + { + "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy", + "type": "tidelift" + } + ], + "time": "2025-08-01T08:46:24+00:00" + }, { "name": "nesbot/carbon", "version": "3.10.0", @@ -3717,119 +3417,6 @@ }, "time": "2023-04-04T09:54:51+00:00" }, - { - "name": "psr/http-server-handler", - "version": "1.0.2", - "source": { - "type": "git", - "url": "https://github.com/php-fig/http-server-handler.git", - "reference": "84c4fb66179be4caaf8e97bd239203245302e7d4" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/php-fig/http-server-handler/zipball/84c4fb66179be4caaf8e97bd239203245302e7d4", - "reference": "84c4fb66179be4caaf8e97bd239203245302e7d4", - "shasum": "" - }, - "require": { - "php": ">=7.0", - "psr/http-message": "^1.0 || ^2.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0.x-dev" - } - }, - "autoload": { - "psr-4": { - "Psr\\Http\\Server\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "PHP-FIG", - "homepage": "https://www.php-fig.org/" - } - ], - "description": "Common interface for HTTP server-side request handler", - "keywords": [ - "handler", - "http", - "http-interop", - "psr", - "psr-15", - "psr-7", - "request", - "response", - "server" - ], - "support": { - "source": "https://github.com/php-fig/http-server-handler/tree/1.0.2" - }, - "time": "2023-04-10T20:06:20+00:00" - }, - { - "name": "psr/http-server-middleware", - "version": "1.0.2", - "source": { - "type": "git", - "url": "https://github.com/php-fig/http-server-middleware.git", - "reference": "c1481f747daaa6a0782775cd6a8c26a1bf4a3829" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/php-fig/http-server-middleware/zipball/c1481f747daaa6a0782775cd6a8c26a1bf4a3829", - "reference": "c1481f747daaa6a0782775cd6a8c26a1bf4a3829", - "shasum": "" - }, - "require": { - "php": ">=7.0", - "psr/http-message": "^1.0 || ^2.0", - "psr/http-server-handler": "^1.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0.x-dev" - } - }, - "autoload": { - "psr-4": { - "Psr\\Http\\Server\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "PHP-FIG", - "homepage": "https://www.php-fig.org/" - } - ], - "description": "Common interface for HTTP server-side middleware", - "keywords": [ - "http", - "http-interop", - "middleware", - "psr", - "psr-15", - "psr-7", - "request", - "response" - ], - "support": { - "issues": "https://github.com/php-fig/http-server-middleware/issues", - "source": "https://github.com/php-fig/http-server-middleware/tree/1.0.2" - }, - "time": "2023-04-11T06:14:47+00:00" - }, { "name": "psr/log", "version": "3.0.2", @@ -3975,6 +3562,78 @@ }, "time": "2019-03-08T08:55:37+00:00" }, + { + "name": "setasign/fpdi", + "version": "v2.6.4", + "source": { + "type": "git", + "url": "https://github.com/Setasign/FPDI.git", + "reference": "4b53852fde2734ec6a07e458a085db627c60eada" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Setasign/FPDI/zipball/4b53852fde2734ec6a07e458a085db627c60eada", + "reference": "4b53852fde2734ec6a07e458a085db627c60eada", + "shasum": "" + }, + "require": { + "ext-zlib": "*", + "php": "^7.1 || ^8.0" + }, + "conflict": { + "setasign/tfpdf": "<1.31" + }, + "require-dev": { + "phpunit/phpunit": "^7", + "setasign/fpdf": "~1.8.6", + "setasign/tfpdf": "~1.33", + "squizlabs/php_codesniffer": "^3.5", + "tecnickcom/tcpdf": "^6.8" + }, + "suggest": { + "setasign/fpdf": "FPDI will extend this class but as it is also possible to use TCPDF or tFPDF as an alternative. There's no fixed dependency configured." + }, + "type": "library", + "autoload": { + "psr-4": { + "setasign\\Fpdi\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jan Slabon", + "email": "jan.slabon@setasign.com", + "homepage": "https://www.setasign.com" + }, + { + "name": "Maximilian Kresse", + "email": "maximilian.kresse@setasign.com", + "homepage": "https://www.setasign.com" + } + ], + "description": "FPDI is a collection of PHP classes facilitating developers to read pages from existing PDF documents and use them as templates in FPDF. Because it is also possible to use FPDI with TCPDF, there are no fixed dependencies defined. Please see suggestions for packages which evaluates the dependencies automatically.", + "homepage": "https://www.setasign.com/fpdi", + "keywords": [ + "fpdf", + "fpdi", + "pdf" + ], + "support": { + "issues": "https://github.com/Setasign/FPDI/issues", + "source": "https://github.com/Setasign/FPDI/tree/v2.6.4" + }, + "funding": [ + { + "url": "https://tidelift.com/funding/github/packagist/setasign/fpdi", + "type": "tidelift" + } + ], + "time": "2025-08-05T09:57:14+00:00" + }, { "name": "shopwwi/filesystem-oss", "version": "v1.0.3", diff --git a/config/route.php b/config/route.php index 86aa1a0..887c683 100644 --- a/config/route.php +++ b/config/route.php @@ -35,6 +35,9 @@ Route::any('/feishu/event', [app\common\controller\NotifyController::class, 'fei Route::any('/feishu/callback', [app\common\controller\NotifyController::class, 'feishuCallback']); // 代理oss文件地址 Route::any('/proxy/ossurl', [app\common\controller\UploadController::class, 'proxy']); +Route::any('/upload', [app\common\controller\UploadController::class, 'uploadFile']); +Route::any('/upload/image', [app\common\controller\UploadController::class, 'uploadImage']); +Route::any('/upload/delete', [app\common\controller\UploadController::class, 'deleteFile']); // 公共 Route::group('/common', function () { @@ -61,8 +64,17 @@ Route::group('/common', function () { Route::any('/feishu/app', [app\common\controller\PresetController::class, 'feishuApp']); Route::any('/systems', [app\common\controller\PresetController::class, 'systems']); Route::any('/roles', [app\common\controller\PresetController::class, 'roles']); + Route::any('/currency', [app\common\controller\PresetController::class, 'currency']); Route::any('/cache/clear', [app\common\controller\PresetController::class, 'cacheClear']); Route::any('/menus', [app\common\controller\PresetController::class, 'menus']); + Route::any('/brand', [app\common\controller\PresetController::class, 'brand']); + Route::any('/brand/series', [app\common\controller\PresetController::class, 'series']); + Route::any('/account', [app\common\controller\PresetController::class, 'account']); + Route::any('/dict', [app\common\controller\PresetController::class, 'dict']); + Route::any('/dictItem', [app\common\controller\PresetController::class, 'dictItem']); + Route::any('/port', [app\common\controller\PresetController::class, 'port']); + Route::any('/color', [app\common\controller\PresetController::class, 'color']); + Route::any('/modification', [app\common\controller\PresetController::class, 'modification']); }); @@ -144,6 +156,7 @@ Route::group('/admin', function () { Route::any('/authorize', [app\admin\controller\UserController::class, 'authorize']); Route::any('/set/resources', [app\admin\controller\UserController::class, 'updateResources']); Route::any('/update/password', [app\admin\controller\UserController::class, 'savePassword']); + Route::any('/update/sign', [app\admin\controller\UserController::class, 'updateSign']); }); // 询单 @@ -181,6 +194,162 @@ Route::group('/admin', function () { Route::any('/follow', [app\crmAdmin\controller\CustomerController::class, 'follow']); Route::any('/allocation', [app\crmAdmin\controller\CustomerController::class, 'allocation']); }); + // 统计 + Route::group('/statistics', function () { + Route::any('/whole', [app\crmAdmin\controller\StatisticsController::class, 'whole']); + Route::any('/channel/count', [app\crmAdmin\controller\StatisticsController::class, 'channelCount']); + Route::any('/country/count', [app\crmAdmin\controller\StatisticsController::class, 'countryCount']); + Route::any('/track/count', [app\crmAdmin\controller\StatisticsController::class, 'trackCount']); + Route::any('/day/count', [app\crmAdmin\controller\StatisticsController::class, 'dayCount']); + Route::any('/customer/count', [app\crmAdmin\controller\StatisticsController::class, 'customerCount']); + }); + }); + + // 车辆 + Route::group('/vehicle', function () { + // 基础信息 + Route::group('/sku', function () { + Route::any('/list', [app\common\controller\VehVehicleSkuController::class, 'index']); + Route::any('/show', [app\common\controller\VehVehicleSkuController::class, 'show']); + Route::any('/store', [app\common\controller\VehVehicleSkuController::class, 'store']); + Route::any('/update', [app\common\controller\VehVehicleSkuController::class, 'update']); + Route::any('/destroy', [app\common\controller\VehVehicleSkuController::class, 'destroy']); + }); + }); + + // 字典 + Route::group('/dict', function () { + Route::any('/list', [app\admin\controller\SysDictController::class, 'index']); + Route::any('/show', [app\admin\controller\SysDictController::class, 'show']); + Route::any('/store', [app\admin\controller\SysDictController::class, 'store']); + Route::any('/update', [app\admin\controller\SysDictController::class, 'update']); + Route::any('/destroy', [app\admin\controller\SysDictController::class, 'destroy']); + Route::any('/scope', [app\admin\controller\SysDictController::class, 'listScope']); + // 字典分类 + Route::any('/group', [app\admin\controller\SysDictController::class, 'group']); + + Route::group('/item', function () { + Route::any('/list', [app\admin\controller\SysDictItemController::class, 'index']); + Route::any('/show', [app\admin\controller\SysDictItemController::class, 'show']); + Route::any('/store', [app\admin\controller\SysDictItemController::class, 'store']); + Route::any('/update', [app\admin\controller\SysDictItemController::class, 'update']); + Route::any('/destroy', [app\admin\controller\SysDictItemController::class, 'destroy']); + }); + + }); + + // 货币 + Route::group('/currency', function () { + Route::any('/list', [app\admin\controller\SysCurrencyController::class, 'index']); + Route::any('/show', [app\admin\controller\SysCurrencyController::class, 'show']); + Route::any('/store', [app\admin\controller\SysCurrencyController::class, 'store']); + Route::any('/update', [app\admin\controller\SysCurrencyController::class, 'update']); + Route::any('/destroy', [app\admin\controller\SysCurrencyController::class, 'destroy']); + }); + + // 品牌 + Route::group('/brand', function () { + Route::any('/list', [app\admin\controller\BrandController::class, 'index']); + Route::any('/show', [app\admin\controller\BrandController::class, 'show']); + Route::any('/store', [app\admin\controller\BrandController::class, 'store']); + Route::any('/update', [app\admin\controller\BrandController::class, 'update']); + Route::any('/hot', [app\admin\controller\BrandController::class, 'update']); + Route::any('/destroy', [app\admin\controller\BrandController::class, 'destroy']); + // 车系 + Route::group('/series', function () { + Route::any('/list', [app\admin\controller\BrandSeriesController::class, 'index']); + Route::any('/show', [app\admin\controller\BrandSeriesController::class, 'show']); + Route::any('/store', [app\admin\controller\BrandSeriesController::class, 'store']); + Route::any('/update', [app\admin\controller\BrandSeriesController::class, 'update']); + Route::any('/destroy', [app\admin\controller\BrandSeriesController::class, 'destroy']); + }); + }); + + // 汽车颜色 + Route::group('/color', function () { + Route::any('/list', [app\admin\controller\VehColorController::class, 'index']); + Route::any('/show', [app\admin\controller\VehColorController::class, 'show']); + Route::any('/store', [app\admin\controller\VehColorController::class, 'store']); + Route::any('/update', [app\admin\controller\VehColorController::class, 'update']); + Route::any('/destroy', [app\admin\controller\VehColorController::class, 'destroy']); + }); + + // 汽车改装项目 + Route::group('/modification', function () { + Route::any('/list', [app\admin\controller\VehModificationController::class, 'index']); + Route::any('/show', [app\admin\controller\VehModificationController::class, 'show']); + Route::any('/store', [app\admin\controller\VehModificationController::class, 'store']); + Route::any('/update', [app\admin\controller\VehModificationController::class, 'update']); + Route::any('/destroy', [app\admin\controller\VehModificationController::class, 'destroy']); + }); + + // 账户 + Route::group('/account', function () { + Route::any('/list', [app\admin\controller\SysAccountController::class, 'index']); + Route::any('/show', [app\admin\controller\SysAccountController::class, 'show']); + Route::any('/store', [app\admin\controller\SysAccountController::class, 'store']); + Route::any('/update', [app\admin\controller\SysAccountController::class, 'update']); + Route::any('/destroy', [app\admin\controller\SysAccountController::class, 'destroy']); + }); + + // 账户 + Route::group('/port', function () { + Route::any('/list', [app\admin\controller\SysPortController::class, 'index']); + Route::any('/show', [app\admin\controller\SysPortController::class, 'show']); + Route::any('/store', [app\admin\controller\SysPortController::class, 'store']); + Route::any('/update', [app\admin\controller\SysPortController::class, 'update']); + Route::any('/destroy', [app\admin\controller\SysPortController::class, 'destroy']); + }); + + // 审批 + Route::group('/approval', function () { + Route::any('/list', [app\admin\controller\ApprovalFlowsController::class, 'index']); + Route::any('/show', [app\admin\controller\ApprovalFlowsController::class, 'show']); + Route::any('/store', [app\admin\controller\ApprovalFlowsController::class, 'store']); + Route::any('/update', [app\admin\controller\ApprovalFlowsController::class, 'update']); + Route::any('/destroy', [app\admin\controller\ApprovalFlowsController::class, 'destroy']); + }); + + // 审批记录 + Route::group('/approval/records', function () { + Route::any('/list', [app\admin\controller\ApprovalRecordsController::class, 'index']); + Route::any('/logs', [app\admin\controller\ApprovalRecordsController::class, 'logs']); + Route::any('/action', [app\admin\controller\ApprovalRecordsController::class, 'approve']); + + Route::any('/todo', [app\admin\controller\ApprovalRecordsController::class, 'todo']); + }); + + Route::group('/stock/order', function () { + Route::any('/list', [app\admin\controller\StockOrdersController::class, 'index']); + Route::any('/show', [app\admin\controller\StockOrdersController::class, 'show']); + Route::any('/store', [app\admin\controller\StockOrdersController::class, 'store']); + Route::any('/print', [app\admin\controller\StockOrdersController::class, 'print']); + Route::any('/detail', [app\admin\controller\StockOrdersController::class, 'detail']); + }); + + // crm + Route::group('/targets', function () { + Route::group('/monthly', function () { + Route::any('/list', [app\admin\targets\controller\SalesMonthlyTargetsController::class, 'index']); + Route::any('/store', [app\admin\targets\controller\SalesMonthlyTargetsController::class, 'store']); + Route::any('/update', [app\admin\targets\controller\SalesMonthlyTargetsController::class, 'update']); + Route::any('/destroy', [app\admin\targets\controller\SalesMonthlyTargetsController::class, 'destroy']); + Route::any('/show', [app\admin\targets\controller\SalesMonthlyTargetsController::class, 'show']); + }); + Route::group('/annual', function () { + Route::any('/list', [app\admin\targets\controller\SalesAnnualTargetsController::class, 'index']); + Route::any('/store', [app\admin\targets\controller\SalesAnnualTargetsController::class, 'store']); + Route::any('/update', [app\admin\targets\controller\SalesAnnualTargetsController::class, 'update']); + Route::any('/destroy', [app\admin\targets\controller\SalesAnnualTargetsController::class, 'destroy']); + Route::any('/show', [app\admin\targets\controller\SalesAnnualTargetsController::class, 'show']); + }); + Route::group('/statistics', function () { + Route::any('/total', [app\admin\targets\controller\StatisticsController::class, 'total']); + Route::any('/ranking', [app\admin\targets\controller\StatisticsController::class, 'ranking']); + Route::any('/user/month', [app\admin\targets\controller\StatisticsController::class, 'achievedUserByMonth']); + Route::any('/user/progressh', [app\admin\targets\controller\StatisticsController::class, 'userProgressh']); + Route::any('/user/achieved', [app\admin\targets\controller\StatisticsController::class, 'userAchieved']); + }); }); }); @@ -235,6 +404,7 @@ Route::group('/crm', function () { Route::any('/customer/followNext', [app\crm\controller\CustomerController::class, 'followNext']); Route::any('/customer/report', [app\crm\controller\CustomerController::class, 'report']); Route::any('/customer/distinctNotice', [app\crm\controller\CustomerController::class, 'distinctNotice']); + Route::any('/customer/preset', [app\crm\controller\CustomerController::class, 'preset']); Route::any('/tagGroup/list', [app\crm\controller\TagGroupController::class, 'list']); Route::any('/tagGroup/updatePivot', [app\crm\controller\TagGroupController::class, 'updatePivot']); @@ -244,5 +414,55 @@ Route::group('/crm', function () { Route::any('/user/edit', [app\common\controller\UserController::class, 'edit']); Route::any('/user/savePassword', [app\common\controller\UserController::class, 'savePassword']); -}); + // 基础信息 + Route::group('/stock/order', function () { + Route::any('/list', [app\crm\controller\StockOrdersController::class, 'index']); + Route::any('/show', [app\crm\controller\StockOrdersController::class, 'show']); + Route::any('/store', [app\crm\controller\StockOrdersController::class, 'store']); + Route::any('/update', [app\crm\controller\StockOrdersController::class, 'update']); + Route::any('/print', [app\crm\controller\StockOrdersController::class, 'print']); + Route::any('/approval', [app\crm\controller\StockOrdersController::class, 'approval']); + Route::any('/detail', [app\crm\controller\StockOrdersController::class, 'detail']); + Route::any('/ultimate/approval', [app\crm\controller\StockOrdersController::class, 'ultimateApproval']); + }); + + // 审批记录 + Route::group('/approval/records', function () { + Route::any('/list', [app\crm\controller\ApprovalRecordsController::class, 'index']); + Route::any('/logs', [app\crm\controller\ApprovalRecordsController::class, 'logs']); + Route::any('/action', [app\crm\controller\ApprovalRecordsController::class, 'approve']); + + Route::any('/todo', [app\admin\controller\ApprovalRecordsController::class, 'todo']); + }); + + // 汽车颜色 + Route::group('/color', function () { + Route::any('/list', [app\common\controller\VehColorController::class, 'index']); + Route::any('/show', [app\common\controller\VehColorController::class, 'show']); + Route::any('/store', [app\common\controller\VehColorController::class, 'store']); + Route::any('/update', [app\common\controller\VehColorController::class, 'update']); + Route::any('/destroy', [app\common\controller\VehColorController::class, 'destroy']); + }); + + // 车辆 + Route::group('/vehicle', function () { + // 基础信息 + Route::group('/sku', function () { + Route::any('/list', [app\common\controller\VehVehicleSkuController::class, 'index']); + Route::any('/show', [app\common\controller\VehVehicleSkuController::class, 'show']); + Route::any('/store', [app\common\controller\VehVehicleSkuController::class, 'store']); + Route::any('/update', [app\common\controller\VehVehicleSkuController::class, 'update']); + Route::any('/destroy', [app\common\controller\VehVehicleSkuController::class, 'destroy']); + }); + }); + + // 汽车改装项目 + Route::group('/modification', function () { + Route::any('/list', [app\common\controller\VehModificationController::class, 'index']); + Route::any('/show', [app\common\controller\VehModificationController::class, 'show']); + Route::any('/store', [app\common\controller\VehModificationController::class, 'store']); + Route::any('/update', [app\common\controller\VehModificationController::class, 'update']); + Route::any('/destroy', [app\common\controller\VehModificationController::class, 'destroy']); + }); +}); \ No newline at end of file diff --git a/public/01.jpg b/public/01.jpg new file mode 100644 index 0000000..d646fb2 Binary files /dev/null and b/public/01.jpg differ diff --git a/public/fonts/NotoSansSC-Bold.ttf b/public/fonts/NotoSansSC-Bold.ttf new file mode 100644 index 0000000..b9010df Binary files /dev/null and b/public/fonts/NotoSansSC-Bold.ttf differ diff --git a/public/fonts/NotoSansSC-Regular.ttf b/public/fonts/NotoSansSC-Regular.ttf new file mode 100644 index 0000000..4d4cadb Binary files /dev/null and b/public/fonts/NotoSansSC-Regular.ttf differ diff --git a/webman b/webman old mode 100755 new mode 100644 index 3f55062..c853676 --- a/webman +++ b/webman @@ -1,71 +1,71 @@ -#!/usr/bin/env php -load(); - } else { - Dotenv::createMutable(run_path())->load(); - } -} - -$appConfig = require $appConfigFile; -if ($timezone = $appConfig['default_timezone'] ?? '') { - date_default_timezone_set($timezone); -} - -if ($errorReporting = $appConfig['error_reporting'] ?? '') { - error_reporting($errorReporting); -} - -if (!in_array($argv[1] ?? '', ['start', 'restart', 'stop', 'status', 'reload', 'connections'])) { - require_once __DIR__ . '/support/bootstrap.php'; -} else { - if (class_exists('Support\App')) { - Support\App::loadAllConfig(['route']); - } else { - Config::reload(config_path(), ['route', 'container']); - } -} - -$cli = new Command(); -$cli->setName('webman cli'); -$cli->installInternalCommands(); -if (is_dir($command_path = Util::guessPath(app_path(), '/command', true))) { - $cli->installCommands($command_path); -} - -foreach (config('plugin', []) as $firm => $projects) { - if (isset($projects['app'])) { - foreach (['', '/app'] as $app) { - if ($command_str = Util::guessPath(base_path() . "/plugin/$firm{$app}", 'command')) { - $command_path = base_path() . "/plugin/$firm{$app}/$command_str"; - $cli->installCommands($command_path, "plugin\\$firm" . str_replace('/', '\\', $app) . "\\$command_str"); - } - } - } - foreach ($projects as $name => $project) { - if (!is_array($project)) { - continue; - } - $project['command'] ??= []; - array_walk($project['command'], [$cli, 'createCommandInstance']); - } -} - -$cli->run(); +#!/usr/bin/env php +load(); + } else { + Dotenv::createMutable(run_path())->load(); + } +} + +$appConfig = require $appConfigFile; +if ($timezone = $appConfig['default_timezone'] ?? '') { + date_default_timezone_set($timezone); +} + +if ($errorReporting = $appConfig['error_reporting'] ?? '') { + error_reporting($errorReporting); +} + +if (!in_array($argv[1] ?? '', ['start', 'restart', 'stop', 'status', 'reload', 'connections'])) { + require_once __DIR__ . '/support/bootstrap.php'; +} else { + if (class_exists('Support\App')) { + Support\App::loadAllConfig(['route']); + } else { + Config::reload(config_path(), ['route', 'container']); + } +} + +$cli = new Command(); +$cli->setName('webman cli'); +$cli->installInternalCommands(); +if (is_dir($command_path = Util::guessPath(app_path(), '/command', true))) { + $cli->installCommands($command_path); +} + +foreach (config('plugin', []) as $firm => $projects) { + if (isset($projects['app'])) { + foreach (['', '/app'] as $app) { + if ($command_str = Util::guessPath(base_path() . "/plugin/$firm{$app}", 'command')) { + $command_path = base_path() . "/plugin/$firm{$app}/$command_str"; + $cli->installCommands($command_path, "plugin\\$firm" . str_replace('/', '\\', $app) . "\\$command_str"); + } + } + } + foreach ($projects as $name => $project) { + if (!is_array($project)) { + continue; + } + $project['command'] ??= []; + array_walk($project['command'], [$cli, 'createCommandInstance']); + } +} + +$cli->run();