diff --git a/app/admin/controller/ApprovalFlowsController.php b/app/admin/controller/ApprovalFlowsController.php new file mode 100644 index 0000000..c2babff --- /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..483ec40 --- /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..1bf914e --- /dev/null +++ b/app/admin/controller/ApprovalRecordsController.php @@ -0,0 +1,90 @@ +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()); + } + } +} \ 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/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/logic/ApprovalFlowsLogic.php b/app/admin/logic/ApprovalFlowsLogic.php new file mode 100644 index 0000000..1608782 --- /dev/null +++ b/app/admin/logic/ApprovalFlowsLogic.php @@ -0,0 +1,17 @@ + 'array', + ]; +} \ No newline at end of file diff --git a/app/admin/model/ApprovalInstances.php b/app/admin/model/ApprovalInstances.php new file mode 100644 index 0000000..b09ff5d --- /dev/null +++ b/app/admin/model/ApprovalInstances.php @@ -0,0 +1,107 @@ + 'array', // 自动转换 JSON 字段为数组 + 'finished_at' => 'datetime', + 'deadline_at' => 'datetime', + 'created_at' => 'datetime', + 'updated_at' => 'datetime', + ]; + + /** + * 一个审批实例可以有多个审批记录 + */ + public function approvalRecords(): HasMany + { + return $this->hasMany(ApprovalRecords::class, 'instance_id'); + } + + /** + * 获取当前步骤的所有审批记录(pending 状态) + */ + public function currentStepRecords() + { + return $this->approvalRecords() + ->where('step_index', $this->current_step) + ->where('status', 'pending'); + } + + 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->hasOne(StockOrders::class, 'id', 'biz_id'); + } +} \ No newline at end of file diff --git a/app/admin/model/ApprovalRecords.php b/app/admin/model/ApprovalRecords.php new file mode 100644 index 0000000..a970ce4 --- /dev/null +++ b/app/admin/model/ApprovalRecords.php @@ -0,0 +1,84 @@ + 'datetime', + 'created_at' => 'datetime', + 'updated_at' => 'datetime', + ]; + + /** + * 一条审批记录属于一个审批实例 + */ + public function approvalInstance(): BelongsTo + { + return $this->belongsTo(ApprovalInstances::class, 'instance_id'); + } + + /** + * 关联审批人 + */ + public function approver() + { + return $this->belongsTo(User::class, 'approver_id'); + } +} \ No newline at end of file diff --git a/app/admin/service/ApprovalFlowsService.php b/app/admin/service/ApprovalFlowsService.php new file mode 100644 index 0000000..3f7a4e9 --- /dev/null +++ b/app/admin/service/ApprovalFlowsService.php @@ -0,0 +1,62 @@ +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/admin/service/ApprovalInstancesService.php b/app/admin/service/ApprovalInstancesService.php new file mode 100644 index 0000000..db9b0cb --- /dev/null +++ b/app/admin/service/ApprovalInstancesService.php @@ -0,0 +1,31 @@ +validator; + } +} \ No newline at end of file diff --git a/app/admin/service/ApprovalRecordsService.php b/app/admin/service/ApprovalRecordsService.php new file mode 100644 index 0000000..a371f41 --- /dev/null +++ b/app/admin/service/ApprovalRecordsService.php @@ -0,0 +1,91 @@ +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'] = [ + 'approvalInstance', + 'approver', + 'approvalInstance.flows', + 'approvalInstance.initiator', + 'approvalInstance.bizInfo' + ]; + + // $config['dataLimit']['type'] = 'relation'; + // $config['dataLimit']['userId'] = $request->user->uid; + // $config['dataLimit']['userField'] = 'approver_id'; + 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/admin/service/SysCurrencyService.php b/app/admin/service/SysCurrencyService.php index 19f0529..c3086da 100644 --- a/app/admin/service/SysCurrencyService.php +++ b/app/admin/service/SysCurrencyService.php @@ -5,6 +5,8 @@ namespace app\admin\service; use app\admin\validate\SysCurrencyValidate; use app\common\traits\crud\CrudServiceTrait; use app\common\model\SysCurrency; +use app\common\utils\translation\service\TranslationService; +use app\queue\utils\Translat; use support\Request; class SysCurrencyService @@ -58,4 +60,20 @@ class SysCurrencyService 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/SysDictItemService.php b/app/admin/service/SysDictItemService.php index 577b9f0..98430b9 100644 --- a/app/admin/service/SysDictItemService.php +++ b/app/admin/service/SysDictItemService.php @@ -5,6 +5,8 @@ namespace app\admin\service; use app\admin\validate\SysDictItemValidate; use app\common\traits\crud\CrudServiceTrait; use app\common\model\SysDictItem; +use app\common\utils\translation\service\TranslationService; +use app\queue\utils\Translat; use support\Request; class SysDictItemService @@ -72,4 +74,20 @@ class SysDictItemService 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 index 859628c..e46b07b 100644 --- a/app/admin/service/SysDictService.php +++ b/app/admin/service/SysDictService.php @@ -5,6 +5,9 @@ namespace app\admin\service; use app\admin\validate\SysDictValidate; use app\common\traits\crud\CrudServiceTrait; use app\common\model\SysDict; +use app\common\model\SysDictCategory; +use app\common\utils\translation\service\TranslationService; +use app\queue\utils\Translat; use support\Request; class SysDictService @@ -80,8 +83,17 @@ class SysDictService 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'])) { - $config['where']['id'] = ['in', explode(',', $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'; @@ -95,7 +107,7 @@ class SysDictService $config['with']['sysDictItem'][] = 'item_value'; break; } - + return $config; } @@ -124,4 +136,105 @@ class SysDictService 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 index 6e8774c..a487848 100644 --- a/app/admin/service/SysPortService.php +++ b/app/admin/service/SysPortService.php @@ -5,6 +5,8 @@ namespace app\admin\service; use app\admin\validate\SysPortValidate; use app\common\traits\crud\CrudServiceTrait; use app\common\model\SysPort; +use app\common\utils\translation\service\TranslationService; +use app\queue\utils\Translat; use support\Request; class SysPortService @@ -22,6 +24,8 @@ class SysPortService */ protected $validator = SysPortValidate::class; + public $translatFields = ['port_name','description']; + /** * 获取验证器 */ @@ -97,6 +101,20 @@ class SysPortService 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 + ); + } + /** * 添加前置处理 */ diff --git a/app/admin/service/UserService.php b/app/admin/service/UserService.php index 455b25b..7f2fadd 100644 --- a/app/admin/service/UserService.php +++ b/app/admin/service/UserService.php @@ -116,6 +116,7 @@ class UserService $config['where']['id'] = $p['id']; break; case 'edit': + case 'updateSign': $config['where']['id'] = $request->user->uid; break; } @@ -157,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 index acc5309..be83fcd 100644 --- a/app/admin/service/VehColorService.php +++ b/app/admin/service/VehColorService.php @@ -5,6 +5,8 @@ namespace app\admin\service; use app\admin\validate\VehColorValidate; use app\common\traits\crud\CrudServiceTrait; use app\common\model\VehColor; +use app\common\utils\translation\service\TranslationService; +use app\queue\utils\Translat; use support\Request; class VehColorService @@ -22,6 +24,8 @@ class VehColorService */ protected $validator = VehColorValidate::class; + public $translatFields = ['color_text']; + /** * 获取验证器 */ @@ -30,6 +34,20 @@ class VehColorService return new $this->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 + ); + } + /** * 添加前置处理 */ diff --git a/app/admin/validate/ApprovalFlowsValidate.php b/app/admin/validate/ApprovalFlowsValidate.php new file mode 100644 index 0000000..9b64000 --- /dev/null +++ b/app/admin/validate/ApprovalFlowsValidate.php @@ -0,0 +1,29 @@ + [ + + ], + + 'update' => [ + + ] + ]; +} \ No newline at end of file diff --git a/app/admin/validate/ApprovalInstancesValidate.php b/app/admin/validate/ApprovalInstancesValidate.php new file mode 100644 index 0000000..e6641be --- /dev/null +++ b/app/admin/validate/ApprovalInstancesValidate.php @@ -0,0 +1,29 @@ + [ + + ], + + 'update' => [ + + ] + ]; +} \ No newline at end of file diff --git a/app/admin/validate/ApprovalRecordsValidate.php b/app/admin/validate/ApprovalRecordsValidate.php new file mode 100644 index 0000000..796768c --- /dev/null +++ b/app/admin/validate/ApprovalRecordsValidate.php @@ -0,0 +1,29 @@ + [ + + ], + + 'update' => [ + + ] + ]; +} \ No newline at end of file diff --git a/app/common/approval/service/ApprovalFlowService.php b/app/common/approval/service/ApprovalFlowService.php deleted file mode 100644 index bf5d20c..0000000 --- a/app/common/approval/service/ApprovalFlowService.php +++ /dev/null @@ -1,626 +0,0 @@ -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("流程步骤配置错误"); - } - - // 2. 查找第一个有效步骤(考虑 condition) - $firstStep = $this->getNextValidStep($steps, $formData); - if (!$firstStep) { - throw new BusinessException("未找到可执行的审批步骤"); - } - - // 3. 创建审批实例 - $instanceId = Db::table('approval_instances')->insertGetId([ - 'flow_type' => $flowType, - 'biz_type' => $bizType, - 'biz_id' => $bizId, - 'form_data' => json_encode($formData, JSON_UNESCAPED_UNICODE), - 'initiator_id' => $initiatorId, - 'status' => 'processing', - 'current_step' => 0, - 'created_at' => date('Y-m-d H:i:s'), - 'updated_at' => date('Y-m-d H:i:s'), - ]); - - // 4. 创建审批记录(待办) - $approverIds = $this->resolveApprovers($firstStep, (object)['initiator_id' => $initiatorId]); - foreach ($approverIds as $approverId) { - Db::table('approval_records')->insert([ - 'instance_id' => $instanceId, - 'step_name' => $firstStep['name'], - 'approver_id' => $approverId, - 'status' => 'pending', - 'created_at' => date('Y-m-d H:i:s'), - ]); - } - - // 5. 通知审批人 - $this->sendNotification($approverIds, (object)['id' => $instanceId, 'flow_type' => $flowType], 'pending', false); - - // 6. 通知抄送人(当前步骤) - $this->notifyCCs($firstStep, (object)['id' => $instanceId, 'flow_type' => $flowType, 'form_data' => json_encode($formData)], 'pending'); - - return ['instance_id' => $instanceId]; - } - - /** - * 执行审批 - */ - public function approve(int $instanceId, int $approverId, string $action, string $comment = '') - { - $instance = Db::table('approval_instances')->where('id', $instanceId)->first(); - if (!$instance) { - throw new BusinessException("审批实例未找到"); - } - - $steps = json_decode($instance->steps, true) ?: json_decode(Db::table('approval_flows')->where('type', $instance->flow_type)->value('steps'), true); - $currentStep = $steps[$instance->current_step] ?? null; - - if (!$currentStep) { - throw new BusinessException("当前步骤配置错误"); - } - - // 检查用户是否有权限审批 - if (!$this->canUserApproveStep($currentStep, $approverId, $instance)) { - throw new BusinessException("您无权审批此步骤"); - } - - // 更新审批记录 - Db::table('approval_records') - ->where('instance_id', $instanceId) - ->where('approver_id', $approverId) - ->where('status', 'pending') - ->update([ - 'status' => $action, - 'comment' => $comment, - 'approved_at' => date('Y-m-d H:i:s'), - 'updated_at' => date('Y-m-d H:i:s'), - ]); - - // 检查当前步骤是否完成(根据 type: any, all, sequential) - $stepCompleted = $this->isStepCompleted($instanceId, $instance->current_step, $currentStep['type']); - - if (!$stepCompleted) { - // 步骤未完成,不进入下一步 - return ['status' => 'step_pending']; - } - - // 步骤已完成,进入下一步或结束 - $nextStep = $this->getNextValidStep($steps, json_decode($instance->form_data, true), $instance->current_step + 1); - - if ($nextStep === null) { - // 流程结束 - Db::table('approval_instances')->where('id', $instanceId)->update([ - 'status' => $action === 'approve' ? 'approved' : 'rejected', - 'updated_at' => date('Y-m-d H:i:s'), - ]); - - // 通知发起人结果 - $this->sendNotification([$instance->initiator_id], $instance, $action, false); - - return ['status' => 'completed', 'result' => $action]; - } - - // 进入下一步 - $nextStepIndex = array_search($nextStep, $steps); - Db::table('approval_instances')->where('id', $instanceId)->update([ - 'current_step' => $nextStepIndex, - 'updated_at' => date('Y-m-d H:i:s'), - ]); - - // 创建下一步的审批记录 - $nextApprovers = $this->resolveApprovers($nextStep, $instance); - foreach ($nextApprovers as $approverId) { - Db::table('approval_records')->insert([ - 'instance_id' => $instanceId, - 'step_name' => $nextStep['name'], - 'approver_id' => $approverId, - 'status' => 'pending', - 'created_at' => date('Y-m-d H:i:s'), - ]); - } - - // 通知下一步审批人 - $this->sendNotification($nextApprovers, $instance, 'pending', false); - - // 通知下一步抄送人 - $this->notifyCCs($nextStep, $instance, 'pending'); - - return ['status' => 'next_step', 'step_name' => $nextStep['name']]; - } - - /** - * 解析审批人 - */ - private function resolveApprovers(array $step, object $instance): array - { - $resolved = []; - $formData = json_decode($instance->form_data ?? '{}', true); - - foreach ($step['approvers'] as $approverConfig) { - $approverIds = []; - - switch ($approverConfig['type']) { - case 'role': - $approverIds = $this->getUserIdsByRole($approverConfig['value']); - break; - case 'user': - $approverIds = [(int)$approverConfig['value']]; - break; - case 'initiator_leader': - $leaderId = $this->getLeaderIdByUserId($instance->initiator_id); - if ($leaderId) $approverIds = [$leaderId]; - break; - case 'field_user': - if (isset($formData[$approverConfig['value']])) { - $approverIds = [(int)$formData[$approverConfig['value']]]; - } - break; - case 'dept_head': - $approverIds = $this->getDeptHeadId($approverConfig['value']); - if ($approverIds) $approverIds = [$approverIds]; - break; - default: - throw new BusinessException("不支持的审批人类型: {$approverConfig['type']}"); - } - - $resolved = array_merge($resolved, $approverIds); - } - - return array_unique(array_filter($resolved)); - } - - /** - * 通知抄送人 - */ - private function notifyCCs(array $step, object $instance, string $action = 'pending') - { - $ccConfig = $step['cc'] ?? []; - $ccUserIds = []; - $formData = json_decode($instance->form_data ?? '{}', true); - - foreach ($ccConfig as $cc) { - switch ($cc['type']) { - case 'role': - $ccUserIds = array_merge($ccUserIds, $this->getUserIdsByRole($cc['value'])); - break; - case 'user': - $ccUserIds[] = (int)$cc['value']; - break; - case 'initiator_leader': - $leaderId = $this->getLeaderIdByUserId($instance->initiator_id); - if ($leaderId) $ccUserIds[] = $leaderId; - break; - case 'field_user': - if (isset($formData[$cc['value']])) { - $ccUserIds[] = (int)$formData[$cc['value']]; - } - break; - case 'dept_head': - $deptHeadId = $this->getDeptHeadId($cc['value']); - if ($deptHeadId) $ccUserIds[] = $deptHeadId; - break; - default: - break; - } - } - - $ccUserIds = array_unique(array_filter($ccUserIds)); - - if (!empty($ccUserIds)) { - $this->sendNotification($ccUserIds, $instance, $action, true); - } - } - - /** - * 评估条件 - */ - private function evaluateCondition(?string $condition, array $formData): bool - { - if (!$condition) return true; - - $pattern = '/^(\w+)\s*(==|!=|>|<|>=|<=)\s*(.+)$/'; - if (!preg_match($pattern, $condition, $matches)) { - throw new BusinessException("条件格式错误: {$condition}"); - } - - $field = $matches[1]; - $op = $matches[2]; - $value = trim($matches[3], "\"'"); - - $actualValue = $formData[$field] ?? null; - - if (is_numeric($value) && is_numeric($actualValue)) { - $value = (float)$value; - $actualValue = (float)$actualValue; - } - - switch ($op) { - case '==': return $actualValue == $value; - case '!=': return $actualValue != $value; - case '>': return $actualValue > $value; - case '<': $actualValue < $value; - case '>=': return $actualValue >= $value; - case '<=': return $actualValue <= $value; - default: throw new BusinessException("不支持的操作符: {$op}"); - } - } - - /** - * 获取下一个有效步骤(考虑 condition) - */ - private function getNextValidStep(array $steps, array $formData, int $startIndex = 0): ?array - { - for ($i = $startIndex; $i < count($steps); $i++) { - $step = $steps[$i]; - if (!isset($step['condition']) || $this->evaluateCondition($step['condition'], $formData)) { - return $step; - } - } - return null; - } - - /** - * 检查用户是否有权限审批 - */ - private function canUserApproveStep(array $step, int $userId, object $instance): bool - { - $approvers = $this->resolveApprovers($step, $instance); - return in_array($userId, $approvers); - } - - /** - * 检查当前步骤是否完成 - */ - private function isStepCompleted(int $instanceId, int $stepIndex, string $type): bool - { - $pendingCount = Db::table('approval_records') - ->where('instance_id', $instanceId) - ->where('status', 'pending') - ->count(); - - if ($type === 'any') { - return $pendingCount === 0; // 任一通过即可 - } - - if ($type === 'all' || $type === 'sequential') { - return $pendingCount === 0; // 所有人都必须审批 - } - - return false; - } - - /** - * 发送通知(你可以替换为你的通知系统) - */ - private function sendNotification(array $userIds, object $instance, string $action, bool $isCC) - { - $actionText = [ - 'pending' => '待审批', - 'approve' => '已通过', - 'reject' => '已拒绝' - ][$action] ?? $action; - - $prefix = $isCC ? '[抄送]' : ''; - $message = "{$prefix}您有一条审批 {$actionText}:流程ID {$instance->id},类型 {$instance->flow_type}"; - - // TODO: 调用你的消息系统,如邮件、站内信、企业微信等 - // 示例:MessageService::send($userIds, $message); - echo "通知用户 " . implode(',', $userIds) . ":$message\n"; - } - - /** - * 获取用户待审批列表 - * @param int $userId 用户ID - * @return array - */ - public function getPendingApprovals(int $userId): array - { - // 简化查询:找到用户需要审批的实例ID - // 更复杂的逻辑(如并行会签)需要更精细的查询 - $instanceIds = Db::table('approval_records') - ->where('approver_id', $userId) - ->pluck('instance_id') - ->unique() - ->toArray(); - - if (empty($instanceIds)) { - return []; - } - - return Db::table('approval_instances') - ->whereIn('id', $instanceIds) - ->where('status', 'processing') - ->get() - ->toArray(); - } - - /** - * 获取审批历史记录 - * @param int $instanceId - * @return array - */ - public function getApprovalHistory(int $instanceId): array - { - return Db::table('approval_records') - ->where('instance_id', $instanceId) - ->orderBy('step_index') - ->orderBy('approved_at') - ->get() - ->toArray(); - } - - /** - * 获取审批流程图数据 (简化版) - * @param int $instanceId - * @return array - */ - 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') - ->orderBy('approved_at') - ->get() - ->groupBy('step_index'); - - $diagramData = []; - foreach ($steps as $index => $step) { - $status = 'pending'; - $actions = $records->get($index, collect())->toArray(); - - if (!empty($actions)) { - $hasReject = collect($actions)->contains('action', 'reject'); - if ($hasReject) { - $status = 'rejected'; - } else { - if ($step['type'] === 'parallel') { - // 简化处理 - $status = 'approved'; - } else { - $status = 'approved'; - } - } - } elseif ($index < $instance->current_step) { - $status = 'approved'; - } elseif ($index == $instance->current_step) { - $status = 'processing'; - } - - $diagramData[] = [ - 'index' => $index, - 'name' => $step['name'], - 'type' => $step['type'], - 'status' => $status, - 'actions' => $actions - ]; - } - return $diagramData; - } - - /** - * 处理超时的审批实例 (定时任务调用) - */ - public function handleTimeouts() - { - $now = date('Y-m-d H:i:s'); - $expiredInstances = Db::table('approval_instances') - ->where('status', 'processing') - ->where('deadline_at', '<=', $now) - ->get(); - - foreach ($expiredInstances as $instance) { - $flow = Db::table('approval_flows')->where('type', $instance->flow_type)->first(); - if ($flow && $flow->timeout_action) { - $updateData = [ - 'finished_at' => $now, - 'updated_at' => $now - ]; - if ($flow->timeout_action === 'auto_reject') { - $updateData['status'] = 'rejected'; - Db::table('approval_instances')->where('id', $instance->id)->update($updateData); - $this->notifyInitiator($instance, 'rejected_timeout'); - } elseif ($flow->timeout_action === 'auto_approve') { - $updateData['status'] = 'approved'; - Db::table('approval_instances')->where('id', $instance->id)->update($updateData); - $this->notifyInitiator($instance, 'approved_timeout'); - } - } - } - } - - // --- 辅助方法 --- - - // private function canUserApproveStep(array $step, int $userId, object $instance): bool - // { - // $approvers = $this->resolveApprovers($step, $instance); - // return in_array($userId, $approvers); - // } - - // private function resolveApprovers(array $step, object $instance): array - // { - // $resolved = []; - // foreach ($step['approvers'] as $approver) { - // if ($approver['type'] === 'role') { - // // 示例:根据角色查找用户ID,你需要替换为实际逻辑 - // if ($approver['value'] === 'admin') { - // $resolved[] = 1; // 假设 admin 角色的用户ID是1 - // } elseif ($approver['value'] === 'manager') { - // $resolved[] = 2; $resolved[] = 3; // 假设有多个manager - // } - // } elseif ($approver['type'] === 'user') { - // $resolved[] = (int)$approver['value']; - // } elseif ($approver['type'] === 'initiator_leader') { - // // 示例:获取发起人的领导 - // $resolved[] = 4; // 假设领导ID是4 - // } - // } - // return array_unique($resolved); - // } - - // private function resolveApprovers(array $step, object $instance): array - // { - // $resolved = []; - - // foreach ($step['approvers'] as $approverConfig) { - // $approverIds = []; - - // switch ($approverConfig['type']) { - // // 1. 按角色查找 - // case 'role': - // $roleCode = $approverConfig['value']; - // // 所有拥有该角色的用户ID - // $approverIds = $this->getUserIdsByRole($roleCode); - // break; - - // // 2. 按具体用户ID - // case 'user': - // $approverIds = [(int)$approverConfig['value']]; - // break; - - // // 3. 按发起人的直属领导 (业务一般是组长) - // case 'initiator_leader': - // // 用户表中 report_to 字段 - // $approverIds = $this->getLeaderIdByUserId($instance->initiator_id); - // if ($approverIds) { - // $approverIds = [$approverIds]; - // } else { - // $approverIds = []; // 没有领导,跳过 - // } - // break; - - // // 4. 按业务字段关联的用户 (高级功能) - // case 'field_user': - // $fieldName = $approverConfig['value']; // 如 'project_manager_id' - // $formData = json_decode($instance->form_data, true); - // if (isset($formData[$fieldName])) { - // $approverIds = [(int)$formData[$fieldName]]; - // } else { - // $approverIds = []; - // } - // break; - - // // 5. 按组织架构层级 (如发起人所在部门的负责人) - // case 'dept_head': - // $deptId = $approverConfig['value']; // 也可以从 instance 或 formData 中获取 - // $approverIds = $this->getDeptHeadId($deptId); - // if ($approverIds) { - // $approverIds = [$approverIds]; - // } else { - // $approverIds = []; - // } - // break; - - // default: - // throw new BusinessException("不支持的审批人类型: {$approverConfig['type']}"); - // } - - // $resolved = array_merge($resolved, $approverIds); - // } - - // return array_unique($resolved); - // } - - // 角色层 - private function getUserIdsByRole(string $roleCode): array - { - // user_roles 表和 roles 表 - return Db::table('user_roles as ur') - ->join('roles as r', 'ur.role_id', '=', 'r.id') - ->where('r.code', $roleCode) // 假设 roles 表有 code 字段 - ->pluck('ur.user_id') - ->toArray(); - } - - // 领导层 - private function getLeaderIdByUserId(int $userId): ?int - { - // users 表 report_to 字段 - return Db::table('user')->where('id', $userId)->value('report_to'); - } - - // 组织架构层 - private function getDeptHeadId(int $deptId): ?int - { - // departments 表 head_user_id 字段(部门逻辑暂时不用,根据后续业务实际情况开发) - return Db::table('departments')->where('id', $deptId)->value('head_user_id'); - } - - private function calculateNextStepIndex(int $instanceId, int $currentStepIndex, array $currentStep): int - { - // 对于 sequential 和 any,单次 approve 即进入下一步 - if ($currentStep['type'] === 'sequential' || $currentStep['type'] === 'any') { - return $currentStepIndex + 1; - } elseif ($currentStep['type'] === 'parallel') { - // 对于并行会签,需要检查是否所有审批人都已 approve - // 这里简化处理,实际需要精确计算 - // 假设只要有一次 approve 就算完成(不准确,仅为示例) - // 正确做法是查询所有 approvers 是否都 approve 了 - return $currentStepIndex + 1; - } - return $currentStepIndex + 1; - } - - private function notifyNextApprovers(int $instanceId) - { - // 实现你的通知逻辑,如发送站内信、邮件、企业微信等 - // 可以查询 instance_id 对应的当前步骤审批人 - echo "通知下一个审批人 (Instance ID: {$instanceId})\n"; - } - - private function notifyInitiator(object $instance, string $result) - { - // 通知发起人审批结果 - echo "通知发起人 {$instance->initiator_id} 审批结果: {$result}\n"; - } - - private function notifyAllApprovers(object $instance, string $status) - { - // 通知所有相关审批人(如撤回时) - echo "通知所有审批人,实例 {$instance->id} 状态变为: {$status}\n"; - } - - private function notifyCc(int $instanceId) - { - // 通知抄送人 - echo "通知抄送人 (Instance ID: {$instanceId})\n"; - } -} \ No newline at end of file diff --git a/app/common/approval/service/ApprovalFlowService_bak.php b/app/common/approval/service/ApprovalFlowService_bak.php deleted file mode 100644 index e6e13e7..0000000 --- a/app/common/approval/service/ApprovalFlowService_bak.php +++ /dev/null @@ -1,431 +0,0 @@ - true, 'instance_id' => ...] 或抛出异常 - */ - public function start(string $flowType, string $bizType, string $bizId, array $formData, int $initiatorId): array - { - // 1. 获取流程定义 - $flow = Db::table('approval_flows') - ->where('type', $flowType) - ->where('is_active', 1) - ->first(); - - if (!$flow) { - throw new BusinessException("审批流程 '{$flowType}' 不存在或已停用"); - } - - // 2. 创建审批实例 - $instanceId = Db::table('approval_instances')->insertGetId([ - 'flow_type' => $flowType, - 'biz_id' => $bizId, - 'biz_type' => $bizType, - 'form_data' => json_encode($formData), - 'initiator_id' => $initiatorId, - 'status' => 'processing', - 'current_step' => 0, - 'deadline_at' => $flow->timeout_hours > 0 ? - (new DateTime())->modify("+{$flow->timeout_hours} hours")->format('Y-m-d H:i:s') : null, - 'created_at' => date('Y-m-d H:i:s'), - 'updated_at' => date('Y-m-d H:i:s'), - ]); - - // 3. 通知下一个审批人 - $this->notifyNextApprovers($instanceId); - - return ['success' => true, 'instance_id' => $instanceId]; - } - - /** - * 执行审批操作 - * @param int $instanceId 审批实例ID - * @param int $approverId 审批人ID - * @param string $action 操作 (approve, reject, revoke) - * @param string $comment 审批意见 - * @return array 操作结果 - */ - public function approve(int $instanceId, int $approverId, string $action, string $comment = ''): array - { - // 1. 获取实例和流程 - $instance = Db::table('approval_instances')->find($instanceId); - if (!$instance) { - throw new BusinessException("审批实例不存在"); - } - if ($instance->status !== 'processing') { - 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); - $currentStepIndex = $instance->current_step; - - if ($currentStepIndex >= count($steps)) { - throw new BusinessException("审批流程已结束"); - } - - $currentStep = $steps[$currentStepIndex]; - - // 2. 验证权限和条件 - if (!$this->canUserApproveStep($currentStep, $approverId, $instance)) { - throw new BusinessException("您无权审批此步骤"); - } - - // 3. 记录审批行为 - Db::table('approval_records')->insert([ - 'instance_id' => $instanceId, - 'approver_id' => $approverId, - 'step_index' => $currentStepIndex, - 'action' => $action, - 'comment' => $comment, - 'approved_at' => date('Y-m-d H:i:s'), - 'created_at' => date('Y-m-d H:i:s'), - 'updated_at' => date('Y-m-d H:i:s'), - ]); - - // 4. 处理审批结果 - $updateData = ['updated_at' => date('Y-m-d H:i:s')]; - if ($action === 'reject') { - $updateData['status'] = 'rejected'; - $updateData['finished_at'] = date('Y-m-d H:i:s'); - Db::table('approval_instances')->where('id', $instanceId)->update($updateData); - $this->notifyInitiator($instance, 'rejected'); - return ['result' => 'rejected']; - } - - if ($action === 'revoke') { - if ($instance->initiator_id !== $approverId || $instance->status !== 'processing') { - throw new BusinessException("无权撤回或状态不允许"); - } - $updateData['status'] = 'revoked'; - $updateData['finished_at'] = date('Y-m-d H:i:s'); - Db::table('approval_instances')->where('id', $instanceId)->update($updateData); - $this->notifyAllApprovers($instance, 'revoked'); - return ['result' => 'revoked']; - } - - // 5. 处理 approve 逻辑 - $nextStepIndex = $this->calculateNextStepIndex($instanceId, $currentStepIndex, $currentStep); - $updateData['current_step'] = $nextStepIndex; - - if ($nextStepIndex >= count($steps)) { - $updateData['status'] = 'approved'; - $updateData['finished_at'] = date('Y-m-d H:i:s'); - Db::table('approval_instances')->where('id', $instanceId)->update($updateData); - $this->notifyInitiator($instance, 'approved'); - $this->notifyCc($instanceId); - } else { - Db::table('approval_instances')->where('id', $instanceId)->update($updateData); - $this->notifyNextApprovers($instanceId); - } - - return ['result' => 'approved', 'next_step' => $nextStepIndex]; - } - - /** - * 获取用户待审批列表 - * @param int $userId 用户ID - * @return array - */ - public function getPendingApprovals(int $userId): array - { - // 简化查询:找到用户需要审批的实例ID - // 更复杂的逻辑(如并行会签)需要更精细的查询 - $instanceIds = Db::table('approval_records') - ->where('approver_id', $userId) - ->pluck('instance_id') - ->unique() - ->toArray(); - - if (empty($instanceIds)) { - return []; - } - - return Db::table('approval_instances') - ->whereIn('id', $instanceIds) - ->where('status', 'processing') - ->get() - ->toArray(); - } - - /** - * 获取审批历史记录 - * @param int $instanceId - * @return array - */ - public function getApprovalHistory(int $instanceId): array - { - return Db::table('approval_records') - ->where('instance_id', $instanceId) - ->orderBy('step_index') - ->orderBy('approved_at') - ->get() - ->toArray(); - } - - /** - * 获取审批流程图数据 (简化版) - * @param int $instanceId - * @return array - */ - 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') - ->orderBy('approved_at') - ->get() - ->groupBy('step_index'); - - $diagramData = []; - foreach ($steps as $index => $step) { - $status = 'pending'; - $actions = $records->get($index, collect())->toArray(); - - if (!empty($actions)) { - $hasReject = collect($actions)->contains('action', 'reject'); - if ($hasReject) { - $status = 'rejected'; - } else { - if ($step['type'] === 'parallel') { - // 简化处理 - $status = 'approved'; - } else { - $status = 'approved'; - } - } - } elseif ($index < $instance->current_step) { - $status = 'approved'; - } elseif ($index == $instance->current_step) { - $status = 'processing'; - } - - $diagramData[] = [ - 'index' => $index, - 'name' => $step['name'], - 'type' => $step['type'], - 'status' => $status, - 'actions' => $actions - ]; - } - return $diagramData; - } - - /** - * 处理超时的审批实例 (定时任务调用) - */ - public function handleTimeouts() - { - $now = date('Y-m-d H:i:s'); - $expiredInstances = Db::table('approval_instances') - ->where('status', 'processing') - ->where('deadline_at', '<=', $now) - ->get(); - - foreach ($expiredInstances as $instance) { - $flow = Db::table('approval_flows')->where('type', $instance->flow_type)->first(); - if ($flow && $flow->timeout_action) { - $updateData = [ - 'finished_at' => $now, - 'updated_at' => $now - ]; - if ($flow->timeout_action === 'auto_reject') { - $updateData['status'] = 'rejected'; - Db::table('approval_instances')->where('id', $instance->id)->update($updateData); - $this->notifyInitiator($instance, 'rejected_timeout'); - } elseif ($flow->timeout_action === 'auto_approve') { - $updateData['status'] = 'approved'; - Db::table('approval_instances')->where('id', $instance->id)->update($updateData); - $this->notifyInitiator($instance, 'approved_timeout'); - } - } - } - } - - // --- 辅助方法 --- - - private function canUserApproveStep(array $step, int $userId, object $instance): bool - { - $approvers = $this->resolveApprovers($step, $instance); - return in_array($userId, $approvers); - } - - // private function resolveApprovers(array $step, object $instance): array - // { - // $resolved = []; - // foreach ($step['approvers'] as $approver) { - // if ($approver['type'] === 'role') { - // // 示例:根据角色查找用户ID,你需要替换为实际逻辑 - // if ($approver['value'] === 'admin') { - // $resolved[] = 1; // 假设 admin 角色的用户ID是1 - // } elseif ($approver['value'] === 'manager') { - // $resolved[] = 2; $resolved[] = 3; // 假设有多个manager - // } - // } elseif ($approver['type'] === 'user') { - // $resolved[] = (int)$approver['value']; - // } elseif ($approver['type'] === 'initiator_leader') { - // // 示例:获取发起人的领导 - // $resolved[] = 4; // 假设领导ID是4 - // } - // } - // return array_unique($resolved); - // } - - private function resolveApprovers(array $step, object $instance): array - { - $resolved = []; - - foreach ($step['approvers'] as $approverConfig) { - $approverIds = []; - - switch ($approverConfig['type']) { - // 1. 按角色查找 - case 'role': - $roleCode = $approverConfig['value']; - // 所有拥有该角色的用户ID - $approverIds = $this->getUserIdsByRole($roleCode); - break; - - // 2. 按具体用户ID - case 'user': - $approverIds = [(int)$approverConfig['value']]; - break; - - // 3. 按发起人的直属领导 (业务一般是组长) - case 'initiator_leader': - // 用户表中 report_to 字段 - $approverIds = $this->getLeaderIdByUserId($instance->initiator_id); - if ($approverIds) { - $approverIds = [$approverIds]; - } else { - $approverIds = []; // 没有领导,跳过 - } - break; - - // 4. 按业务字段关联的用户 (高级功能) - case 'field_user': - $fieldName = $approverConfig['value']; // 如 'project_manager_id' - $formData = json_decode($instance->form_data, true); - if (isset($formData[$fieldName])) { - $approverIds = [(int)$formData[$fieldName]]; - } else { - $approverIds = []; - } - break; - - // 5. 按组织架构层级 (如发起人所在部门的负责人) - case 'dept_head': - $deptId = $approverConfig['value']; // 也可以从 instance 或 formData 中获取 - $approverIds = $this->getDeptHeadId($deptId); - if ($approverIds) { - $approverIds = [$approverIds]; - } else { - $approverIds = []; - } - break; - - default: - throw new BusinessException("不支持的审批人类型: {$approverConfig['type']}"); - } - - $resolved = array_merge($resolved, $approverIds); - } - - return array_unique($resolved); - } - - // 角色层 - private function getUserIdsByRole(string $roleCode): array - { - // user_roles 表和 roles 表 - return Db::table('user_roles as ur') - ->join('roles as r', 'ur.role_id', '=', 'r.id') - ->where('r.code', $roleCode) // 假设 roles 表有 code 字段 - ->pluck('ur.user_id') - ->toArray(); - } - - // 领导层 - private function getLeaderIdByUserId(int $userId): ?int - { - // users 表 report_to 字段 - return Db::table('user')->where('id', $userId)->value('report_to'); - } - - // 组织架构层 - private function getDeptHeadId(int $deptId): ?int - { - // departments 表 head_user_id 字段(部门逻辑暂时不用,根据后续业务实际情况开发) - return Db::table('departments')->where('id', $deptId)->value('head_user_id'); - } - - private function calculateNextStepIndex(int $instanceId, int $currentStepIndex, array $currentStep): int - { - // 对于 sequential 和 any,单次 approve 即进入下一步 - if ($currentStep['type'] === 'sequential' || $currentStep['type'] === 'any') { - return $currentStepIndex + 1; - } elseif ($currentStep['type'] === 'parallel') { - // 对于并行会签,需要检查是否所有审批人都已 approve - // 这里简化处理,实际需要精确计算 - // 假设只要有一次 approve 就算完成(不准确,仅为示例) - // 正确做法是查询所有 approvers 是否都 approve 了 - return $currentStepIndex + 1; - } - return $currentStepIndex + 1; - } - - private function notifyNextApprovers(int $instanceId) - { - // 实现你的通知逻辑,如发送站内信、邮件、企业微信等 - // 可以查询 instance_id 对应的当前步骤审批人 - echo "通知下一个审批人 (Instance ID: {$instanceId})\n"; - } - - private function notifyInitiator(object $instance, string $result) - { - // 通知发起人审批结果 - echo "通知发起人 {$instance->initiator_id} 审批结果: {$result}\n"; - } - - private function notifyAllApprovers(object $instance, string $status) - { - // 通知所有相关审批人(如撤回时) - echo "通知所有审批人,实例 {$instance->id} 状态变为: {$status}\n"; - } - - private function notifyCc(int $instanceId) - { - // 通知抄送人 - echo "通知抄送人 (Instance ID: {$instanceId})\n"; - } -} \ No newline at end of file diff --git a/app/common/model/StockOrderItems.php b/app/common/model/StockOrderItems.php index 610f694..f45c532 100644 --- a/app/common/model/StockOrderItems.php +++ b/app/common/model/StockOrderItems.php @@ -43,21 +43,9 @@ class StockOrderItems extends Model */ protected $fillable = [ 'id', 'order_id', 'product_code', 'sku_id', 'sku_snapshot', - 'model_snapshot', 'quantity', 'color_interior_coide', 'color_interior_snapshot', - 'color_exterior_coide', 'color_exterior_snapshot', 'remark', - // 'unit_price', - // 'handling_fee', - // 'fob_transport_fee', - // 'port_misc_fee', - // 'retrofit_fee', - // 'total_price', - 'created_at', 'updated_at', - // 'unit_price_value_id', - // 'handling_fee_value_id', - // 'fob_transport_fee_value_id', - // 'port_misc_fee_value_id', - // 'retrofit_fee_value_id', - // 'total_price_value_id' + 'model_snapshot', 'quantity', 'color_interior_code', 'color_interior_snapshot', + 'color_exterior_code', 'color_exterior_snapshot', 'remark', 'vin', 'created_at', 'updated_at', + 'mileage', 'unique_id' ]; /** diff --git a/app/common/model/StockOrders.php b/app/common/model/StockOrders.php index 123eb66..0b97fe4 100644 --- a/app/common/model/StockOrders.php +++ b/app/common/model/StockOrders.php @@ -3,6 +3,7 @@ namespace app\common\model; use support\Model; +use app\common\traits\HasMoneyJournal; class StockOrders extends Model { @@ -49,7 +50,7 @@ class StockOrders extends Model 'trade_term_id', 'trade_term_snapshot', 'clearance_port_id', 'clearance_port_snapshot', 'latest_arrival_date', 'contract_exchange_rate', 'settlement_currency', 'status', 'remark', 'created_at', 'updated_at', 'deleted_at', - 'object_id', 'contract', 'is_use', 'is_final', 'approval_status' + 'object_id', 'contract', 'is_use', 'is_final', 'approval_status', 'file_url', 'pick_up_date', 'collect_all_date', 'profit_total' ]; /** @@ -58,6 +59,13 @@ class StockOrders extends Model protected $guarded = []; + use HasMoneyJournal; + // 实现抽象方法 + protected static function moneyObjectScene(): string + { + return 'stock_orders'; + } + /** * 关联订单项 */ @@ -81,4 +89,10 @@ class StockOrders extends Model { 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/SysCurrencyTranslation.php b/app/common/model/SysCurrencyTranslation.php new file mode 100644 index 0000000..b6e5b68 --- /dev/null +++ b/app/common/model/SysCurrencyTranslation.php @@ -0,0 +1,49 @@ +hasMany(SysDictItem::class, 'dict_id', 'id'); } + + + public function category() + { + return $this->belongsTo(SysDictCategory::class, 'category_id', 'id'); + } } \ No newline at end of file diff --git a/app/common/model/SysDictCategory.php b/app/common/model/SysDictCategory.php index fef0219..cbca49a 100644 --- a/app/common/model/SysDictCategory.php +++ b/app/common/model/SysDictCategory.php @@ -46,4 +46,13 @@ class SysDictCategory extends Model * 不允许自动填充模型字段 */ protected $guarded = []; + + + public function dicts() + { + return $this->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/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 @@ +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']; @@ -55,6 +81,7 @@ class StockOrdersService $config['with'][] = 'items.moneyValues'; $config['with'][] = 'accounts'; $config['with'][] = 'moneyValues'; + $config['with'][] = 'salesperson'; break; case 'store': break; @@ -69,52 +96,6 @@ class StockOrdersService 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 - - // 车辆 - foreach ($data['veh_list'] as $veh) { - $veh['order_id'] = $created->id; - $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'], '优惠价'); - $item->addMoney('unit_price', $veh['unit_price']['currency'], $veh['unit_price']['amount'], $veh['unit_price']['rate'], '单价'); - $item->addMoney('total_price', $veh['total_price']['currency'], $veh['total_price']['amount'], $veh['total_price']['rate'], '总价'); - $item->addMoney('handling_fee', $veh['handling_fee']['currency'], $veh['handling_fee']['amount'], $veh['handling_fee']['rate'], '手续费'); - $item->addMoney('fob_transport_fee', $veh['fob_transport_fee']['currency'], $veh['fob_transport_fee']['amount'], $veh['fob_transport_fee']['rate'], 'FOB运输费'); - $item->addMoney('port_misc_fee', $veh['port_misc_fee']['currency'], $veh['port_misc_fee']['amount'], $veh['port_misc_fee']['rate'], '港杂'); - $item->addMoney('retrofit_fee', $veh['retrofit_fee']['currency'], $veh['retrofit_fee']['amount'], $veh['retrofit_fee']['rate'], '改装费'); - - // 改装 - if (count($veh['modify_list']) > 0) { - foreach ($veh['modify_list'] as $modify) { - $modify['order_item_id'] = $item->id; - StockOrderItemOptions::create($modify); // 表4 - } - } - } - - // 账户 - 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; @@ -125,21 +106,637 @@ class StockOrdersService $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) - // { + 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_order_flow', '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', // '利润' + // ]; - // return $res; - // } + 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 serviceShow(Request $request) + 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('Show', $item); + 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..dfaffda --- /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_order_flow', '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/UploadService.php b/app/common/service/UploadService.php index 33b7293..33b9137 100644 --- a/app/common/service/UploadService.php +++ b/app/common/service/UploadService.php @@ -50,7 +50,7 @@ class UploadService $size = filesize($filePath) ?: 0; $sceneUrl = $scene ? ($scene . '/') : ''; - $dir = 'vehicle_album/' . $sceneUrl . date('Ymd') . '/'; + $dir = 'x_server/' . $sceneUrl . date('Ymd') . '/'; $uniqueName = md5($originalName . microtime(true)); $objectKey = $dir . $uniqueName . ($extension ? ('.' . $extension) : ''); 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/approval/model/ApprovalCcs.php b/app/common/utils/approval/model/ApprovalCcs.php similarity index 95% rename from app/common/approval/model/ApprovalCcs.php rename to app/common/utils/approval/model/ApprovalCcs.php index 89be176..f44dfff 100644 --- a/app/common/approval/model/ApprovalCcs.php +++ b/app/common/utils/approval/model/ApprovalCcs.php @@ -1,6 +1,6 @@ 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', + ]; + + // 解析首步审批人(支持 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']; + }); + } + + /** 我的待办 */ + public function getPendingApprovals(int $userId): array + { + return Db::table('approval_records as ar') + ->join('approval_instances as ai', 'ar.instance_id', '=', 'ai.id') + ->where('ar.approver_id', $userId) + ->where('ar.status', 'pending') + ->where('ai.status', 'processing') + ->select('ai.*') + ->distinct() + ->get() + ->toArray(); + } + + /** 审批历史 */ + public function getApprovalHistory(int $instanceId): array + { + return Db::table('approval_records') + ->where('instance_id', $instanceId) + ->orderBy('step_index') + ->orderByRaw('COALESCE(approved_at, created_at) asc') + ->get() + ->toArray(); + } + + /** 流程图数据(把 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; + $msg = "{$prefix}您有一条审批 {$text}:流程ID {$instance->id},类型 {$instance->flow_type}"; + // MessageService::send($userIds, $msg); + + $pushData = [ + 'sender_id' => 0, + 'sender_type' => 'system', + 'title' => $msg, + 'content' => $msg, + 'scene' => 1, + 'recipient' => $userIds, + 'push_method' => 1 + ]; + $msgId = (new MessageNotificationsService())->pushNotice($pushData); + var_dump($msgId); + } + + 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 68aca42..e910504 100644 --- a/app/common/utils/email/Emails.php +++ b/app/common/utils/email/Emails.php @@ -431,12 +431,12 @@ class Emails $enterUser = User::where('id', $enterUserId)->first(); // 固定渠道 - $channel = 8; + $channel = 12; $channelInfo = InquiryChannel::where('id', $channel)->first(); // 初始化 UID 轮询器 $uidRotator = new \app\common\utils\RedisUidRotator('email_parsed_result_20250804'); - $uidRotator->setUids([64, 63, 83, 93, 108, 118, 132, 145, 150, 143, 156, 157, 158], false); + $uidRotator->setUids([64, 63, 83, 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) { @@ -452,7 +452,13 @@ class Emails $assignedUid = $uidRotator->next(); // 获取分配的用户信息 - $recipientInfo = User::where('id', $assignedUid)->first(); + $recipientInfo = User::where('id', $assignedUid)->where('status', 1)->first(); + if (!$recipientInfo) { + flg("用户ID=" . $assignedUid . " 不存在或已离职跳过分配,需要手动移除 UID"); + + $assignedUid = $uidRotator->next(); + $recipientInfo = User::where('id', $assignedUid)->where('status', 1)->first(); + } // 生成录单 $log = [ 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/translation/config/trans.php b/app/common/utils/translation/config/trans.php index e2c7702..83b9708 100644 --- a/app/common/utils/translation/config/trans.php +++ b/app/common/utils/translation/config/trans.php @@ -7,6 +7,26 @@ return [ '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', + ], ], // 默认支持的目标语种列表 diff --git a/app/controller/IndexController.php b/app/controller/IndexController.php index 1c69073..9779886 100644 --- a/app/controller/IndexController.php +++ b/app/controller/IndexController.php @@ -2,20 +2,11 @@ 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\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; -use support\Redis; class IndexController { @@ -77,27 +68,27 @@ class IndexController public function createResource() { $options = [ - // 'controller' => [ - // 'module' => 'common', - // 'name' => 'stock_orders' - // ], - 'model' => [ - 'module' => 'approval', - 'name' => 'approval_records' + 'controller' => [ + 'module' => 'crm', + 'name' => 'approval_flows' ], - // 'service' => [ - // 'module' => 'common', - // 'name' => 'stock_orders' - // ], - // 'logic' => [ - // 'module' => 'common', - // 'name' => 'stock_orders' - // ], - // 'validate' => [ - // 'module' => 'common', - // 'name' => 'stock_orders' - // ], - 'table' => 'approval_records' + 'model' => [ + 'module' => 'crm', + 'name' => 'approval_flows' + ], + 'service' => [ + 'module' => 'crm', + 'name' => 'approval_flows' + ], + 'logic' => [ + 'module' => 'crm', + 'name' => 'approval_flows' + ], + 'validate' => [ + 'module' => 'crm', + 'name' => 'approval_flows' + ], + 'table' => 'approval_flows' ]; $result = (new Generate($options))->doGenerate(); @@ -105,48 +96,48 @@ class IndexController } - public function test() + public function test(Request $request) { - $data = [ - 'id' => 1, - 'user_id' => 1, - 'leave_type_id' => 1, - 'start_date' => '2019-01-01', - 'end_date' => '2019-01-02', - 'reason' => 'test', - 'status' => 1 - ]; - $code = $this->submitLeaveApplication(1, $data); - - return $code; - } - // 假设这是你的业务逻辑 - public function submitLeaveApplication(int $bizId, $leaveData) { - // 1. 保存业务数据 - // $bizId = DB::table('leave_applications')->insertGetId($leaveData); - - // 2. 启动审批流程 - $service = new \app\common\approval\service\ApprovalFlowService(); + // $flowType = 'stock_order_flow'; + // $bizType = 'stock_orders'; + // $bizId = 32; + // $formData = []; + // $initiatorId = 74; + + // $appro = new \app\common\utils\approval\service\ApprovalFlowService(); + // // $sp = $appro->start($flowType, $bizType, $bizId, $formData, $initiatorId); + + // $sp = $appro->approve(11, 1, 'reject'); + // var_dump($sp); + + // return json([ + // 'code' => 0, + // 'msg' => 'success' + // ]); + + // $param = [ + // 'path' => public_path().'/pdf/BH20250908014.pdf', + // 'scene' => 'stock_orders', + // 'category' => 'pdf', + // 'original_name' => 'BH20250908014.pdf' + // ]; + + // $up = new UploadService(); + // $res = $up->uploadLocalFile($param); + // var_dump($res); + $param = $request->all(); try { - $result = $service->start( - 'leave', // 流程类型,对应 approval_flows.type - 'leave_application', // 业务类型标识 - $bizId, // 业务ID - $leaveData, // 表单数据快照 - $_SESSION['user_id'] ?? 1 // 发起人ID - ); - - // 3. 将审批实例ID关联回业务表 - DB::table('leave_applications') - ->where('id', $bizId) - ->update(['approval_instance_id' => $result['instance_id']]); - - return ['status' => 'success', 'message' => '申请已提交,等待审批']; - } catch (\Exception $e) { - // 处理错误,可能需要回滚业务数据插入 - return ['status' => 'error', 'message' => $e->getMessage()]; + // $service = new \app\crm\service\StockOrdersService(); + // $result = $service->createdPdfFile($param['id']); + // return json($result); + + \Webman\RedisQueue\Redis::send('stock-order-pdf', ['id'=>82]); + } catch (\Throwable $th) { + //throw $th; + var_dump($th->getMessage().$th->getLine().$th->getFile()); } + } } \ 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..7eacb12 --- /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..e854af6 --- /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..3edc6b5 --- /dev/null +++ b/app/crm/controller/ApprovalRecordsController.php @@ -0,0 +1,89 @@ +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()); + } + } +} \ No newline at end of file diff --git a/app/crm/controller/CustomerController.php b/app/crm/controller/CustomerController.php index b2e125e..abfed18 100644 --- a/app/crm/controller/CustomerController.php +++ b/app/crm/controller/CustomerController.php @@ -314,14 +314,10 @@ class CustomerController extends BaseController if (Util::isEmpty($channel_id)) { return $this->error(trans('channel_not_null')); } - $country_id = Util::getParam($request, 'country_id', 0); - if (Util::isEmpty($country_id)) { -// return $this->error('country_id'.trans('not_null')); + $country_id = Util::getParam($request, 'country_id', 245); + if(empty($country_id)){ + $country_id = 245; } -// $car_type_id = Util::getParam($request, 'car_type_id'); -// if (Util::isEmpty($car_type_id)) { -// return $this->error(trans('car_type_not_null')); -// } $contacts = Util::getParam($request, 'contacts'); $email = Util::getParam($request, 'email'); $phone = Util::getParam($request, 'phone'); diff --git a/app/crm/controller/StockOrdersController.php b/app/crm/controller/StockOrdersController.php index adc9d92..b4d2034 100644 --- a/app/crm/controller/StockOrdersController.php +++ b/app/crm/controller/StockOrdersController.php @@ -3,11 +3,15 @@ namespace app\crm\controller; use app\common\controller\BaseController; use app\common\traits\crud\CrudControllerTrait; -use app\common\service\StockOrdersService; +use app\crm\service\StockOrdersService; use support\Request; class StockOrdersController extends BaseController { + protected $noNeedLogin = ['print']; + + protected $noNeedHeaderData = ['print']; + use CrudControllerTrait; protected $service; @@ -23,20 +27,162 @@ class StockOrdersController extends BaseController * @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 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 index(Request $request) + 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 desc(Request $request) { $data = [ diff --git a/app/crm/logic/ApprovalFlowsLogic.php b/app/crm/logic/ApprovalFlowsLogic.php new file mode 100644 index 0000000..8a5260f --- /dev/null +++ b/app/crm/logic/ApprovalFlowsLogic.php @@ -0,0 +1,17 @@ + 'array', + ]; +} \ No newline at end of file diff --git a/app/crm/model/ApprovalInstances.php b/app/crm/model/ApprovalInstances.php new file mode 100644 index 0000000..b5a9a7b --- /dev/null +++ b/app/crm/model/ApprovalInstances.php @@ -0,0 +1,107 @@ + 'array', // 自动转换 JSON 字段为数组 + 'finished_at' => 'datetime', + 'deadline_at' => 'datetime', + 'created_at' => 'datetime', + 'updated_at' => 'datetime', + ]; + + /** + * 一个审批实例可以有多个审批记录 + */ + public function approvalRecords(): HasMany + { + return $this->hasMany(ApprovalRecords::class, 'instance_id'); + } + + /** + * 获取当前步骤的所有审批记录(pending 状态) + */ + public function currentStepRecords() + { + return $this->approvalRecords() + ->where('step_index', $this->current_step) + ->where('status', 'pending'); + } + + 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->hasOne(StockOrders::class, 'id', 'biz_id'); + } +} \ No newline at end of file diff --git a/app/crm/model/ApprovalRecords.php b/app/crm/model/ApprovalRecords.php new file mode 100644 index 0000000..4f4ea06 --- /dev/null +++ b/app/crm/model/ApprovalRecords.php @@ -0,0 +1,84 @@ + 'datetime', + 'created_at' => 'datetime', + 'updated_at' => 'datetime', + ]; + + /** + * 一条审批记录属于一个审批实例 + */ + public function approvalInstance(): BelongsTo + { + return $this->belongsTo(ApprovalInstances::class, 'instance_id'); + } + + /** + * 关联审批人 + */ + public function approver() + { + return $this->belongsTo(User::class, 'approver_id'); + } +} \ No newline at end of file diff --git a/app/crm/service/ApprovalFlowsService.php b/app/crm/service/ApprovalFlowsService.php new file mode 100644 index 0000000..e373c07 --- /dev/null +++ b/app/crm/service/ApprovalFlowsService.php @@ -0,0 +1,31 @@ +validator; + } +} \ No newline at end of file diff --git a/app/crm/service/ApprovalInstancesService.php b/app/crm/service/ApprovalInstancesService.php new file mode 100644 index 0000000..86e6e57 --- /dev/null +++ b/app/crm/service/ApprovalInstancesService.php @@ -0,0 +1,31 @@ +validator; + } +} \ No newline at end of file diff --git a/app/crm/service/ApprovalRecordsService.php b/app/crm/service/ApprovalRecordsService.php new file mode 100644 index 0000000..16c070d --- /dev/null +++ b/app/crm/service/ApprovalRecordsService.php @@ -0,0 +1,91 @@ +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'] = [ + 'approvalInstance', + 'approver', + 'approvalInstance.flows', + 'approvalInstance.initiator', + 'approvalInstance.bizInfo' + ]; + + $config['dataLimit']['type'] = 'relation'; + $config['dataLimit']['userId'] = $request->user->uid; + $config['dataLimit']['userField'] = 'approver_id'; + 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/crm/service/StockOrdersService.php b/app/crm/service/StockOrdersService.php new file mode 100644 index 0000000..7713c89 --- /dev/null +++ b/app/crm/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_order_flow', '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/crm/service/StockOrdersService_bak.php b/app/crm/service/StockOrdersService_bak.php new file mode 100644 index 0000000..356fcca --- /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_order_flow', '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/process/Emails.php b/app/process/Emails.php index c37d4e2..2679f00 100644 --- a/app/process/Emails.php +++ b/app/process/Emails.php @@ -29,26 +29,26 @@ class Emails // }); if (getenv('ENVIRONMENT') === 'master') { - // 拉取新邮件 - 每3小时的第0分钟0秒执行 - new Crontab('0 0 */3 * * *', function(){ + // 拉取新邮件 - 每小时的第20分钟0秒执行 + new Crontab('0 */20 * * * *', function(){ $email = new EmailsUtils(); $email->uids(); }); - // 邮件详情 - 每3小时的第10分钟0秒执行 - new Crontab('0 10 */3 * * *', function(){ + // 邮件详情 - 每小时的第25分钟0秒执行 + new Crontab('0 */25 * * * *', function(){ $email = new EmailsUtils(); $email->details(); }); - // 邮件处理 - 每3小时的第20分钟0秒执行 - new Crontab('0 20 */3 * * *', function(){ + // 邮件处理 - 每小时的第30分钟0秒执行 + new Crontab('0 */30 * * * *', function(){ $email = new EmailsUtils(); $email->process(); }); - // 邮件分派 - 每3小时的第30分钟0秒执行 - new Crontab('0 30 */3 * * *', function(){ + // 邮件分派 - 每小时的第35分钟0秒执行 + new Crontab('0 */35 * * * *', function(){ $email = new EmailsUtils(); $email->fenpai(); }); 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/composer.json b/composer.json index c474010..fcc2089 100644 --- a/composer.json +++ b/composer.json @@ -46,7 +46,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..2272db8 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": "68e74d2aef3a2f7f8163bfaf65431cb2", "packages": [ { "name": "aliyuncs/oss-sdk-php", @@ -2890,6 +2890,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", @@ -3975,6 +4208,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 cc117fd..7f97d12 100644 --- a/config/route.php +++ b/config/route.php @@ -165,6 +165,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']); }); // 询单 @@ -299,6 +300,29 @@ Route::group('/admin', function () { 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::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']); + }); }); @@ -368,7 +392,16 @@ Route::group('/crm', 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('/destroy', [app\crm\controller\StockOrdersController::class, 'destroy']); + 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::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']); }); }); \ 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