From 36d1d333b1f9ef6ca192f8ad7a033e4999b0ba7e Mon Sep 17 00:00:00 2001 From: "WIN-C27C5MSVTPO\\16961" <1696136552@qq.com> Date: Tue, 23 Sep 2025 15:13:56 +0800 Subject: [PATCH 001/110] =?UTF-8?q?=E4=BB=98=E6=AC=BE=E4=B8=8E=E6=94=B6?= =?UTF-8?q?=E6=AC=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/FinPaymentApplyController.php | 33 +++++ .../controller/FinReceiptController.php | 56 ++++++++ app/common/logic/FinPaymentApplyLogic.php | 17 +++ app/common/logic/FinReceiptLogic.php | 17 +++ app/common/model/FinPaymentApply.php | 65 +++++++++ app/common/model/FinPaymentStock.php | 67 +++++++++ app/common/model/FinReceipt.php | 65 +++++++++ app/common/model/StockOrders.php | 8 ++ app/common/service/FinPaymentApplyService.php | 107 +++++++++++++++ app/common/service/FinReceiptService.php | 127 ++++++++++++++++++ .../validate/FinPaymentApplyValidate.php | 29 ++++ app/common/validate/FinReceiptValidate.php | 56 ++++++++ app/controller/IndexController.php | 38 +++--- config/route.php | 18 +++ 14 files changed, 684 insertions(+), 19 deletions(-) create mode 100644 app/common/controller/FinPaymentApplyController.php create mode 100644 app/common/controller/FinReceiptController.php create mode 100644 app/common/logic/FinPaymentApplyLogic.php create mode 100644 app/common/logic/FinReceiptLogic.php create mode 100644 app/common/model/FinPaymentApply.php create mode 100644 app/common/model/FinPaymentStock.php create mode 100644 app/common/model/FinReceipt.php create mode 100644 app/common/service/FinPaymentApplyService.php create mode 100644 app/common/service/FinReceiptService.php create mode 100644 app/common/validate/FinPaymentApplyValidate.php create mode 100644 app/common/validate/FinReceiptValidate.php diff --git a/app/common/controller/FinPaymentApplyController.php b/app/common/controller/FinPaymentApplyController.php new file mode 100644 index 0000000..2d0f22e --- /dev/null +++ b/app/common/controller/FinPaymentApplyController.php @@ -0,0 +1,33 @@ +service = new FinPaymentApplyService(); + } + + /** + * 列表页数据格式化 + * + * @param array $data + * @return array + */ + // protected function indexFormat($data) + // { + // if (isset($data['created_at'])) + // $data['created_at'] = date('Y-m-d H:i:s', $data['created_at']); + // if (isset($data['updated_at'])) + // $data['updated_at'] = date('Y-m-d H:i:s', $data['updated_at']); + // return $data; + // } +} \ No newline at end of file diff --git a/app/common/controller/FinReceiptController.php b/app/common/controller/FinReceiptController.php new file mode 100644 index 0000000..4acf899 --- /dev/null +++ b/app/common/controller/FinReceiptController.php @@ -0,0 +1,56 @@ +service = new FinReceiptService(); + } + + /** + * 列表页数据格式化 + * + * @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['arrived_at'])) + $data['arrived_at'] = date('Y-m-d H:i:s', $data['arrived_at']); + if (isset($data['received_at'])) + $data['received_at'] = date('Y-m-d H:i:s', $data['received_at']); + return $data; + } + + /** + * 数据格式化 + * + * @param array $data + * @return array + */ + protected function showFormat($data) + { + if (isset($data['created_at'])) + $data['created_at'] = date('Y-m-d H:i:s', $data['created_at']); + if (isset($data['updated_at'])) + $data['updated_at'] = date('Y-m-d H:i:s', $data['updated_at']); + if (isset($data['arrived_at'])) + $data['arrived_at'] = date('Y-m-d H:i:s', $data['arrived_at']); + if (isset($data['received_at'])) + $data['received_at'] = date('Y-m-d H:i:s', $data['received_at']); + return $data; + } +} \ No newline at end of file diff --git a/app/common/logic/FinPaymentApplyLogic.php b/app/common/logic/FinPaymentApplyLogic.php new file mode 100644 index 0000000..09b93e1 --- /dev/null +++ b/app/common/logic/FinPaymentApplyLogic.php @@ -0,0 +1,17 @@ +hasOne(FinReceipt::class, 'payment_apply_id', 'id'); + } + + /** + * 备货单 + */ + public function stock() + { + return $this->belongsToMany(StockOrders::class, 'fin_payment_stock', 'payment_apply_id', 'stock_id'); + } +} \ No newline at end of file diff --git a/app/common/model/FinPaymentStock.php b/app/common/model/FinPaymentStock.php new file mode 100644 index 0000000..6f4a3ba --- /dev/null +++ b/app/common/model/FinPaymentStock.php @@ -0,0 +1,67 @@ +belongsTo(FinPaymentApply::class, 'payment_apply_id', 'id'); + } + + // 收款记录 + public function receipt() + { + return $this->belongsTo(FinReceipt::class, 'receipt_id', 'id'); + } + + // 付款申请 + public function stock() + { + return $this->belongsTo(StockOrders::class, 'stock_id', 'id'); + } +} \ No newline at end of file diff --git a/app/common/model/FinReceipt.php b/app/common/model/FinReceipt.php new file mode 100644 index 0000000..a98914f --- /dev/null +++ b/app/common/model/FinReceipt.php @@ -0,0 +1,65 @@ +hasOne(FinPaymentApply::class, 'id', 'payment_apply_id'); + } + + /** + * 备货单 + */ + public function stock() + { + return $this->belongsToMany(StockOrders::class, 'fin_payment_stock', 'receipt_id', 'stock_id'); + } +} \ No newline at end of file diff --git a/app/common/model/StockOrders.php b/app/common/model/StockOrders.php index 0b97fe4..0802afe 100644 --- a/app/common/model/StockOrders.php +++ b/app/common/model/StockOrders.php @@ -95,4 +95,12 @@ class StockOrders extends Model { return $this->hasOne(User::class, 'id', 'salesperson_id'); } + + /** + * 客户的标签 + */ + public function payment() + { + return $this->belongsToMany(FinPaymentApply::class, 'fin_payment_stock', 'stock_id', 'payment_apply_id'); + } } \ No newline at end of file diff --git a/app/common/service/FinPaymentApplyService.php b/app/common/service/FinPaymentApplyService.php new file mode 100644 index 0000000..00ad976 --- /dev/null +++ b/app/common/service/FinPaymentApplyService.php @@ -0,0 +1,107 @@ +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 'update': + case 'destroy': + $config['where']['id'] = $p['id']; + break; + case 'store': + break; + } + + return $config; + } + + public function beforeServiceStore($data, Request $request) + { + $data['apply_no'] = $this->generateUniqueApplyNo(); + $data['submit_files'] = json_encode($data['submit_files']); + return $data; + } + + public function beforeServiceUpdate($data, Request $request) + { + $data['submit_files'] = json_encode($data['submit_files']); + return $data; + } + + + /** + * 生成唯一的付款申请编码 + * 格式:PA + 日期 + 6位字母数字组合 + * + * @return string 唯一编码 + */ + public function generateUniqueApplyNo() + { + $chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ'; + $maxAttempts = 10; // 防止无限循环 + $secondChar = 'PA' . date('Ymd'); + + for ($attempt = 0; $attempt < $maxAttempts; $attempt++) { + // 生成后6位随机字符 + $suffix = ''; + for ($i = 0; $i < 6; $i++) { + $suffix .= $chars[rand(0, strlen($chars) - 1)]; + } + + $applyNo = $secondChar . $suffix; + + // 检查是否已存在 + $existing = FinPaymentApply::where('apply_no', $applyNo)->first(); + if (!$existing) { + return $applyNo; + } + } + + // 如果1000次尝试后仍然重复,使用时间戳确保唯一性 + $timestamp = substr(time(), -6); + return $secondChar . $timestamp; + } +} \ No newline at end of file diff --git a/app/common/service/FinReceiptService.php b/app/common/service/FinReceiptService.php new file mode 100644 index 0000000..2cc781c --- /dev/null +++ b/app/common/service/FinReceiptService.php @@ -0,0 +1,127 @@ +validator; + } + + + /** + * 构建查询条件 + */ + protected function buildQueryConfig(Request $request, string $scene = 'index'): array + { + $p = $request->all(); + + $config = []; + $config['model'] = $this->modelClass; + switch ($scene) { + case 'index': + // ====== ====== 排序 ====== ====== + $config['order']['id'] = 'desc'; + + // ====== ====== 筛选 ====== ====== + // 汇款国家 + if (isset($p['country_id']) && !empty($p['country_id'])) { + $config['where']['country_id'] = $p['country_id']; + } + // 收款账户筛选 + if (isset($p['account_id']) && !empty($p['account_id'])) { + $config['where']['account_id'] = $p['account_id']; + } + // 关键字 + if (isset($p['keyword']) && $p['keyword']) { + $k = '%' . $p['keyword'] . '%'; + $config['orWhere'][] = ['remitter', 'like', $k]; + } + break; + case 'show': + $config['where']['id'] = $p['id']; + break; + case 'update': + case 'destroy': + $config['where']['id'] = $p['id']; + break; + case 'store': + break; + } + + return $config; + } + + public function beforeServiceStore($data, Request $request) + { + if(isset($data['payment_apply_id']) && empty($data['payment_apply_id'])){ + unset($data['payment_apply_id']); + } + $data['receipt_no'] = $this->generateUniqueReceipt(); + $data['arrived_at'] = strtotime( $data['arrived_at']); + return $data; + } + + public function beforeServiceUpdate($data, Request $request) + { + $data['arrived_at'] = strtotime( $data['arrived_at']); + $data['received_at'] = strtotime( $data['received_at']); + return $data; + } + + + /** + * 生成唯一的付款申请编码 + * 格式:PA + 日期 + 6位字母数字组合 + * + * @return string 唯一编码 + */ + public function generateUniqueReceipt() + { + $chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ'; + $maxAttempts = 10; // 防止无限循环 + $secondChar = 'RE' . date('Ymd'); + + for ($attempt = 0; $attempt < $maxAttempts; $attempt++) { + // 生成后6位随机字符 + $suffix = ''; + for ($i = 0; $i < 6; $i++) { + $suffix .= $chars[rand(0, strlen($chars) - 1)]; + } + + $ReceiptNo = $secondChar . $suffix; + + // 检查是否已存在 + $existing = FinReceipt::where('receipt_no', $ReceiptNo)->first(); + if (!$existing) { + return $ReceiptNo; + } + } + + // 如果1000次尝试后仍然重复,使用时间戳确保唯一性 + $timestamp = substr(time(), -6); + return $secondChar . $timestamp; + } +} \ No newline at end of file diff --git a/app/common/validate/FinPaymentApplyValidate.php b/app/common/validate/FinPaymentApplyValidate.php new file mode 100644 index 0000000..58145e3 --- /dev/null +++ b/app/common/validate/FinPaymentApplyValidate.php @@ -0,0 +1,29 @@ + [ + + ], + + 'update' => [ + + ] + ]; +} \ No newline at end of file diff --git a/app/common/validate/FinReceiptValidate.php b/app/common/validate/FinReceiptValidate.php new file mode 100644 index 0000000..c006dee --- /dev/null +++ b/app/common/validate/FinReceiptValidate.php @@ -0,0 +1,56 @@ + 'integer|gt:0', + 'country_id' => 'require|integer|gt:0', + 'remitter' => 'require', + 'account_id' => 'require|integer|gt:0', + 'currency_id' => 'require|integer|gt:0', + 'amount' => 'require|gt:0', + 'exchange_rate' => 'require|gt:0', + 'arrived_at' => 'require', + 'is_domestic_return' => 'require|integer', + ]; + + // 提示 + protected $message = [ + 'payment_apply_id.integer' => '付款申请ID必须是整数', + 'payment_apply_id.gt' => '付款申请ID必须大于0', + 'country_id.require' => '付款来源国家ID不能为空', + 'country_id.integer' => '付款来源国家ID必须是整数', + 'country_id.gt' => '付款来源国家ID必须大于0', + 'remitter.require' => '汇款人不能为空', + 'account_id.require' => '收款账号ID不能为空', + 'account_id.integer' => '收款账号ID必须是整数', + 'account_id.gt' => '收款账号ID必须大于0', + 'currency_id.require' => '收款币种ID不能为空', + 'currency_id.integer' => '收款币种ID必须是整数', + 'currency_id.gt' => '收款币种ID必须大于0', + 'amount.require' => '收款金额不能为空', + 'amount.gt' => '收款金额必须大于0', + 'exchange_rate.require' => '实际汇率不能为空', + 'exchange_rate.gt' => '实际汇率必须大于0', + 'arrived_at.require' => '到账时间不能为空', + 'arrived_at.gt' => '到账时间必须大于0', + 'is_domestic_return.require' => '是否回转内地不能为空', + 'is_domestic_return.integer' => '是否回转内地必须是整数', + ]; + + // 场景 + protected $scene = [ + 'store' => [ + 'payment_apply_id', 'country_id', 'remitter', 'account_id', 'currency_id', 'amount', 'exchange_rate', 'arrived_at' + ], + + 'update' => [ + + ] + ]; +} \ No newline at end of file diff --git a/app/controller/IndexController.php b/app/controller/IndexController.php index b25b5a4..cf30328 100644 --- a/app/controller/IndexController.php +++ b/app/controller/IndexController.php @@ -70,27 +70,27 @@ class IndexController public function createResource() { $options = [ - 'controller' => [ - 'module' => 'crm', - 'name' => 'approval_flows' - ], +// 'controller' => [ +// 'module' => 'common', +// 'name' => 'fin_payment_apply' +// ], 'model' => [ - 'module' => 'crm', - 'name' => 'approval_flows' + 'module' => 'common', + 'name' => 'fin_payment_stock' ], - 'service' => [ - 'module' => 'crm', - 'name' => 'approval_flows' - ], - 'logic' => [ - 'module' => 'crm', - 'name' => 'approval_flows' - ], - 'validate' => [ - 'module' => 'crm', - 'name' => 'approval_flows' - ], - 'table' => 'approval_flows' +// 'service' => [ +// 'module' => 'common', +// 'name' => 'fin_payment_apply' +// ], +// 'logic' => [ +// 'module' => 'common', +// 'name' => 'fin_payment_apply' +// ], +// 'validate' => [ +// 'module' => 'common', +// 'name' => 'fin_payment_apply' +// ], + 'table' => 'fin_payment_stock' ]; $result = (new Generate($options))->doGenerate(); diff --git a/config/route.php b/config/route.php index 887c683..087fcf8 100644 --- a/config/route.php +++ b/config/route.php @@ -351,6 +351,24 @@ Route::group('/admin', function () { Route::any('/user/achieved', [app\admin\targets\controller\StatisticsController::class, 'userAchieved']); }); }); + + // 收款记录 + Route::group('/receipt', function () { + Route::any('/list', [app\common\controller\FinReceiptController::class, 'index']); + Route::any('/show', [app\common\controller\FinReceiptController::class, 'show']); + Route::any('/store', [app\common\controller\FinReceiptController::class, 'store']); + Route::any('/update', [app\common\controller\FinReceiptController::class, 'update']); + Route::any('/destroy', [app\common\controller\FinReceiptController::class, 'destroy']); + }); + + // 收款记录 + Route::group('/payment', function () { + Route::any('/list', [app\common\controller\FinPaymentApplyController::class, 'index']); + Route::any('/show', [app\common\controller\FinPaymentApplyController::class, 'show']); + Route::any('/store', [app\common\controller\FinPaymentApplyController::class, 'store']); + Route::any('/update', [app\common\controller\FinPaymentApplyController::class, 'update']); + Route::any('/destroy', [app\common\controller\FinPaymentApplyController::class, 'destroy']); + }); }); From 4717bb01ca52dfb3c8ebebf4c617b34155dc71ef Mon Sep 17 00:00:00 2001 From: "WIN-C27C5MSVTPO\\16961" <1696136552@qq.com> Date: Tue, 23 Sep 2025 15:14:08 +0800 Subject: [PATCH 002/110] =?UTF-8?q?=E4=BB=98=E6=AC=BE=E4=B8=8E=E6=94=B6?= =?UTF-8?q?=E6=AC=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/common/model/FinPaymentApply.php | 8 +++ app/common/model/FinPaymentStock.php | 4 +- app/common/model/FinReceipt.php | 14 ++++-- app/common/model/FinReceiptHis.php | 49 +++++++++++++++++++ app/common/model/StockOrders.php | 8 +++ app/common/service/FinPaymentApplyService.php | 19 +++++++ app/common/service/FinReceiptService.php | 4 +- app/controller/IndexController.php | 4 +- 8 files changed, 102 insertions(+), 8 deletions(-) create mode 100644 app/common/model/FinReceiptHis.php diff --git a/app/common/model/FinPaymentApply.php b/app/common/model/FinPaymentApply.php index 2fb04a7..0556210 100644 --- a/app/common/model/FinPaymentApply.php +++ b/app/common/model/FinPaymentApply.php @@ -62,4 +62,12 @@ class FinPaymentApply extends Model { return $this->belongsToMany(StockOrders::class, 'fin_payment_stock', 'payment_apply_id', 'stock_id'); } + + /** + * 关联的备货单(通过中间表) + */ + public function stocks() + { + return $this->belongsToMany(StockOrders::class, 'fin_payment_stock', 'payment_apply_id', 'stock_id'); + } } \ No newline at end of file diff --git a/app/common/model/FinPaymentStock.php b/app/common/model/FinPaymentStock.php index 6f4a3ba..d5b3d10 100644 --- a/app/common/model/FinPaymentStock.php +++ b/app/common/model/FinPaymentStock.php @@ -40,7 +40,7 @@ class FinPaymentStock extends Model /** * 允许自动填充模型字段 */ - protected $fillable = ['id', 'payment_apply_id', 'receipt_id', 'stock_id', 'allocated_amount', 'allocated_amount_cny']; + protected $fillable = ['id', 'pi_no', 'payment_apply_id', 'receipt_id', 'stock_id', 'allocated_amount', 'allocated_amount_cny', 'updated_at', 'created_at']; /** * 不允许自动填充模型字段 @@ -59,7 +59,7 @@ class FinPaymentStock extends Model return $this->belongsTo(FinReceipt::class, 'receipt_id', 'id'); } - // 付款申请 + // 备货单 public function stock() { return $this->belongsTo(StockOrders::class, 'stock_id', 'id'); diff --git a/app/common/model/FinReceipt.php b/app/common/model/FinReceipt.php index a98914f..c87ab12 100644 --- a/app/common/model/FinReceipt.php +++ b/app/common/model/FinReceipt.php @@ -40,7 +40,7 @@ class FinReceipt extends Model /** * 允许自动填充模型字段 */ - protected $fillable = ['id', 'receipt_no', 'payment_apply_id', 'country_id', 'remitter', 'account_id', 'currency_id', 'amount', 'discount_amount', 'total_amount_cny', 'exchange_rate', 'arrived_at', 'received_at', 'voucher_no', 'fx_receipt_file', 'is_domestic_return', 'created_at', 'updated_at']; + protected $fillable = ['id', 'receipt_no', 'payment_apply_id', 'country_id', 'remitter', 'account_id', 'currency_id', 'amount', 'discount_amount', 'total_amount_cny', 'exchange_rate', 'arrived_at', 'received_at', 'voucher_no', 'fx_receipt_file', 'is_domestic_return', 'created_by', 'updated_by', 'created_at', 'updated_at']; /** * 不允许自动填充模型字段 @@ -48,11 +48,11 @@ class FinReceipt extends Model protected $guarded = []; /** - * 获取付款申请 + * 对应的付款申请(1-1,可为空) */ public function payment() { - return $this->hasOne(FinPaymentApply::class, 'id', 'payment_apply_id'); + return $this->belongsTo(FinPaymentApply::class, 'payment_apply_id', 'id'); } /** @@ -62,4 +62,12 @@ class FinReceipt extends Model { return $this->belongsToMany(StockOrders::class, 'fin_payment_stock', 'receipt_id', 'stock_id'); } + + /** + * 关联的备货单(通过中间表) + */ + public function stocks() + { + return $this->belongsToMany(StockOrders::class, 'fin_payment_stock', 'receipt_id', 'stock_id'); + } } \ No newline at end of file diff --git a/app/common/model/FinReceiptHis.php b/app/common/model/FinReceiptHis.php new file mode 100644 index 0000000..814b87f --- /dev/null +++ b/app/common/model/FinReceiptHis.php @@ -0,0 +1,49 @@ +belongsToMany(FinPaymentApply::class, 'fin_payment_stock', 'stock_id', 'payment_apply_id'); } + + /** + * 关联收款记录(多对多,经由中间表) + */ + public function receipts() + { + return $this->belongsToMany(FinReceipt::class, 'fin_payment_stock', 'stock_id', 'receipt_id'); + } } \ No newline at end of file diff --git a/app/common/service/FinPaymentApplyService.php b/app/common/service/FinPaymentApplyService.php index 00ad976..d7912e1 100644 --- a/app/common/service/FinPaymentApplyService.php +++ b/app/common/service/FinPaymentApplyService.php @@ -2,9 +2,11 @@ namespace app\common\service; +use app\common\model\FinPaymentStock; use app\common\validate\FinPaymentApplyValidate; use app\common\traits\crud\CrudServiceTrait; use app\common\model\FinPaymentApply; +use support\Redis; use support\Request; class FinPaymentApplyService @@ -61,10 +63,27 @@ class FinPaymentApplyService public function beforeServiceStore($data, Request $request) { $data['apply_no'] = $this->generateUniqueApplyNo(); + $data['doc_user_id'] = $request->user->uid; $data['submit_files'] = json_encode($data['submit_files']); + redis::set('stock'.$data['doc_user_id'],json_encode($request->post('stock'))); return $data; } + + public function afterServiceStore($res) + { + $data = [ + 'payment_apply_id' => $res['id'], + ]; + $stock = json_decode(redis::get('stock'.$res['doc_user_id']), true); + redis::del('stock'); + foreach ($stock as $k => $v) { + $data['stock_id'] = $v['stock_id']; + $data['pi_no'] = $v['pi_on']; + FinPaymentStock::create($data); + } + } + public function beforeServiceUpdate($data, Request $request) { $data['submit_files'] = json_encode($data['submit_files']); diff --git a/app/common/service/FinReceiptService.php b/app/common/service/FinReceiptService.php index 2cc781c..3a7832d 100644 --- a/app/common/service/FinReceiptService.php +++ b/app/common/service/FinReceiptService.php @@ -80,12 +80,14 @@ class FinReceiptService unset($data['payment_apply_id']); } $data['receipt_no'] = $this->generateUniqueReceipt(); - $data['arrived_at'] = strtotime( $data['arrived_at']); + $data['arrived_at'] = $request->user->uid; + $data['updated_by'] = $request->user->uid; return $data; } public function beforeServiceUpdate($data, Request $request) { + $data['updated_by'] = $request->user->uid; $data['arrived_at'] = strtotime( $data['arrived_at']); $data['received_at'] = strtotime( $data['received_at']); return $data; diff --git a/app/controller/IndexController.php b/app/controller/IndexController.php index cf30328..e92751b 100644 --- a/app/controller/IndexController.php +++ b/app/controller/IndexController.php @@ -76,7 +76,7 @@ class IndexController // ], 'model' => [ 'module' => 'common', - 'name' => 'fin_payment_stock' + 'name' => 'fin_receipt_his' ], // 'service' => [ // 'module' => 'common', @@ -90,7 +90,7 @@ class IndexController // 'module' => 'common', // 'name' => 'fin_payment_apply' // ], - 'table' => 'fin_payment_stock' + 'table' => 'fin_receipt_his' ]; $result = (new Generate($options))->doGenerate(); From e6d3cc3f047e365c0f76fc0ba0e6eb60c6ae8892 Mon Sep 17 00:00:00 2001 From: "WIN-C27C5MSVTPO\\16961" <1696136552@qq.com> Date: Tue, 23 Sep 2025 16:45:06 +0800 Subject: [PATCH 003/110] =?UTF-8?q?=E4=BB=98=E6=AC=BE=E7=94=B3=E8=AF=B7?= =?UTF-8?q?=E6=9F=A5=E8=AF=A2=E7=9B=B8=E5=85=B3=E5=AD=97=E6=AE=B5=E5=90=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/FinPaymentApplyController.php | 65 ++++++++++++++++--- app/common/service/FinPaymentApplyService.php | 6 +- app/common/service/FinReceiptService.php | 28 ++++++++ config/route.php | 2 +- 4 files changed, 89 insertions(+), 12 deletions(-) diff --git a/app/common/controller/FinPaymentApplyController.php b/app/common/controller/FinPaymentApplyController.php index 2d0f22e..76e51bf 100644 --- a/app/common/controller/FinPaymentApplyController.php +++ b/app/common/controller/FinPaymentApplyController.php @@ -1,7 +1,13 @@ first()->name; + if (isset($data['sales_user_id'])) //业务员 + $data['sales_user_name'] = User::where('id', $data['doc_user_id'])->first()->name; + if (isset($data['corp_id'])) //公司(渠道) + $data['corp_name'] = SysDictItem::where('id', $data['corp_id'])->first()->item_label; + if (isset($data['account_id'])) //收款账号 + $data['account_name'] = SysAccount::where('id', $data['account_id'])->first()->account_name; + if (isset($data['country_id'])) //付款来源国家 + $data['country_name'] = Country::where('id', $data['currency_id'])->first()->cn_addr; + if (isset($data['currency_id'])) //付款币种 + $data['currency_name'] = SysCurrency::where('id', $data['currency_id'])->first()->currency_name; + if (isset($data['submit_files'])) + $data['submit_files'] = json_decode($data['submit_files'], true); + if (isset($data['created_at'])) + $data['created_at'] = date('Y-m-d H:i:s', $data['created_at']); + if (isset($data['updated_at'])) + $data['updated_at'] = date('Y-m-d H:i:s', $data['updated_at']); + return $data; + } + + /** + * 数据格式化 + * + * @param array $data + * @return array + */ + protected function showFormat($data) + { + if (isset($data['doc_user_id'])) //内勤 + $data['doc_user_name'] = User::where('id', $data['doc_user_id'])->first()->name; + if (isset($data['sales_user_id'])) //业务员 + $data['sales_user_name'] = User::where('id', $data['doc_user_id'])->first()->name; + if (isset($data['corp_id'])) //公司(渠道) + $data['corp_name'] = SysDictItem::where('id', $data['corp_id'])->first()->item_label; + if (isset($data['account_id'])) //收款账号 + $data['account_name'] = SysAccount::where('id', $data['account_id'])->first()->account_name; + if (isset($data['country_id'])) //付款来源国家 + $data['country_name'] = Country::where('id', $data['currency_id'])->first()->cn_addr; + if (isset($data['currency_id'])) //付款币种 + $data['currency_name'] = SysCurrency::where('id', $data['currency_id'])->first()->currency_name; + if (isset($data['submit_files'])) + $data['submit_files'] = json_decode($data['submit_files'], true); + if (isset($data['created_at'])) + $data['created_at'] = date('Y-m-d H:i:s', $data['created_at']); + if (isset($data['updated_at'])) + $data['updated_at'] = date('Y-m-d H:i:s', $data['updated_at']); + return $data; + } } \ No newline at end of file diff --git a/app/common/service/FinPaymentApplyService.php b/app/common/service/FinPaymentApplyService.php index d7912e1..535d621 100644 --- a/app/common/service/FinPaymentApplyService.php +++ b/app/common/service/FinPaymentApplyService.php @@ -65,7 +65,7 @@ class FinPaymentApplyService $data['apply_no'] = $this->generateUniqueApplyNo(); $data['doc_user_id'] = $request->user->uid; $data['submit_files'] = json_encode($data['submit_files']); - redis::set('stock'.$data['doc_user_id'],json_encode($request->post('stock'))); + redis::set('stock_' . $data['doc_user_id'], json_encode($request->post('stock')), 'EX', 30); return $data; } @@ -75,8 +75,8 @@ class FinPaymentApplyService $data = [ 'payment_apply_id' => $res['id'], ]; - $stock = json_decode(redis::get('stock'.$res['doc_user_id']), true); - redis::del('stock'); + $stock = json_decode(redis::get('stock_' . $res['doc_user_id']), true); + redis::del('stock' . $res['doc_user_id']); foreach ($stock as $k => $v) { $data['stock_id'] = $v['stock_id']; $data['pi_no'] = $v['pi_on']; diff --git a/app/common/service/FinReceiptService.php b/app/common/service/FinReceiptService.php index 3a7832d..53d61ff 100644 --- a/app/common/service/FinReceiptService.php +++ b/app/common/service/FinReceiptService.php @@ -2,6 +2,7 @@ namespace app\common\service; +use app\common\model\FinReceiptHis; use app\common\validate\FinReceiptValidate; use app\common\traits\crud\CrudServiceTrait; use app\common\model\FinReceipt; @@ -50,10 +51,22 @@ class FinReceiptService if (isset($p['country_id']) && !empty($p['country_id'])) { $config['where']['country_id'] = $p['country_id']; } + + // 没有付款记录的收款记录 + if (isset($p['payment_apply']) && $p['payment_apply'] == 'not') { + $config['where']['payment_apply_id'] = ['not null']; + } + + // 收款账户筛选 + if (isset($p['payment_apply_id']) && !empty($p['payment_apply_id'])) { + $config['where']['payment_apply_id'] = $p['payment_apply_id']; + } + // 收款账户筛选 if (isset($p['account_id']) && !empty($p['account_id'])) { $config['where']['account_id'] = $p['account_id']; } + // 关键字 if (isset($p['keyword']) && $p['keyword']) { $k = '%' . $p['keyword'] . '%'; @@ -85,6 +98,14 @@ class FinReceiptService return $data; } + public function afterServiceStore($res) + { + $receipt = FinReceipt::where('id',$res->id)->first()->toArray(); + $receipt = array_merge($receipt,['receipt_id' => $res->id]); + unset($receipt['id']); + FinReceiptHis::create($receipt); + } + public function beforeServiceUpdate($data, Request $request) { $data['updated_by'] = $request->user->uid; @@ -92,6 +113,13 @@ class FinReceiptService $data['received_at'] = strtotime( $data['received_at']); return $data; } + public function afterServiceUpdate($res) + { + $receipt = FinReceipt::where('id',$res->id)->first()->toArray(); + $receipt = array_merge($receipt,['receipt_id' => $res->id]); + unset($receipt['id']); + FinReceiptHis::create($receipt); + } /** diff --git a/config/route.php b/config/route.php index 087fcf8..f2c9cb7 100644 --- a/config/route.php +++ b/config/route.php @@ -361,7 +361,7 @@ Route::group('/admin', function () { Route::any('/destroy', [app\common\controller\FinReceiptController::class, 'destroy']); }); - // 收款记录 + // 付款申请 Route::group('/payment', function () { Route::any('/list', [app\common\controller\FinPaymentApplyController::class, 'index']); Route::any('/show', [app\common\controller\FinPaymentApplyController::class, 'show']); From 1d6caad7b08f15c56d69db3482f888675338e723 Mon Sep 17 00:00:00 2001 From: "WIN-C27C5MSVTPO\\16961" <1696136552@qq.com> Date: Tue, 23 Sep 2025 17:33:09 +0800 Subject: [PATCH 004/110] =?UTF-8?q?=E4=BB=98=E6=AC=BE=E7=94=B3=E8=AF=B7?= =?UTF-8?q?=E5=8F=82=E6=95=B0=E7=BB=93=E6=9E=84=E4=BF=AE=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/common/controller/FinReceiptController.php | 15 +++++++++++++++ app/common/service/FinPaymentApplyService.php | 14 ++++++++------ 2 files changed, 23 insertions(+), 6 deletions(-) diff --git a/app/common/controller/FinReceiptController.php b/app/common/controller/FinReceiptController.php index 4acf899..c80dc3a 100644 --- a/app/common/controller/FinReceiptController.php +++ b/app/common/controller/FinReceiptController.php @@ -2,6 +2,9 @@ namespace app\common\controller; use app\common\controller\BaseController; +use app\common\model\Country; +use app\common\model\SysAccount; +use app\common\model\SysCurrency; use app\common\traits\crud\CrudControllerTrait; use app\common\service\FinReceiptService; @@ -32,6 +35,12 @@ class FinReceiptController extends BaseController $data['arrived_at'] = date('Y-m-d H:i:s', $data['arrived_at']); if (isset($data['received_at'])) $data['received_at'] = date('Y-m-d H:i:s', $data['received_at']); + if (isset($data['account_id'])) //收款账号 + $data['account_name'] = SysAccount::where('id', $data['account_id'])->first()->account_name; + if (isset($data['country_id'])) //付款来源国家 + $data['country_name'] = Country::where('id', $data['currency_id'])->first()->cn_addr; + if (isset($data['currency_id'])) //付款币种 + $data['currency_name'] = SysCurrency::where('id', $data['currency_id'])->first()->currency_name; return $data; } @@ -51,6 +60,12 @@ class FinReceiptController extends BaseController $data['arrived_at'] = date('Y-m-d H:i:s', $data['arrived_at']); if (isset($data['received_at'])) $data['received_at'] = date('Y-m-d H:i:s', $data['received_at']); + if (isset($data['account_id'])) //收款账号 + $data['account_name'] = SysAccount::where('id', $data['account_id'])->first()->account_name; + if (isset($data['country_id'])) //付款来源国家 + $data['country_name'] = Country::where('id', $data['currency_id'])->first()->cn_addr; + if (isset($data['currency_id'])) //付款币种 + $data['currency_name'] = SysCurrency::where('id', $data['currency_id'])->first()->currency_name; return $data; } } \ No newline at end of file diff --git a/app/common/service/FinPaymentApplyService.php b/app/common/service/FinPaymentApplyService.php index 535d621..14b9d64 100644 --- a/app/common/service/FinPaymentApplyService.php +++ b/app/common/service/FinPaymentApplyService.php @@ -66,6 +66,7 @@ class FinPaymentApplyService $data['doc_user_id'] = $request->user->uid; $data['submit_files'] = json_encode($data['submit_files']); redis::set('stock_' . $data['doc_user_id'], json_encode($request->post('stock')), 'EX', 30); + unset($data['stock']); return $data; } @@ -75,12 +76,14 @@ class FinPaymentApplyService $data = [ 'payment_apply_id' => $res['id'], ]; - $stock = json_decode(redis::get('stock_' . $res['doc_user_id']), true); + $piStock = json_decode(redis::get('stock_' . $res['doc_user_id']), true); redis::del('stock' . $res['doc_user_id']); - foreach ($stock as $k => $v) { - $data['stock_id'] = $v['stock_id']; - $data['pi_no'] = $v['pi_on']; - FinPaymentStock::create($data); + foreach ($piStock as $stock) { + foreach ($stock['stock_id'] as $v) { + $data['stock_id'] = $v; + $data['pi_no'] = $stock['pi_on']; + FinPaymentStock::create($data); + } } } @@ -90,7 +93,6 @@ class FinPaymentApplyService return $data; } - /** * 生成唯一的付款申请编码 * 格式:PA + 日期 + 6位字母数字组合 From 870bca5e578a5d697491cd8b3f425c71962b6099 Mon Sep 17 00:00:00 2001 From: "WIN-C27C5MSVTPO\\16961" <1696136552@qq.com> Date: Tue, 23 Sep 2025 18:26:17 +0800 Subject: [PATCH 005/110] =?UTF-8?q?=E4=BB=98=E6=AC=BE=E8=AF=A6=E6=83=85-?= =?UTF-8?q?=E5=A4=87=E8=B4=A7=E5=8D=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/common/controller/FinPaymentApplyController.php | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/app/common/controller/FinPaymentApplyController.php b/app/common/controller/FinPaymentApplyController.php index 76e51bf..3d82e92 100644 --- a/app/common/controller/FinPaymentApplyController.php +++ b/app/common/controller/FinPaymentApplyController.php @@ -4,6 +4,7 @@ namespace app\common\controller; use app\common\controller\BaseController; use app\common\model\Country; +use app\common\model\FinPaymentStock; use app\common\model\SysAccount; use app\common\model\SysCurrency; use app\common\model\SysDictItem; @@ -39,7 +40,7 @@ class FinPaymentApplyController extends BaseController if (isset($data['account_id'])) //收款账号 $data['account_name'] = SysAccount::where('id', $data['account_id'])->first()->account_name; if (isset($data['country_id'])) //付款来源国家 - $data['country_name'] = Country::where('id', $data['currency_id'])->first()->cn_addr; + $data['country_name'] = Country::where('id', $data['country_id'])->first()->cn_addr; if (isset($data['currency_id'])) //付款币种 $data['currency_name'] = SysCurrency::where('id', $data['currency_id'])->first()->currency_name; if (isset($data['submit_files'])) @@ -68,7 +69,7 @@ class FinPaymentApplyController extends BaseController if (isset($data['account_id'])) //收款账号 $data['account_name'] = SysAccount::where('id', $data['account_id'])->first()->account_name; if (isset($data['country_id'])) //付款来源国家 - $data['country_name'] = Country::where('id', $data['currency_id'])->first()->cn_addr; + $data['country_name'] = Country::where('id', $data['country_id'])->first()->cn_addr; if (isset($data['currency_id'])) //付款币种 $data['currency_name'] = SysCurrency::where('id', $data['currency_id'])->first()->currency_name; if (isset($data['submit_files'])) @@ -77,6 +78,14 @@ class FinPaymentApplyController extends BaseController $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']); + + $finPaymentStock = FinPaymentStock::where('payment_apply_id', $data['id'])->get(); + $stock = []; + foreach ($finPaymentStock as $v) { + $stock[$v['pi_no']] = ['pi_no' => $v['pi_no']]; + $stock[$v['pi_no']]['stock_id'] = [$v['stock_id']]; + } + $data['stock'] = $stock; return $data; } } \ No newline at end of file From db10ece6b702b15188a2b74800018be5439a54a8 Mon Sep 17 00:00:00 2001 From: "WIN-C27C5MSVTPO\\16961" <1696136552@qq.com> Date: Tue, 23 Sep 2025 18:30:13 +0800 Subject: [PATCH 006/110] =?UTF-8?q?=E4=BB=98=E6=AC=BE=E8=AF=A6=E6=83=85-?= =?UTF-8?q?=E5=A4=87=E8=B4=A7=E5=8D=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/common/controller/FinPaymentApplyController.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/common/controller/FinPaymentApplyController.php b/app/common/controller/FinPaymentApplyController.php index 3d82e92..abf31a8 100644 --- a/app/common/controller/FinPaymentApplyController.php +++ b/app/common/controller/FinPaymentApplyController.php @@ -85,7 +85,7 @@ class FinPaymentApplyController extends BaseController $stock[$v['pi_no']] = ['pi_no' => $v['pi_no']]; $stock[$v['pi_no']]['stock_id'] = [$v['stock_id']]; } - $data['stock'] = $stock; + $data['stock'] = array_values($stock); return $data; } } \ No newline at end of file From 54f83679ea79c08b3fd3366bc2f8a6b9f3537b37 Mon Sep 17 00:00:00 2001 From: "WIN-C27C5MSVTPO\\16961" <1696136552@qq.com> Date: Tue, 23 Sep 2025 18:38:02 +0800 Subject: [PATCH 007/110] =?UTF-8?q?=E6=94=B6=E6=AC=BE=E8=AE=B0=E5=BD=95?= =?UTF-8?q?=E7=BC=96=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/common/service/FinReceiptService.php | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/app/common/service/FinReceiptService.php b/app/common/service/FinReceiptService.php index 53d61ff..e50bc7d 100644 --- a/app/common/service/FinReceiptService.php +++ b/app/common/service/FinReceiptService.php @@ -93,7 +93,8 @@ class FinReceiptService unset($data['payment_apply_id']); } $data['receipt_no'] = $this->generateUniqueReceipt(); - $data['arrived_at'] = $request->user->uid; + $data['arrived_at'] = strtotime( $data['arrived_at']); + $data['created_by'] = $request->user->uid; $data['updated_by'] = $request->user->uid; return $data; } @@ -109,8 +110,10 @@ class FinReceiptService public function beforeServiceUpdate($data, Request $request) { $data['updated_by'] = $request->user->uid; - $data['arrived_at'] = strtotime( $data['arrived_at']); - $data['received_at'] = strtotime( $data['received_at']); + $data['arrived_at'] = strtotime($data['arrived_at']); + if(isset($data['received_at'])) + $data['received_at'] = strtotime($data['received_at']); + return $data; } public function afterServiceUpdate($res) From abc1142e88e1707d4e1aeaf0070ff691fb8c3897 Mon Sep 17 00:00:00 2001 From: "WIN-C27C5MSVTPO\\16961" <1696136552@qq.com> Date: Wed, 24 Sep 2025 10:16:34 +0800 Subject: [PATCH 008/110] =?UTF-8?q?=E7=BB=91=E5=AE=9A=E4=BB=98=E6=AC=BE?= =?UTF-8?q?=E8=AE=B0=E5=BD=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/common/service/FinReceiptService.php | 9 ++-- app/common/validate/FinReceiptValidate.php | 62 +++++++++++----------- config/route.php | 1 + 3 files changed, 38 insertions(+), 34 deletions(-) diff --git a/app/common/service/FinReceiptService.php b/app/common/service/FinReceiptService.php index e50bc7d..bf83f38 100644 --- a/app/common/service/FinReceiptService.php +++ b/app/common/service/FinReceiptService.php @@ -54,6 +54,8 @@ class FinReceiptService // 没有付款记录的收款记录 if (isset($p['payment_apply']) && $p['payment_apply'] == 'not') { + $config['where']['payment_apply_id'] = ['null']; + } elseif (isset($p['payment_apply']) && $p['payment_apply'] == 'already') { $config['where']['payment_apply_id'] = ['not null']; } @@ -110,7 +112,8 @@ class FinReceiptService public function beforeServiceUpdate($data, Request $request) { $data['updated_by'] = $request->user->uid; - $data['arrived_at'] = strtotime($data['arrived_at']); + if(isset($data['arrived_at'])) + $data['arrived_at'] = strtotime($data['arrived_at']); if(isset($data['received_at'])) $data['received_at'] = strtotime($data['received_at']); @@ -118,8 +121,8 @@ class FinReceiptService } public function afterServiceUpdate($res) { - $receipt = FinReceipt::where('id',$res->id)->first()->toArray(); - $receipt = array_merge($receipt,['receipt_id' => $res->id]); + $receipt = FinReceipt::where('id',$res['id'])->first()->toArray(); + $receipt = array_merge($receipt,['receipt_id' => $res['id']]); unset($receipt['id']); FinReceiptHis::create($receipt); } diff --git a/app/common/validate/FinReceiptValidate.php b/app/common/validate/FinReceiptValidate.php index c006dee..237b03e 100644 --- a/app/common/validate/FinReceiptValidate.php +++ b/app/common/validate/FinReceiptValidate.php @@ -8,49 +8,49 @@ class FinReceiptValidate extends Validate { // 规则 protected $rule = [ - 'payment_apply_id' => 'integer|gt:0', - 'country_id' => 'require|integer|gt:0', - 'remitter' => 'require', - 'account_id' => 'require|integer|gt:0', - 'currency_id' => 'require|integer|gt:0', - 'amount' => 'require|gt:0', - 'exchange_rate' => 'require|gt:0', - 'arrived_at' => 'require', - 'is_domestic_return' => 'require|integer', +// 'payment_apply_id' => 'integer|gt:0', +// 'country_id' => 'require|integer|gt:0', +// 'remitter' => 'require', +// 'account_id' => 'require|integer|gt:0', +// 'currency_id' => 'require|integer|gt:0', +// 'amount' => 'require|gt:0', +// 'exchange_rate' => 'require|gt:0', +// 'arrived_at' => 'require', +// 'is_domestic_return' => 'require|integer', ]; // 提示 protected $message = [ - 'payment_apply_id.integer' => '付款申请ID必须是整数', - 'payment_apply_id.gt' => '付款申请ID必须大于0', - 'country_id.require' => '付款来源国家ID不能为空', - 'country_id.integer' => '付款来源国家ID必须是整数', - 'country_id.gt' => '付款来源国家ID必须大于0', - 'remitter.require' => '汇款人不能为空', - 'account_id.require' => '收款账号ID不能为空', - 'account_id.integer' => '收款账号ID必须是整数', - 'account_id.gt' => '收款账号ID必须大于0', - 'currency_id.require' => '收款币种ID不能为空', - 'currency_id.integer' => '收款币种ID必须是整数', - 'currency_id.gt' => '收款币种ID必须大于0', - 'amount.require' => '收款金额不能为空', - 'amount.gt' => '收款金额必须大于0', - 'exchange_rate.require' => '实际汇率不能为空', - 'exchange_rate.gt' => '实际汇率必须大于0', - 'arrived_at.require' => '到账时间不能为空', - 'arrived_at.gt' => '到账时间必须大于0', - 'is_domestic_return.require' => '是否回转内地不能为空', - 'is_domestic_return.integer' => '是否回转内地必须是整数', +// 'payment_apply_id.integer' => '付款申请ID必须是整数', +// 'payment_apply_id.gt' => '付款申请ID必须大于0', +// 'country_id.require' => '付款来源国家ID不能为空', +// 'country_id.integer' => '付款来源国家ID必须是整数', +// 'country_id.gt' => '付款来源国家ID必须大于0', +// 'remitter.require' => '汇款人不能为空', +// 'account_id.require' => '收款账号ID不能为空', +// 'account_id.integer' => '收款账号ID必须是整数', +// 'account_id.gt' => '收款账号ID必须大于0', +// 'currency_id.require' => '收款币种ID不能为空', +// 'currency_id.integer' => '收款币种ID必须是整数', +// 'currency_id.gt' => '收款币种ID必须大于0', +// 'amount.require' => '收款金额不能为空', +// 'amount.gt' => '收款金额必须大于0', +// 'exchange_rate.require' => '实际汇率不能为空', +// 'exchange_rate.gt' => '实际汇率必须大于0', +// 'arrived_at.require' => '到账时间不能为空', +// 'arrived_at.gt' => '到账时间必须大于0', +// 'is_domestic_return.require' => '是否回转内地不能为空', +// 'is_domestic_return.integer' => '是否回转内地必须是整数', ]; // 场景 protected $scene = [ 'store' => [ - 'payment_apply_id', 'country_id', 'remitter', 'account_id', 'currency_id', 'amount', 'exchange_rate', 'arrived_at' +// 'payment_apply_id', 'country_id', 'remitter', 'account_id', 'currency_id', 'amount', 'exchange_rate', 'arrived_at' ], 'update' => [ - +// 'payment_apply_id' ] ]; } \ No newline at end of file diff --git a/config/route.php b/config/route.php index f2c9cb7..be37a5b 100644 --- a/config/route.php +++ b/config/route.php @@ -358,6 +358,7 @@ Route::group('/admin', function () { Route::any('/show', [app\common\controller\FinReceiptController::class, 'show']); Route::any('/store', [app\common\controller\FinReceiptController::class, 'store']); Route::any('/update', [app\common\controller\FinReceiptController::class, 'update']); + Route::any('/payment', [app\common\controller\FinReceiptController::class, 'update']); Route::any('/destroy', [app\common\controller\FinReceiptController::class, 'destroy']); }); From 435d3ce67737213c36f5faaa5a307182b08f7dd7 Mon Sep 17 00:00:00 2001 From: "WIN-C27C5MSVTPO\\16961" <1696136552@qq.com> Date: Wed, 24 Sep 2025 15:23:47 +0800 Subject: [PATCH 009/110] =?UTF-8?q?=E4=BB=98=E6=AC=BE=E8=AE=B0=E5=BD=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/FinPaymentApplyController.php | 14 ++- app/common/model/FinPaymentApply.php | 10 +- app/common/service/FinPaymentApplyService.php | 106 ++++++++++++++++++ 3 files changed, 120 insertions(+), 10 deletions(-) diff --git a/app/common/controller/FinPaymentApplyController.php b/app/common/controller/FinPaymentApplyController.php index abf31a8..3d35f09 100644 --- a/app/common/controller/FinPaymentApplyController.php +++ b/app/common/controller/FinPaymentApplyController.php @@ -9,6 +9,7 @@ use app\common\model\SysAccount; use app\common\model\SysCurrency; use app\common\model\SysDictItem; use app\common\model\User; +use app\common\service\OssService; use app\common\traits\crud\CrudControllerTrait; use app\common\service\FinPaymentApplyService; @@ -43,8 +44,19 @@ class FinPaymentApplyController extends BaseController $data['country_name'] = Country::where('id', $data['country_id'])->first()->cn_addr; if (isset($data['currency_id'])) //付款币种 $data['currency_name'] = SysCurrency::where('id', $data['currency_id'])->first()->currency_name; - if (isset($data['submit_files'])) + $OssService = new OssService(); + if (isset($data['submit_files'])){ $data['submit_files'] = json_decode($data['submit_files'], true); + foreach ($data['submit_files'] as $key => $value) { + $data['submit_files'][$key] = [ + 'file_src' => $value['file_src'], + 'file_full' => $OssService->generatePrivateUrl($value['file_src']), + 'file_name' => $value['file_name'], + ]; + } + } + if (isset($data['voucher_file'])) + $data['voucher_file_full'] = $OssService->generatePrivateUrl($data['voucher_file']); if (isset($data['created_at'])) $data['created_at'] = date('Y-m-d H:i:s', $data['created_at']); if (isset($data['updated_at'])) diff --git a/app/common/model/FinPaymentApply.php b/app/common/model/FinPaymentApply.php index 0556210..f02f137 100644 --- a/app/common/model/FinPaymentApply.php +++ b/app/common/model/FinPaymentApply.php @@ -40,7 +40,7 @@ class FinPaymentApply extends Model /** * 允许自动填充模型字段 */ - protected $fillable = ['id', 'apply_no', 'doc_user_id', 'sales_user_id', 'corp_id', 'account_id', 'currency_id', 'amount', 'swift', 'remit_bank', 'country_id', 'remitter', 'exchange_rate', 'total_amount_cny', 'voucher_file', 'submit_files', 'created_at', 'updated_at']; + protected $fillable = ['id', 'apply_no', 'doc_user_id', 'sales_user_id', 'corp_id', 'account_id', 'currency_id', 'amount', 'swift', 'remit_bank', 'country_id', 'remitter', 'exchange_rate', 'total_amount_cny', 'voucher_file', 'voucher_name', 'submit_files', 'created_at', 'updated_at']; /** * 不允许自动填充模型字段 @@ -55,14 +55,6 @@ class FinPaymentApply extends Model return $this->hasOne(FinReceipt::class, 'payment_apply_id', 'id'); } - /** - * 备货单 - */ - public function stock() - { - return $this->belongsToMany(StockOrders::class, 'fin_payment_stock', 'payment_apply_id', 'stock_id'); - } - /** * 关联的备货单(通过中间表) */ diff --git a/app/common/service/FinPaymentApplyService.php b/app/common/service/FinPaymentApplyService.php index 14b9d64..294bc01 100644 --- a/app/common/service/FinPaymentApplyService.php +++ b/app/common/service/FinPaymentApplyService.php @@ -45,6 +45,23 @@ class FinPaymentApplyService case 'index': // ====== ====== 排序 ====== ====== $config['order']['id'] = 'desc'; + + // ====== ====== 筛选 ====== ====== + // 汇款国家 + if (isset($p['country_id']) && !empty($p['country_id'])) { + $config['where']['country_id'] = $p['country_id']; + } + + // 收款账户筛选 + if (isset($p['account_id']) && !empty($p['account_id'])) { + $config['where']['account_id'] = $p['account_id']; + } + + // 关键字 + if (isset($p['keyword']) && $p['keyword']) { + $k = '%' . $p['keyword'] . '%'; + $config['orWhere'][] = ['remitter', 'like', $k]; + } break; case 'show': $config['where']['id'] = $p['id']; @@ -125,4 +142,93 @@ class FinPaymentApplyService $timestamp = substr(time(), -6); return $secondChar . $timestamp; } + + /** + * 根据相似度排序付款记录 + * @param Request $request + * @return \Illuminate\Contracts\Pagination\LengthAwarePaginator|\Illuminate\Pagination\LengthAwarePaginator + */ + public function complexQueryIndex(Request $request) + { + $limit = (int)$request->get('size', 15); + $page = (int)$request->get('current', 1); + $param = $request->all(); + + $where = []; + + // 汇款国家 + if (isset($param['country_id']) && !empty($param['country_id'])) { + $where[] = ['country_id', $param['country_id']]; + } + // 收款账户筛选 + if (isset($param['account_id']) && !empty($param['account_id'])) { + $where[] = ['account_id', $param['account_id']]; + } + + // 关键字 + if (isset($param['keyword']) && $param['keyword']) { + $k = '%' . $param['keyword'] . '%'; + $where[] = ['remitter', 'like', $k]; + } + + // 构建查询 + $query = $this->modelClass::where($where); + + // 应用是否有关联收款记录的筛选 + $correlation = $param['correlation']??''; + if ($correlation === 'receipt') { + $query->whereHas('receipt'); + } elseif ($correlation === 'unbind') { + $query->whereDoesntHave('receipt'); + } + + if (isset($param['scene']) && $param['scene'] == 'list') { + return $query->paginate($limit, ['*'], 'page', $page); + } + + // 计算七天前的时间戳 + $sevenDaysAgo = time() - (10 * 24 * 60 * 60); + + // 构建相似度排序逻辑 + $similarityOrder = []; + + // 如果有传入参数,计算相似度 + if (isset($param['account_id']) && !empty($param['account_id'])) { + $similarityOrder[] = "CASE WHEN account_id = {$param['account_id']} THEN 0 ELSE 1 END"; + } + + if (isset($param['country_id']) && !empty($param['country_id'])) { + $similarityOrder[] = "CASE WHEN country_id = {$param['country_id']} THEN 0 ELSE 1 END"; + } + + if (isset($param['keyword']) && $param['keyword']) { + $keyword = addslashes($param['keyword']); + $similarityOrder[] = "CASE WHEN remitter LIKE '%{$keyword}%' THEN 0 ELSE 1 END"; + } + + if (isset($param['amount']) && !empty($param['amount'])) { + $amount = (float)$param['amount']; + $similarityOrder[] = "ABS(amount - {$amount})"; + } + + // 构建排序SQL + $orderByClause = ""; + if (!empty($similarityOrder)) { + $orderByClause = "CASE + WHEN created_at >= {$sevenDaysAgo} THEN + (" . implode(' + ', $similarityOrder) . ") + ELSE 999999 + END ASC, "; + } + + $orderByClause .= "CASE + WHEN created_at < {$sevenDaysAgo} THEN id + ELSE 0 + END DESC"; + + $query->orderByRaw($orderByClause); + + return $query->paginate($limit, ['*'], 'page', $page); + } + } \ No newline at end of file From 82e4435a2cca7569342524a33d270f5018484385 Mon Sep 17 00:00:00 2001 From: "WIN-C27C5MSVTPO\\16961" <1696136552@qq.com> Date: Wed, 24 Sep 2025 17:49:14 +0800 Subject: [PATCH 010/110] =?UTF-8?q?=E6=94=B6=E6=B1=87=E9=99=84=E4=BB=B6?= =?UTF-8?q?=E5=90=8D=E7=A7=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/common/controller/FinReceiptController.php | 5 +++++ app/common/model/FinReceipt.php | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/app/common/controller/FinReceiptController.php b/app/common/controller/FinReceiptController.php index c80dc3a..a443537 100644 --- a/app/common/controller/FinReceiptController.php +++ b/app/common/controller/FinReceiptController.php @@ -5,6 +5,7 @@ use app\common\controller\BaseController; use app\common\model\Country; use app\common\model\SysAccount; use app\common\model\SysCurrency; +use app\common\service\OssService; use app\common\traits\crud\CrudControllerTrait; use app\common\service\FinReceiptService; @@ -41,6 +42,8 @@ class FinReceiptController extends BaseController $data['country_name'] = Country::where('id', $data['currency_id'])->first()->cn_addr; if (isset($data['currency_id'])) //付款币种 $data['currency_name'] = SysCurrency::where('id', $data['currency_id'])->first()->currency_name; + if (isset($data['fx_receipt_file'])) + $data['fx_receipt_file_full'] = (new OssService)->generatePrivateUrl($data['fx_receipt_file']); return $data; } @@ -66,6 +69,8 @@ class FinReceiptController extends BaseController $data['country_name'] = Country::where('id', $data['currency_id'])->first()->cn_addr; if (isset($data['currency_id'])) //付款币种 $data['currency_name'] = SysCurrency::where('id', $data['currency_id'])->first()->currency_name; + if (isset($data['fx_receipt_file'])) + $data['fx_receipt_file_full'] = (new OssService)->generatePrivateUrl($data['fx_receipt_file']); return $data; } } \ No newline at end of file diff --git a/app/common/model/FinReceipt.php b/app/common/model/FinReceipt.php index c87ab12..dbefafa 100644 --- a/app/common/model/FinReceipt.php +++ b/app/common/model/FinReceipt.php @@ -40,7 +40,7 @@ class FinReceipt extends Model /** * 允许自动填充模型字段 */ - protected $fillable = ['id', 'receipt_no', 'payment_apply_id', 'country_id', 'remitter', 'account_id', 'currency_id', 'amount', 'discount_amount', 'total_amount_cny', 'exchange_rate', 'arrived_at', 'received_at', 'voucher_no', 'fx_receipt_file', 'is_domestic_return', 'created_by', 'updated_by', 'created_at', 'updated_at']; + protected $fillable = ['id', 'receipt_no', 'payment_apply_id', 'country_id', 'remitter', 'account_id', 'currency_id', 'amount', 'discount_amount', 'total_amount_cny', 'exchange_rate', 'arrived_at', 'received_at', 'voucher_no', 'fx_receipt_file', 'fx_receipt_file_name', 'is_domestic_return', 'created_by', 'updated_by', 'created_at', 'updated_at']; /** * 不允许自动填充模型字段 From e0b11718a744fc3d1dd51ee53d6379ba1ecd4018 Mon Sep 17 00:00:00 2001 From: "WIN-C27C5MSVTPO\\16961" <1696136552@qq.com> Date: Wed, 24 Sep 2025 17:55:16 +0800 Subject: [PATCH 011/110] =?UTF-8?q?=E6=94=B6=E6=AC=BE=E8=AE=B0=E5=BD=95?= =?UTF-8?q?=E5=85=B3=E8=81=94=E5=A4=87=E8=B4=A7=E5=8D=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/common/model/FinReceipt.php | 8 -------- 1 file changed, 8 deletions(-) diff --git a/app/common/model/FinReceipt.php b/app/common/model/FinReceipt.php index dbefafa..7347be0 100644 --- a/app/common/model/FinReceipt.php +++ b/app/common/model/FinReceipt.php @@ -55,14 +55,6 @@ class FinReceipt extends Model return $this->belongsTo(FinPaymentApply::class, 'payment_apply_id', 'id'); } - /** - * 备货单 - */ - public function stock() - { - return $this->belongsToMany(StockOrders::class, 'fin_payment_stock', 'receipt_id', 'stock_id'); - } - /** * 关联的备货单(通过中间表) */ From ec2502cd586a2060a0862ffa8b37b6fa1045dd47 Mon Sep 17 00:00:00 2001 From: "WIN-C27C5MSVTPO\\16961" <1696136552@qq.com> Date: Thu, 25 Sep 2025 17:48:31 +0800 Subject: [PATCH 012/110] =?UTF-8?q?=E6=94=B6=E6=AC=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/common/service/FinReceiptService.php | 95 +++++++++++++++++++++++- 1 file changed, 93 insertions(+), 2 deletions(-) diff --git a/app/common/service/FinReceiptService.php b/app/common/service/FinReceiptService.php index bf83f38..2f77af5 100644 --- a/app/common/service/FinReceiptService.php +++ b/app/common/service/FinReceiptService.php @@ -59,7 +59,7 @@ class FinReceiptService $config['where']['payment_apply_id'] = ['not null']; } - // 收款账户筛选 + // 收款申请筛选 if (isset($p['payment_apply_id']) && !empty($p['payment_apply_id'])) { $config['where']['payment_apply_id'] = $p['payment_apply_id']; } @@ -70,7 +70,7 @@ class FinReceiptService } // 关键字 - if (isset($p['keyword']) && $p['keyword']) { + if (isset($p['$query']) && $p['keyword']) { $k = '%' . $p['keyword'] . '%'; $config['orWhere'][] = ['remitter', 'like', $k]; } @@ -160,4 +160,95 @@ class FinReceiptService $timestamp = substr(time(), -6); return $secondChar . $timestamp; } + + /** + * 根据相似度排序付款记录 + * @param Request $request + * @return \Illuminate\Contracts\Pagination\LengthAwarePaginator|\Illuminate\Pagination\LengthAwarePaginator + */ + public function complexQueryIndex(Request $request) + { + $limit = (int)$request->get('size', 15); + $page = (int)$request->get('current', 1); + $param = $request->all(); + + $where = []; + + // 汇款国家 + if (isset($param['country_id']) && !empty($param['country_id'])) { + $where[] = ['country_id', $param['country_id']]; + } + // 收款账户筛选 + if (isset($param['account_id']) && !empty($param['account_id'])) { + $where[] = ['account_id', $param['account_id']]; + } + // 收款申请筛选 + if (isset($param['payment_apply_id']) && !empty($param['payment_apply_id'])) { + $where[]['payment_apply_id'] = $param['payment_apply_id']; + } + + // 关键字 + if (isset($param['keyword']) && $param['keyword']) { + $k = '%' . $param['keyword'] . '%'; + $where[] = ['remitter', 'like', $k]; + } + + // 构建查询 + $query = $this->modelClass::where($where); + + // 没有付款记录的收款记录 + if (isset($param['payment_apply']) && $param['payment_apply'] == 'not') { + $query->whereNull('payment_apply_id'); + } elseif (isset($param['payment_apply']) && $param['payment_apply'] == 'already') { + $query->whereNotNull('payment_apply_id'); + } + + if (isset($param['scene']) && $param['scene'] == 'list') { + return $query->paginate($limit, ['*'], 'page', $page); + } + + // 计算七天前的时间戳 + $sevenDaysAgo = time() - (10 * 24 * 60 * 60); + + // 构建相似度排序逻辑 + $similarityOrder = []; + + // 如果有传入参数,计算相似度 + if (isset($param['account_id']) && !empty($param['account_id'])) { + $similarityOrder[] = "CASE WHEN account_id = {$param['account_id']} THEN 0 ELSE 1 END"; + } + + if (isset($param['country_id']) && !empty($param['country_id'])) { + $similarityOrder[] = "CASE WHEN country_id = {$param['country_id']} THEN 0 ELSE 1 END"; + } + + if (isset($param['keyword']) && $param['keyword']) { + $keyword = addslashes($param['keyword']); + $similarityOrder[] = "CASE WHEN remitter LIKE '%{$keyword}%' THEN 0 ELSE 1 END"; + } + + if (isset($param['amount']) && !empty($param['amount'])) { + $amount = (float)$param['amount']; + $similarityOrder[] = "ABS(amount - {$amount})"; + } + + // 构建排序SQL + $orderByClause = ""; + if (!empty($similarityOrder)) { + $orderByClause = "CASE + WHEN created_at >= {$sevenDaysAgo} THEN + (" . implode(' + ', $similarityOrder) . ") + ELSE 999999 + END ASC, "; + } + + $orderByClause .= "CASE + WHEN created_at < {$sevenDaysAgo} THEN id + ELSE 0 + END DESC"; + + $query->orderByRaw($orderByClause); + + return $query->paginate($limit, ['*'], 'page', $page); + } } \ No newline at end of file From ae6cd4ec78739fee47a584d4b5c55b70c3ea157d Mon Sep 17 00:00:00 2001 From: "WIN-C27C5MSVTPO\\16961" <1696136552@qq.com> Date: Fri, 26 Sep 2025 16:43:09 +0800 Subject: [PATCH 013/110] =?UTF-8?q?=E5=8F=98=E6=9B=B4=E5=8E=86=E5=8F=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/FinReceiptHisController.php | 77 +++++++++++++++++++ app/common/logic/FinReceiptHisLogic.php | 17 ++++ app/common/service/FinReceiptHisService.php | 62 +++++++++++++++ app/common/validate/FinReceiptHisValidate.php | 29 +++++++ config/route.php | 5 ++ 5 files changed, 190 insertions(+) create mode 100644 app/common/controller/FinReceiptHisController.php create mode 100644 app/common/logic/FinReceiptHisLogic.php create mode 100644 app/common/service/FinReceiptHisService.php create mode 100644 app/common/validate/FinReceiptHisValidate.php diff --git a/app/common/controller/FinReceiptHisController.php b/app/common/controller/FinReceiptHisController.php new file mode 100644 index 0000000..eb57d99 --- /dev/null +++ b/app/common/controller/FinReceiptHisController.php @@ -0,0 +1,77 @@ +service = new FinReceiptHisService(); + } + + /** + * 列表页数据格式化 + * + * @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['arrived_at'])) + $data['arrived_at'] = date('Y-m-d H:i:s', $data['arrived_at']); + if (isset($data['received_at'])) + $data['received_at'] = date('Y-m-d H:i:s', $data['received_at']); + if (isset($data['account_id'])) //收款账号 + $data['account_name'] = SysAccount::where('id', $data['account_id'])->first()->account_name; + if (isset($data['country_id'])) //付款来源国家 + $data['country_name'] = Country::where('id', $data['currency_id'])->first()->cn_addr; + if (isset($data['currency_id'])) //付款币种 + $data['currency_name'] = SysCurrency::where('id', $data['currency_id'])->first()->currency_name; + if (isset($data['fx_receipt_file'])) + $data['fx_receipt_file_full'] = (new OssService)->generatePrivateUrl($data['fx_receipt_file']); + return $data; + } + + /** + * 数据格式化 + * + * @param array $data + * @return array + */ + protected function showFormat($data) + { + if (isset($data['created_at'])) + $data['created_at'] = date('Y-m-d H:i:s', $data['created_at']); + if (isset($data['updated_at'])) + $data['updated_at'] = date('Y-m-d H:i:s', $data['updated_at']); + if (isset($data['arrived_at'])) + $data['arrived_at'] = date('Y-m-d H:i:s', $data['arrived_at']); + if (isset($data['received_at'])) + $data['received_at'] = date('Y-m-d H:i:s', $data['received_at']); + if (isset($data['account_id'])) //收款账号 + $data['account_name'] = SysAccount::where('id', $data['account_id'])->first()->account_name; + if (isset($data['country_id'])) //付款来源国家 + $data['country_name'] = Country::where('id', $data['currency_id'])->first()->cn_addr; + if (isset($data['currency_id'])) //付款币种 + $data['currency_name'] = SysCurrency::where('id', $data['currency_id'])->first()->currency_name; + if (isset($data['fx_receipt_file'])) + $data['fx_receipt_file_full'] = (new OssService)->generatePrivateUrl($data['fx_receipt_file']); + return $data; + } +} \ No newline at end of file diff --git a/app/common/logic/FinReceiptHisLogic.php b/app/common/logic/FinReceiptHisLogic.php new file mode 100644 index 0000000..3e90dac --- /dev/null +++ b/app/common/logic/FinReceiptHisLogic.php @@ -0,0 +1,17 @@ +validator; + } + + /** + * 构建查询条件 + */ + protected function buildQueryConfig(Request $request, string $scene = 'index'): array + { + $p = $request->all(); + + $config = []; + $config['model'] = $this->modelClass; + switch ($scene) { + case 'index': + if(empty($p['receipt_id'])){ + throw new BusinessException('receipt_id不能为空'); + } + // ====== ====== 排序 ====== ====== + $config['order']['id'] = 'desc'; + + // ====== ====== 筛选 ====== ====== + $config['where']['receipt_id'] = $p['receipt_id']; + + break; + case 'show': + $config['where']['id'] = $p['id']; + break; + } + + return $config; + } +} \ No newline at end of file diff --git a/app/common/validate/FinReceiptHisValidate.php b/app/common/validate/FinReceiptHisValidate.php new file mode 100644 index 0000000..7a1f7c4 --- /dev/null +++ b/app/common/validate/FinReceiptHisValidate.php @@ -0,0 +1,29 @@ + [ + + ], + + 'update' => [ + + ] + ]; +} \ No newline at end of file diff --git a/config/route.php b/config/route.php index be37a5b..3f7477f 100644 --- a/config/route.php +++ b/config/route.php @@ -361,6 +361,11 @@ Route::group('/admin', function () { Route::any('/payment', [app\common\controller\FinReceiptController::class, 'update']); Route::any('/destroy', [app\common\controller\FinReceiptController::class, 'destroy']); }); + // 收款记录历史 + Route::group('/receipt/his', function () { + Route::any('/list', [app\common\controller\FinReceiptHisController::class, 'index']); + Route::any('/show', [app\common\controller\FinReceiptHisController::class, 'show']); + }); // 付款申请 Route::group('/payment', function () { From 40dfd99f4e59d88952b30168e706af70b14c63b6 Mon Sep 17 00:00:00 2001 From: "WIN-C27C5MSVTPO\\16961" <1696136552@qq.com> Date: Sun, 28 Sep 2025 09:47:09 +0800 Subject: [PATCH 014/110] =?UTF-8?q?=E5=85=B3=E8=81=94=E6=9F=A5=E8=AF=A2?= =?UTF-8?q?=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/FinPaymentApplyController.php | 33 +++++++++++++------ .../controller/FinReceiptController.php | 15 +++++---- 2 files changed, 31 insertions(+), 17 deletions(-) diff --git a/app/common/controller/FinPaymentApplyController.php b/app/common/controller/FinPaymentApplyController.php index 3d35f09..d8158fa 100644 --- a/app/common/controller/FinPaymentApplyController.php +++ b/app/common/controller/FinPaymentApplyController.php @@ -37,13 +37,13 @@ class FinPaymentApplyController extends BaseController if (isset($data['sales_user_id'])) //业务员 $data['sales_user_name'] = User::where('id', $data['doc_user_id'])->first()->name; if (isset($data['corp_id'])) //公司(渠道) - $data['corp_name'] = SysDictItem::where('id', $data['corp_id'])->first()->item_label; + $data['corp_name'] = SysDictItem::where('id', $data['corp_id'])->value('item_label')??''; if (isset($data['account_id'])) //收款账号 - $data['account_name'] = SysAccount::where('id', $data['account_id'])->first()->account_name; + $data['account_name'] = SysAccount::where('id', $data['account_id'])->value('account_name')??''; if (isset($data['country_id'])) //付款来源国家 - $data['country_name'] = Country::where('id', $data['country_id'])->first()->cn_addr; + $data['country_name'] = Country::where('id', $data['country_id'])->value('cn_addr')??''; if (isset($data['currency_id'])) //付款币种 - $data['currency_name'] = SysCurrency::where('id', $data['currency_id'])->first()->currency_name; + $data['currency_name'] = SysCurrency::where('id', $data['currency_id'])->value('currency_name')??''; $OssService = new OssService(); if (isset($data['submit_files'])){ $data['submit_files'] = json_decode($data['submit_files'], true); @@ -55,7 +55,8 @@ class FinPaymentApplyController extends BaseController ]; } } - if (isset($data['voucher_file'])) + $data['voucher_file_full'] = ''; + if (!empty($data['voucher_file'])) $data['voucher_file_full'] = $OssService->generatePrivateUrl($data['voucher_file']); if (isset($data['created_at'])) $data['created_at'] = date('Y-m-d H:i:s', $data['created_at']); @@ -77,15 +78,27 @@ class FinPaymentApplyController extends BaseController if (isset($data['sales_user_id'])) //业务员 $data['sales_user_name'] = User::where('id', $data['doc_user_id'])->first()->name; if (isset($data['corp_id'])) //公司(渠道) - $data['corp_name'] = SysDictItem::where('id', $data['corp_id'])->first()->item_label; + $data['corp_name'] = SysDictItem::where('id', $data['corp_id'])->value('item_label')??''; if (isset($data['account_id'])) //收款账号 - $data['account_name'] = SysAccount::where('id', $data['account_id'])->first()->account_name; + $data['account_name'] = SysAccount::where('id', $data['account_id'])->value('account_name')??''; if (isset($data['country_id'])) //付款来源国家 - $data['country_name'] = Country::where('id', $data['country_id'])->first()->cn_addr; + $data['country_name'] = Country::where('id', $data['country_id'])->value('cn_addr')??''; if (isset($data['currency_id'])) //付款币种 - $data['currency_name'] = SysCurrency::where('id', $data['currency_id'])->first()->currency_name; - if (isset($data['submit_files'])) + $data['currency_name'] = SysCurrency::where('id', $data['currency_id'])->value('currency_name')??''; + $OssService = new OssService(); + if (isset($data['submit_files'])){ $data['submit_files'] = json_decode($data['submit_files'], true); + foreach ($data['submit_files'] as $key => $value) { + $data['submit_files'][$key] = [ + 'file_src' => $value['file_src'], + 'file_full' => $OssService->generatePrivateUrl($value['file_src']), + 'file_name' => $value['file_name'], + ]; + } + } + $data['voucher_file_full'] = ''; + if (!empty($data['voucher_file'])) + $data['voucher_file_full'] = $OssService->generatePrivateUrl($data['voucher_file']); if (isset($data['created_at'])) $data['created_at'] = date('Y-m-d H:i:s', $data['created_at']); if (isset($data['updated_at'])) diff --git a/app/common/controller/FinReceiptController.php b/app/common/controller/FinReceiptController.php index a443537..3dcdf72 100644 --- a/app/common/controller/FinReceiptController.php +++ b/app/common/controller/FinReceiptController.php @@ -37,12 +37,13 @@ class FinReceiptController extends BaseController if (isset($data['received_at'])) $data['received_at'] = date('Y-m-d H:i:s', $data['received_at']); if (isset($data['account_id'])) //收款账号 - $data['account_name'] = SysAccount::where('id', $data['account_id'])->first()->account_name; + $data['account_name'] = SysAccount::where('id', $data['account_id'])->value('account_name')??''; if (isset($data['country_id'])) //付款来源国家 - $data['country_name'] = Country::where('id', $data['currency_id'])->first()->cn_addr; + $data['country_name'] = Country::where('id', $data['country_id'])->value('cn_addr')??''; if (isset($data['currency_id'])) //付款币种 - $data['currency_name'] = SysCurrency::where('id', $data['currency_id'])->first()->currency_name; - if (isset($data['fx_receipt_file'])) + $data['currency_name'] = SysCurrency::where('id', $data['currency_id'])->value('currency_name')??''; + $data['fx_receipt_file_full'] = ''; + if (!empty($data['fx_receipt_file'])) $data['fx_receipt_file_full'] = (new OssService)->generatePrivateUrl($data['fx_receipt_file']); return $data; } @@ -64,11 +65,11 @@ class FinReceiptController extends BaseController if (isset($data['received_at'])) $data['received_at'] = date('Y-m-d H:i:s', $data['received_at']); if (isset($data['account_id'])) //收款账号 - $data['account_name'] = SysAccount::where('id', $data['account_id'])->first()->account_name; + $data['account_name'] = SysAccount::where('id', $data['account_id'])->value('account_name')??''; if (isset($data['country_id'])) //付款来源国家 - $data['country_name'] = Country::where('id', $data['currency_id'])->first()->cn_addr; + $data['country_name'] = Country::where('id', $data['country_id'])->value('cn_addr')??''; if (isset($data['currency_id'])) //付款币种 - $data['currency_name'] = SysCurrency::where('id', $data['currency_id'])->first()->currency_name; + $data['currency_name'] = SysCurrency::where('id', $data['currency_id'])->value('currency_name')??''; if (isset($data['fx_receipt_file'])) $data['fx_receipt_file_full'] = (new OssService)->generatePrivateUrl($data['fx_receipt_file']); return $data; From a3a6cfefc062b2c2d0a3c96da36054dfae973b91 Mon Sep 17 00:00:00 2001 From: "WIN-C27C5MSVTPO\\16961" <1696136552@qq.com> Date: Sun, 28 Sep 2025 09:56:01 +0800 Subject: [PATCH 015/110] =?UTF-8?q?=E6=94=B6=E6=AC=BE=E9=99=84=E4=BB=B6?= =?UTF-8?q?=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/common/controller/FinPaymentApplyController.php | 12 ++++++++---- app/common/controller/FinReceiptController.php | 11 ++++++++--- 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/app/common/controller/FinPaymentApplyController.php b/app/common/controller/FinPaymentApplyController.php index d8158fa..8739043 100644 --- a/app/common/controller/FinPaymentApplyController.php +++ b/app/common/controller/FinPaymentApplyController.php @@ -55,9 +55,11 @@ class FinPaymentApplyController extends BaseController ]; } } - $data['voucher_file_full'] = ''; - if (!empty($data['voucher_file'])) + if (!empty($data['voucher_file'])) { $data['voucher_file_full'] = $OssService->generatePrivateUrl($data['voucher_file']); + } elseif (isset($data['voucher_file'])) { + $data['voucher_file_full'] = ''; + } if (isset($data['created_at'])) $data['created_at'] = date('Y-m-d H:i:s', $data['created_at']); if (isset($data['updated_at'])) @@ -96,9 +98,11 @@ class FinPaymentApplyController extends BaseController ]; } } - $data['voucher_file_full'] = ''; - if (!empty($data['voucher_file'])) + if (!empty($data['voucher_file'])) { $data['voucher_file_full'] = $OssService->generatePrivateUrl($data['voucher_file']); + } elseif (isset($data['voucher_file'])) { + $data['voucher_file_full'] = ''; + } if (isset($data['created_at'])) $data['created_at'] = date('Y-m-d H:i:s', $data['created_at']); if (isset($data['updated_at'])) diff --git a/app/common/controller/FinReceiptController.php b/app/common/controller/FinReceiptController.php index 3dcdf72..76b1ed8 100644 --- a/app/common/controller/FinReceiptController.php +++ b/app/common/controller/FinReceiptController.php @@ -42,9 +42,11 @@ class FinReceiptController extends BaseController $data['country_name'] = Country::where('id', $data['country_id'])->value('cn_addr')??''; if (isset($data['currency_id'])) //付款币种 $data['currency_name'] = SysCurrency::where('id', $data['currency_id'])->value('currency_name')??''; - $data['fx_receipt_file_full'] = ''; - if (!empty($data['fx_receipt_file'])) + if (!empty($data['fx_receipt_file'])){ $data['fx_receipt_file_full'] = (new OssService)->generatePrivateUrl($data['fx_receipt_file']); + }elseif (isset($data['fx_receipt_file'])){ + $data['fx_receipt_file_full'] = ''; + } return $data; } @@ -70,8 +72,11 @@ class FinReceiptController extends BaseController $data['country_name'] = Country::where('id', $data['country_id'])->value('cn_addr')??''; if (isset($data['currency_id'])) //付款币种 $data['currency_name'] = SysCurrency::where('id', $data['currency_id'])->value('currency_name')??''; - if (isset($data['fx_receipt_file'])) + if (!empty($data['fx_receipt_file'])) { $data['fx_receipt_file_full'] = (new OssService)->generatePrivateUrl($data['fx_receipt_file']); + } elseif (isset($data['fx_receipt_file'])) { + $data['fx_receipt_file_full'] = ''; + } return $data; } } \ No newline at end of file From 90cce108b0b3e27d15351b70b420308fc5187a82 Mon Sep 17 00:00:00 2001 From: "WIN-C27C5MSVTPO\\16961" <1696136552@qq.com> Date: Sun, 28 Sep 2025 10:10:15 +0800 Subject: [PATCH 016/110] =?UTF-8?q?=E6=94=B6=E6=AC=BE=E8=AE=B0=E5=BD=95?= =?UTF-8?q?=E5=88=9B=E5=BB=BA=E4=B8=8E=E4=BF=AE=E6=94=B9=E4=BA=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/common/controller/FinPaymentApplyController.php | 8 ++++---- app/common/controller/FinReceiptHisController.php | 11 +++++++++++ 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/app/common/controller/FinPaymentApplyController.php b/app/common/controller/FinPaymentApplyController.php index 8739043..7dead81 100644 --- a/app/common/controller/FinPaymentApplyController.php +++ b/app/common/controller/FinPaymentApplyController.php @@ -33,9 +33,9 @@ class FinPaymentApplyController extends BaseController protected function indexFormat($data) { if (isset($data['doc_user_id'])) //内勤 - $data['doc_user_name'] = User::where('id', $data['doc_user_id'])->first()->name; + $data['doc_user_name'] = User::where('id', $data['doc_user_id'])->value('name')??''; if (isset($data['sales_user_id'])) //业务员 - $data['sales_user_name'] = User::where('id', $data['doc_user_id'])->first()->name; + $data['sales_user_name'] = User::where('id', $data['doc_user_id'])->value('name')??''; if (isset($data['corp_id'])) //公司(渠道) $data['corp_name'] = SysDictItem::where('id', $data['corp_id'])->value('item_label')??''; if (isset($data['account_id'])) //收款账号 @@ -76,9 +76,9 @@ class FinPaymentApplyController extends BaseController protected function showFormat($data) { if (isset($data['doc_user_id'])) //内勤 - $data['doc_user_name'] = User::where('id', $data['doc_user_id'])->first()->name; + $data['doc_user_name'] = User::where('id', $data['doc_user_id'])->value('name')??''; if (isset($data['sales_user_id'])) //业务员 - $data['sales_user_name'] = User::where('id', $data['doc_user_id'])->first()->name; + $data['sales_user_name'] = User::where('id', $data['doc_user_id'])->value('name')??''; if (isset($data['corp_id'])) //公司(渠道) $data['corp_name'] = SysDictItem::where('id', $data['corp_id'])->value('item_label')??''; if (isset($data['account_id'])) //收款账号 diff --git a/app/common/controller/FinReceiptHisController.php b/app/common/controller/FinReceiptHisController.php index eb57d99..e10d859 100644 --- a/app/common/controller/FinReceiptHisController.php +++ b/app/common/controller/FinReceiptHisController.php @@ -5,6 +5,7 @@ use app\common\controller\BaseController; use app\common\model\Country; use app\common\model\SysAccount; use app\common\model\SysCurrency; +use app\common\model\User; use app\common\service\OssService; use app\common\traits\crud\CrudControllerTrait; use app\common\service\FinReceiptHisService; @@ -45,6 +46,11 @@ class FinReceiptHisController extends BaseController $data['currency_name'] = SysCurrency::where('id', $data['currency_id'])->first()->currency_name; if (isset($data['fx_receipt_file'])) $data['fx_receipt_file_full'] = (new OssService)->generatePrivateUrl($data['fx_receipt_file']); + + if (isset($data['created_by'])) + $data['created_by'] = User::where('id', $data['created_by'])->value('name')??''; + if (isset($data['updated_by'])) + $data['updated_by'] = User::where('id', $data['updated_by'])->value('name')??''; return $data; } @@ -72,6 +78,11 @@ class FinReceiptHisController extends BaseController $data['currency_name'] = SysCurrency::where('id', $data['currency_id'])->first()->currency_name; if (isset($data['fx_receipt_file'])) $data['fx_receipt_file_full'] = (new OssService)->generatePrivateUrl($data['fx_receipt_file']); + + if (isset($data['created_by'])) + $data['created_by'] = User::where('id', $data['created_by'])->value('name')??''; + if (isset($data['updated_by'])) + $data['updated_by'] = User::where('id', $data['updated_by'])->value('name')??''; return $data; } } \ No newline at end of file From d05106f9dfc7cfc323ecc3f686c0d442a8f9de43 Mon Sep 17 00:00:00 2001 From: "WIN-C27C5MSVTPO\\16961" <1696136552@qq.com> Date: Thu, 9 Oct 2025 10:16:21 +0800 Subject: [PATCH 017/110] =?UTF-8?q?=E6=94=B6=E6=AC=BE=E8=AE=B0=E5=BD=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/FinPaymentApplyController.php | 8 +++++--- app/common/controller/FinReceiptController.php | 4 +++- app/common/controller/FinReceiptHisController.php | 15 ++++++++++----- 3 files changed, 18 insertions(+), 9 deletions(-) diff --git a/app/common/controller/FinPaymentApplyController.php b/app/common/controller/FinPaymentApplyController.php index 7dead81..9129390 100644 --- a/app/common/controller/FinPaymentApplyController.php +++ b/app/common/controller/FinPaymentApplyController.php @@ -81,8 +81,10 @@ class FinPaymentApplyController extends BaseController $data['sales_user_name'] = User::where('id', $data['doc_user_id'])->value('name')??''; if (isset($data['corp_id'])) //公司(渠道) $data['corp_name'] = SysDictItem::where('id', $data['corp_id'])->value('item_label')??''; - if (isset($data['account_id'])) //收款账号 + if (isset($data['account_id'])){ //收款账号 $data['account_name'] = SysAccount::where('id', $data['account_id'])->value('account_name')??''; + $data['account_short_desc'] = SysAccount::where('id', $data['account_id'])->value('short_desc')??''; + } if (isset($data['country_id'])) //付款来源国家 $data['country_name'] = Country::where('id', $data['country_id'])->value('cn_addr')??''; if (isset($data['currency_id'])) //付款币种 @@ -111,8 +113,8 @@ class FinPaymentApplyController extends BaseController $finPaymentStock = FinPaymentStock::where('payment_apply_id', $data['id'])->get(); $stock = []; foreach ($finPaymentStock as $v) { - $stock[$v['pi_no']] = ['pi_no' => $v['pi_no']]; - $stock[$v['pi_no']]['stock_id'] = [$v['stock_id']]; + $stock[$v['pi_no']]['pi_no'] = $v['pi_no']; + $stock[$v['pi_no']]['stock_id'][] = [$v['stock_id']]; } $data['stock'] = array_values($stock); return $data; diff --git a/app/common/controller/FinReceiptController.php b/app/common/controller/FinReceiptController.php index 76b1ed8..74052f7 100644 --- a/app/common/controller/FinReceiptController.php +++ b/app/common/controller/FinReceiptController.php @@ -36,8 +36,10 @@ class FinReceiptController extends BaseController $data['arrived_at'] = date('Y-m-d H:i:s', $data['arrived_at']); if (isset($data['received_at'])) $data['received_at'] = date('Y-m-d H:i:s', $data['received_at']); - if (isset($data['account_id'])) //收款账号 + if (isset($data['account_id'])) { //收款账号 $data['account_name'] = SysAccount::where('id', $data['account_id'])->value('account_name')??''; + $data['account_short_desc'] = SysAccount::where('id', $data['account_id'])->value('short_desc')??''; + } if (isset($data['country_id'])) //付款来源国家 $data['country_name'] = Country::where('id', $data['country_id'])->value('cn_addr')??''; if (isset($data['currency_id'])) //付款币种 diff --git a/app/common/controller/FinReceiptHisController.php b/app/common/controller/FinReceiptHisController.php index e10d859..16e4b3c 100644 --- a/app/common/controller/FinReceiptHisController.php +++ b/app/common/controller/FinReceiptHisController.php @@ -70,14 +70,19 @@ class FinReceiptHisController extends BaseController $data['arrived_at'] = date('Y-m-d H:i:s', $data['arrived_at']); if (isset($data['received_at'])) $data['received_at'] = date('Y-m-d H:i:s', $data['received_at']); - if (isset($data['account_id'])) //收款账号 - $data['account_name'] = SysAccount::where('id', $data['account_id'])->first()->account_name; + if (isset($data['account_id'])){ //收款账号 + $data['account_name'] = SysAccount::where('id', $data['account_id'])->value('account_name')??''; + $data['account_short_desc'] = SysAccount::where('id', $data['account_id'])->value('short_desc')??''; + } if (isset($data['country_id'])) //付款来源国家 - $data['country_name'] = Country::where('id', $data['currency_id'])->first()->cn_addr; + $data['country_name'] = Country::where('id', $data['country_id'])->value('cn_addr')??''; if (isset($data['currency_id'])) //付款币种 - $data['currency_name'] = SysCurrency::where('id', $data['currency_id'])->first()->currency_name; - if (isset($data['fx_receipt_file'])) + $data['currency_name'] = SysCurrency::where('id', $data['currency_id'])->value('currency_name')??''; + if (!empty($data['fx_receipt_file'])){ $data['fx_receipt_file_full'] = (new OssService)->generatePrivateUrl($data['fx_receipt_file']); + }elseif (isset($data['fx_receipt_file'])){ + $data['fx_receipt_file_full'] = ''; + } if (isset($data['created_by'])) $data['created_by'] = User::where('id', $data['created_by'])->value('name')??''; From 73ed4a413c84a81c76b2cacaf8a257f14b90feb6 Mon Sep 17 00:00:00 2001 From: "WIN-C27C5MSVTPO\\16961" <1696136552@qq.com> Date: Thu, 9 Oct 2025 10:24:13 +0800 Subject: [PATCH 018/110] =?UTF-8?q?=E4=BB=98=E6=AC=BE=E8=B4=A6=E5=8F=B7?= =?UTF-8?q?=E5=90=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/FinPaymentApplyController.php | 4 +++- app/common/controller/FinReceiptController.php | 4 +++- .../controller/FinReceiptHisController.php | 17 +++++++++++------ 3 files changed, 17 insertions(+), 8 deletions(-) diff --git a/app/common/controller/FinPaymentApplyController.php b/app/common/controller/FinPaymentApplyController.php index 9129390..e916df4 100644 --- a/app/common/controller/FinPaymentApplyController.php +++ b/app/common/controller/FinPaymentApplyController.php @@ -38,8 +38,10 @@ class FinPaymentApplyController extends BaseController $data['sales_user_name'] = User::where('id', $data['doc_user_id'])->value('name')??''; if (isset($data['corp_id'])) //公司(渠道) $data['corp_name'] = SysDictItem::where('id', $data['corp_id'])->value('item_label')??''; - if (isset($data['account_id'])) //收款账号 + if (isset($data['account_id'])){ //收款账号 $data['account_name'] = SysAccount::where('id', $data['account_id'])->value('account_name')??''; + $data['account_short_desc'] = SysAccount::where('id', $data['account_id'])->value('short_desc')??''; + } if (isset($data['country_id'])) //付款来源国家 $data['country_name'] = Country::where('id', $data['country_id'])->value('cn_addr')??''; if (isset($data['currency_id'])) //付款币种 diff --git a/app/common/controller/FinReceiptController.php b/app/common/controller/FinReceiptController.php index 74052f7..ddddfac 100644 --- a/app/common/controller/FinReceiptController.php +++ b/app/common/controller/FinReceiptController.php @@ -68,8 +68,10 @@ class FinReceiptController extends BaseController $data['arrived_at'] = date('Y-m-d H:i:s', $data['arrived_at']); if (isset($data['received_at'])) $data['received_at'] = date('Y-m-d H:i:s', $data['received_at']); - if (isset($data['account_id'])) //收款账号 + if (isset($data['account_id'])) { //收款账号 $data['account_name'] = SysAccount::where('id', $data['account_id'])->value('account_name')??''; + $data['account_short_desc'] = SysAccount::where('id', $data['account_id'])->value('short_desc')??''; + } if (isset($data['country_id'])) //付款来源国家 $data['country_name'] = Country::where('id', $data['country_id'])->value('cn_addr')??''; if (isset($data['currency_id'])) //付款币种 diff --git a/app/common/controller/FinReceiptHisController.php b/app/common/controller/FinReceiptHisController.php index 16e4b3c..7adcf0d 100644 --- a/app/common/controller/FinReceiptHisController.php +++ b/app/common/controller/FinReceiptHisController.php @@ -38,15 +38,20 @@ class FinReceiptHisController extends BaseController $data['arrived_at'] = date('Y-m-d H:i:s', $data['arrived_at']); if (isset($data['received_at'])) $data['received_at'] = date('Y-m-d H:i:s', $data['received_at']); - if (isset($data['account_id'])) //收款账号 - $data['account_name'] = SysAccount::where('id', $data['account_id'])->first()->account_name; + if (isset($data['account_id'])) { //收款账号 + $data['account_name'] = SysAccount::where('id', $data['account_id'])->value('account_name')??''; + $data['account_short_desc'] = SysAccount::where('id', $data['account_id'])->value('short_desc')??''; + } if (isset($data['country_id'])) //付款来源国家 - $data['country_name'] = Country::where('id', $data['currency_id'])->first()->cn_addr; + $data['country_name'] = Country::where('id', $data['country_id'])->value('cn_addr')??''; if (isset($data['currency_id'])) //付款币种 - $data['currency_name'] = SysCurrency::where('id', $data['currency_id'])->first()->currency_name; - if (isset($data['fx_receipt_file'])) + $data['currency_name'] = SysCurrency::where('id', $data['currency_id'])->value('currency_name')??''; + if (!empty($data['fx_receipt_file'])){ $data['fx_receipt_file_full'] = (new OssService)->generatePrivateUrl($data['fx_receipt_file']); - + }elseif (isset($data['fx_receipt_file'])){ + $data['fx_receipt_file_full'] = ''; + } + if (isset($data['created_by'])) $data['created_by'] = User::where('id', $data['created_by'])->value('name')??''; if (isset($data['updated_by'])) From 4c94392ef648fd15584f794569a094f6218c4ce1 Mon Sep 17 00:00:00 2001 From: mzl Date: Tue, 21 Oct 2025 13:34:26 +0800 Subject: [PATCH 019/110] ts --- app/common/service/StockOrdersService_bak.php | 1044 +++++++++++++---- app/common/utils/WsEvents.php | 11 +- .../approval/service/ApprovalFlowService.php | 3 +- plugin/webman/gateway/Events.php | 2 +- 4 files changed, 813 insertions(+), 247 deletions(-) diff --git a/app/common/service/StockOrdersService_bak.php b/app/common/service/StockOrdersService_bak.php index 65ef296..5ee03a9 100644 --- a/app/common/service/StockOrdersService_bak.php +++ b/app/common/service/StockOrdersService_bak.php @@ -12,6 +12,10 @@ use app\common\model\SysCurrency; use support\Request; use app\common\model\User; use support\Db; +use support\Model; +use app\common\model\ApprovalInstances; +use app\common\model\ApprovalRecords; +use support\exception\BusinessException; class StockOrdersService_bak { @@ -63,13 +67,13 @@ class StockOrdersService_bak $config['order']['id'] = 'desc'; - // $config['dataLimit']['type'] = 'relation'; - // $config['dataLimit']['userId'] = $request->user->uid; - // $config['dataLimit']['userField'] = 'salesperson_id'; + // $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']; @@ -93,113 +97,6 @@ class StockOrdersService_bak 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; @@ -210,7 +107,30 @@ class StockOrdersService_bak $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; + // } + + if ($data['is_final'] == 1) { + // 检查是否至少有一个车辆填写了vin + $hasVin = false; + foreach ($data['veh_list'] as $k => $v) { + if ($v['vin'] != '') { + $hasVin = true; + break; + } + } + // 如果没有任何车辆填写vin,则抛出异常 + if (!$hasVin) { + throw new BusinessException('至少需要为一辆车辆填写vin,才能提交审核'); + } + } + + // 检查绑定 + (new UserService())->checkBind($userId); return $data; } @@ -226,16 +146,27 @@ class StockOrdersService_bak } } // 启动审批 - $appro = new \app\common\utils\approval\service\ApprovalFlowService(); - $result = $appro->start('stock_orders', 'stock_orders', $res->id, $res->toArray(), $res->salesperson_id); - var_dump($result); - - // 异步生成pdf文件 - \Webman\RedisQueue\Redis::send('stock-order-pdf', ['id'=>$res->id]); + if ($res->approval_status == 1) { + $this->startApproval($res); + } return $res; } + /** + * 启动审批 + */ + public function startApproval($item) + { + // 启动审批 + $appro = new \app\common\utils\approval\service\ApprovalFlowService(); + $appro->start('stock_orders', 'stock_orders', $item->id, [], $item->salesperson_id); + // 异步生成pdf文件 + \Webman\RedisQueue\Redis::send('stock-order-pdf', ['id'=>$item->id]); + } + /** + * 创建PDF文件 + */ public function createdPdfFile($id) { $config = []; @@ -248,7 +179,7 @@ class StockOrdersService_bak $config['with'][] = 'moneyValues'; $config['with'][] = 'salesperson'; $item = $this->logicShow($config); - $pdfData = $this->pdfData($item); + $pdfData = $this->formatData($item); // 渲染模板为最终HTML $tplPath = app_path('common/utils/pdf/template/stockOrder.stub'); @@ -269,17 +200,6 @@ class StockOrdersService_bak $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, @@ -287,7 +207,7 @@ class StockOrdersService_bak 'category' => 'pdf', 'original_name' => $fileName ]; - $up = new UploadService(); + $up = new \app\common\service\UploadService(); $res = $up->uploadLocalFile($param); @unlink($dir . $fileName); @@ -299,6 +219,34 @@ class StockOrdersService_bak return $res; } + + /** + * 查询审批 + */ + 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'); @@ -306,130 +254,744 @@ class StockOrdersService_bak return $this->postProcess('Print', $item); } - - public function pdfData($item) + public function serviceDetail(Request $request) { - $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, '.', ',') - ]; + $config = $this->buildQueryConfig($request, 'detail'); + $item = $this->logicShow($config); + $detail = $this->formatData($item); + return $this->postProcess('Detail', $detail); + } - // 根据里程处理 新车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, '.', ','); - } - } + /** + * 审批 + */ + public function serviceApproval(Request $request) + { + $config = $this->buildQueryConfig($request, 'approval'); + $this->logicUpdateBy($config, [ + 'approval_status' => 1 + ]); + $item = $this->logicShow($config); + $this->startApproval($item); + return $this->postProcess('Approval', $item); + } + + /** + * 作废审批 + */ + public function serviceApprovalNullify(Request $request) + { + $config = $this->buildQueryConfig($request, 'approval'); + $item = $this->logicShow($config); + $this->approvalNullify($item); + return $this->postProcess('Approval', $item); + } + + /** + * 审批作废 + */ + public function approvalNullify($item) + { + $appro = new \app\common\utils\approval\service\ApprovalFlowService(); + $appro->startNullify('stock_orders_nullify', 'stock_orders', $item->id, [], $item->salesperson_id); + } + + /** + * 撤销审批 + */ + public function serviceApprovalRevoke(Request $request) + { + $config = $this->buildQueryConfig($request, 'approval'); + $item = $this->logicShow($config); + $this->approvalRevoke($item); + return $this->postProcess('Approval', $item); + } + + public function approvalRevoke($item) + { + $instance = \app\common\model\ApprovalInstances::where('biz_id', $item->id)->where('flow_type', 'stock_orders')->first(); + $appro = new \app\common\utils\approval\service\ApprovalFlowService(); + $appro->revoke($instance->id, $item->salesperson_id); + } + + /** + * 创建 + * + * @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); - $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; + 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; - 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); + $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, $item->mileage, $order); + // $item->addMoney('profit', $order->settlement_currency, $profit, $order->contract_exchange_rate, '利润'); + + // 累计到岸价 + $cifTotalAmount += $veh['total_price']['amount'] ?? 0; + $profitTotalAmount += $veh['profit']['amount'] ?? 0; + + // 处理改装项 + $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'] ?? []); + + $keysToRemove = ['vin', 'remark', 'modify_list', 'order_id']; + $array = array_diff_key($veh, array_flip($keysToRemove)); + $uniqueIdStr = json_encode($array); + + return md5($uniqueIdStr); + } + + /** + * 处理车辆价格信息 + */ + private function processVehiclePrices($item, array $veh): void + { + // unit_price 单价/成交价/收款价 + // total_price 总价 + // handling_fee 手续费 + // fob_transport_fee FOB运输费 + // port_misc_fee 港杂 + // retrofit_fee 改装费 + // ocean_freight 海运费 + // commission 佣金 + // purchase_tax 购置税 + // tax_refund 预估退税 + // clearance_price 报关价 + // profit 利润 + // official_price 官方指导价 + // discount_price 优惠价 + // listing_price 二手挂牌价 + // reduction_price 二手减价/优惠价 + // retention_price 留款/挂账 + // car_price 车价 + // modify_price 改装价 + + // 基础费用 + $priceFields = [ + 'unit_price' => '收款价', + 'total_price' => '总价', + 'handling_fee' => '手续费', + 'fob_transport_fee' => 'FOB运输费', + 'port_misc_fee' => '港杂', + 'retrofit_fee' => '改装费', + 'ocean_freight' => '海运费', + 'commission' => '佣金', + 'purchase_tax' => '购置税', + 'tax_refund' => '预估退税', + 'clearance_price' => '报关价', + 'profit' => '利润', + 'retention_price' => '挂账', + 'car_price' => '车价', + 'modify_price' => '改装价' + ]; + + foreach ($priceFields as $field => $description) { + $item->addMoney( + $field, + $veh[$field]['currency'], + (float)$veh[$field]['amount'], + $veh[$field]['rate'], + $description + ); + } + + // 根据里程处理新车或二手车价格 + if (!empty($veh['mileage']) && $veh['mileage'] > 0) { + // 二手车 + $item->addMoney( + 'listing_price', + $veh['listing_price']['currency'], + (float)$veh['listing_price']['amount'], + $veh['listing_price']['rate'], + '二手挂牌价' + ); + $item->addMoney( + 'reduction_price', + $veh['reduction_price']['currency'], + (float)$veh['reduction_price']['amount'], + $veh['reduction_price']['rate'], + '优惠价' + ); + } else { + // 新车 + $item->addMoney( + 'official_price', + $veh['official_price']['currency'], + (float)$veh['official_price']['amount'], + $veh['official_price']['rate'], + '官方指导价' + ); + $item->addMoney( + 'discount_price', + $veh['discount_price']['currency'], + (float)$veh['discount_price']['amount'], + $veh['discount_price']['rate'], + '优惠价' + ); + } + } + + /** + * 计算利润 + */ + private function calculateProfit(array $veh, int $mileage, $order): float + { + $contractExchangeRate = $order->contract_exchange_rate ?: 1; + + // 提取单价并进行汇率转换(如果需要) + $unitPriceRate = $veh['unit_price']['rate'] ?: 1; + if ($unitPriceRate != $contractExchangeRate) { + $unitPrice = (float)($veh['unit_price']['amount'] ?? 0) / $contractExchangeRate; + } else { + $unitPrice = (float)($veh['unit_price']['amount'] ?? 0); + } + + // 处理主要价格字段,根据是否需要汇率转换来计算 + if ($mileage > 0) { + // 二手车 + $listingPriceRate = $veh['listing_price']['rate'] ?: 1; + if ($listingPriceRate != $contractExchangeRate) { + $ttPrice = ($veh['listing_price']['amount'] ?? 0) / $contractExchangeRate; + } else { + $ttPrice = $veh['listing_price']['amount'] ?? 0; + } + + $reductionPriceRate = $veh['reduction_price']['rate'] ?: 1; + if ($reductionPriceRate != $contractExchangeRate) { + $ddPrice = ($veh['reduction_price']['amount'] ?? 0) / $contractExchangeRate; + } else { + $ddPrice = $veh['reduction_price']['amount'] ?? 0; + } + } else { + // 新车 + $officialPriceRate = $veh['official_price']['rate'] ?: 1; + if ($officialPriceRate != $contractExchangeRate) { + $ttPrice = ($veh['official_price']['amount'] ?? 0) / $contractExchangeRate; + } else { + $ttPrice = $veh['official_price']['amount'] ?? 0; + } + + $discountPriceRate = $veh['discount_price']['rate'] ?: 1; + if ($discountPriceRate != $contractExchangeRate) { + $ddPrice = ($veh['discount_price']['amount'] ?? 0) / $contractExchangeRate; + } else { + $ddPrice = $veh['discount_price']['amount'] ?? 0; + } + } + + // 处理其他费用字段,根据是否需要汇率转换来计算 + $handlingFeeRate = $veh['handling_fee']['rate'] ?: 1; + if ($handlingFeeRate != $contractExchangeRate) { + $handlingFee = (float)($veh['handling_fee']['amount'] ?? 0) / $contractExchangeRate; + } else { + $handlingFee = (float)($veh['handling_fee']['amount'] ?? 0); + } + + $fobTransportFeeRate = $veh['fob_transport_fee']['rate'] ?: 1; + if ($fobTransportFeeRate != $contractExchangeRate) { + $fobTransportFee = (float)($veh['fob_transport_fee']['amount'] ?? 0) / $contractExchangeRate; + } else { + $fobTransportFee = (float)($veh['fob_transport_fee']['amount'] ?? 0); + } + + $portMiscFeeRate = $veh['port_misc_fee']['rate'] ?: 1; + if ($portMiscFeeRate != $contractExchangeRate) { + $portMiscFee = (float)($veh['port_misc_fee']['amount'] ?? 0) / $contractExchangeRate; + } else { + $portMiscFee = (float)($veh['port_misc_fee']['amount'] ?? 0); + } + + $retrofitFeeRate = $veh['retrofit_fee']['rate'] ?: 1; + if ($retrofitFeeRate != $contractExchangeRate) { + $retrofitFee = (float)($veh['retrofit_fee']['amount'] ?? 0) / $contractExchangeRate; + } else { + $retrofitFee = (float)($veh['retrofit_fee']['amount'] ?? 0); + } + + $oceanFreightRate = $veh['ocean_freight']['rate'] ?: 1; + if ($oceanFreightRate != $contractExchangeRate) { + $oceanFreight = (float)($veh['ocean_freight']['amount'] ?? 0) / $contractExchangeRate; + } else { + $oceanFreight = (float)($veh['ocean_freight']['amount'] ?? 0); + } + + $commissionRate = $veh['commission']['rate'] ?: 1; + if ($commissionRate != $contractExchangeRate) { + $commission = (float)($veh['commission']['amount'] ?? 0) / $contractExchangeRate; + } else { + $commission = (float)($veh['commission']['amount'] ?? 0); + } + + $purchaseTaxRate = $veh['purchase_tax']['rate'] ?: 1; + if ($purchaseTaxRate != $contractExchangeRate) { + $purchaseTax = (float)($veh['purchase_tax']['amount'] ?? 0) / $contractExchangeRate; + } else { + $purchaseTax = (float)($veh['purchase_tax']['amount'] ?? 0); + } + + // 预估退税不需要参与汇率转换计算,前端已经计算过了 + $taxRefund = (float)($veh['tax_refund']['amount'] ?? 0); + + // 退税额 是增加利润 + return $unitPrice - $ttPrice + $ddPrice - $handlingFee - $fobTransportFee - $portMiscFee - $retrofitFee - $oceanFreight - $commission - $purchaseTax + $taxRefund; + } + + /** + * 处理改装项 + */ + 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); + } + } + + + // ====== ======= ====== ======= ====== 信息处理 ======= ====== ======= ====== ======= + + // PDF数据 + public function formatData($item) + { + // 提取车辆列表信息 + $vehListData = $this->prepareVehicleListData($item); + + // 提取订单金额信息 + $moneyValues = $this->prepareOrderMoneyValues($item); + + // 提取账户列表信息 + $accountList = $this->prepareAccountList($item); + + // 构建返回数据 $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, - + // 基本信息 + 'order_no' => $item->order_no, + 'salesperson_snapshot' => $item->salesperson_snapshot, + 'sign_img_url' => $this->getSignatureImageUrl($item->salesperson->sign_img ?? null), + '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 + 'veh_list' => array_values($vehListData['list']), + 'contract_exchange_rate' => $item->contract_exchange_rate, + 'cif_total_amount' => $moneyValues['cif_total_amount'] ?? [], + 'profit_total' => $moneyValues['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' => $accountList, + 'acc_index' => count($accountList) + 1, + 'currency_symbol' => $item->settlement_currency, + 'sign_imgs' => $this->queryAppro($item->id), + 'contract_url' => $this->getContractUrl($item->contract ?? null) ]; return $veh; } + + /** + * 准备车辆列表数据 + */ + private function prepareVehicleListData($item): array + { + $vehList = []; + $index = 0; + $currencyInfo = $this->getCurrencyInfo($item->settlement_currency); + + foreach ($item->items as $v) { + // 如果已存在相同车辆,增加数量 + if (isset($vehList[$v->unique_id])) { + $this->incrementVehicleCount($vehList[$v->unique_id], $currencyInfo); + // 拼接 remark 值 + if (!empty($v->remark)) { + if (!empty($vehList[$v->unique_id]['remark'])) { + $vehList[$v->unique_id]['remark'] .= "; " . $v->remark; + } else { + $vehList[$v->unique_id]['remark'] = $v->remark; + } + } + continue; + } + + $index++; + // 处理金额信息 + $moneyValues = $this->processItemMoneyValues($v); + + // 处理改装信息 + $modificationText = $this->getModificationText($v->options); + + // 计算单项利润 + // $itemProfit = $this->calculateProfit($moneyValues, $v->mileage, $item); + // var_dump($moneyValues); + $itemProfit = empty($moneyValues['profit']['amount']) ? 0 : $moneyValues['profit']['amount']; + + // 构建车辆信息 + $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' => $modificationText, + 'mileage' => $v->mileage, + 'remark' => $v->remark, + ] + $moneyValues; + + // 添加利润信息 + $vehList[$v->unique_id]['profit'] = [ + 'currency' => $item->settlement_currency, + 'amount' => $itemProfit, + 'currency_info' => $currencyInfo['currency_symbol'] . number_format($itemProfit, 2, '.', ',') + ]; + } + + return ['list' => $vehList]; + } + + /** + * 处理单项金额值 + */ + private function processItemMoneyValues($item): array + { + $moneyValues = []; + foreach ($item->moneyValues as $money) { + $moneyValues[$money->field] = [ + 'currency' => $money->currency, + 'amount' => $money->amount, + 'rate' => $money->exchange_rate, + 'currency_info' => $this->formatCurrency($money->amount, $money->currency) + ]; + } + return $moneyValues; + } + + /** + * 获取改装文本 + */ + private function getModificationText($options): string + { + $modificationText = ''; + foreach ($options as $option) { + $modificationText .= $option->option_name_snapshot . ' '; + } + return trim($modificationText); + } + + /** + * 增加车辆数量并更新相关金额显示 + */ + private function incrementVehicleCount(array &$vehicleData, array $currencyInfo): void + { + $vehicleData['num'] += 1; + + // 更新总价显示 + if (isset($vehicleData['total_price']['amount'])) { + $totalAmount = $vehicleData['total_price']['amount'] * $vehicleData['num']; + $vehicleData['total_price']['currency_info'] = $currencyInfo['currency_symbol'] . number_format($totalAmount, 2, '.', ','); + } + + // 更新利润显示 + if (isset($vehicleData['profit']['amount'])) { + $profitAmount = $vehicleData['profit']['amount'] * $vehicleData['num']; + $vehicleData['profit']['currency_info'] = $currencyInfo['currency_symbol'] . number_format($profitAmount, 2, '.', ','); + } + } + + /** + * 准备订单金额值 + */ + private function prepareOrderMoneyValues($item): array + { + $moneyValues = []; + foreach ($item->moneyValues as $money) { + $moneyValues[$money->field] = [ + 'currency' => $money->currency, + 'amount' => $money->amount, + 'rate' => $money->exchange_rate, + 'currency_info' => $this->formatCurrency($money->amount, $money->currency) + ]; + } + return $moneyValues; + } + + /** + * 准备账户列表 + */ + private function prepareAccountList($item): array + { + $accountList = []; + foreach ($item->accounts as $k => $account) { + $accountList[] = [ + 'index' => '收款' . ($k + 1), + 'remit_country_name_snapshot' => $account->remit_country_name_snapshot, + 'remit_date' => date('Y-m-d', $account->remit_date), + 'currency_name_snapshot' => $account->currency_name_snapshot, + 'account_name_snapshot' => $account->account_name_snapshot, + 'remitter_name' => $account->remitter_name, + 'amount' => $account->amount, + ]; + } + return $accountList; + } + + /** + * 获取货币信息 + */ + private function getCurrencyInfo(string $currencyCode): array + { + return SysCurrency::where('currency_code', $currencyCode) + ->first(['currency_symbol', 'currency_name']) + ->toArray(); + } + + /** + * 格式化货币显示 + */ + private function formatCurrency(float $amount, string $currencyCode): string + { + $currencySymbol = SysCurrency::where('currency_code', $currencyCode)->value('currency_symbol'); + return $currencySymbol . number_format($amount, 2, '.', ','); + } + + /** + * 获取签名图片URL + */ + private function getSignatureImageUrl(?string $signImg): string + { + if (empty($signImg)) { + return ''; + } + return (new \app\common\service\OssService)->generatePrivateUrl($signImg); + } + + /** + * 获取合同URL + */ + private function getContractUrl(?string $contract): string + { + if (empty($contract)) { + return ''; + } + return (new \app\common\service\OssService)->generatePrivateUrl($contract); + } } \ No newline at end of file diff --git a/app/common/utils/WsEvents.php b/app/common/utils/WsEvents.php index 5bf4792..b02eb90 100644 --- a/app/common/utils/WsEvents.php +++ b/app/common/utils/WsEvents.php @@ -117,7 +117,7 @@ class WsEvents // 绑定SESSION $_SESSION['uid'] = $user->uid; - $_SESSION['name'] = $user->uname; + // $_SESSION['name'] = $user->uname; // 响应 Gateway::sendToClient($client_id, WsResponse::success( @@ -138,9 +138,12 @@ class WsEvents public function logout(string $client_id) { - if ($_SESSION !== null) { - var_dump($_SESSION); - } + // var_dump('====== ====== ====== start ====== ====== ======'); + // var_dump('关闭客户端ID:'.$client_id); + // if ($_SESSION !== null) { + // var_dump($_SESSION); + // } + // var_dump('====== ====== ====== end ====== ====== ======'); } /** diff --git a/app/common/utils/approval/service/ApprovalFlowService.php b/app/common/utils/approval/service/ApprovalFlowService.php index 6eae8d8..60957f0 100644 --- a/app/common/utils/approval/service/ApprovalFlowService.php +++ b/app/common/utils/approval/service/ApprovalFlowService.php @@ -146,7 +146,8 @@ class ApprovalFlowService 'initiator_id' => $instance->initiator_id, 'status' => $action ]; - \Webman\RedisQueue\Redis::send('approval-callback', $approvalcallback); + $xx = \Webman\RedisQueue\Redis::send('approval-callback', $approvalcallback); + var_dump($xx); // 2) 聚合本步结论(any / all) $typeRaw = strtolower($step['type'] ?? 'all'); diff --git a/plugin/webman/gateway/Events.php b/plugin/webman/gateway/Events.php index 9c6573b..72848ce 100644 --- a/plugin/webman/gateway/Events.php +++ b/plugin/webman/gateway/Events.php @@ -33,7 +33,7 @@ class Events public static function onClose($client_id) { self::$wsEvents->logout($client_id); - var_dump('关闭客户端ID:'.$client_id); + // var_dump('关闭客户端ID:'.$client_id); } } \ No newline at end of file From 13ab4b7868fcf778753eaacc94bf665f0589591b Mon Sep 17 00:00:00 2001 From: mzl Date: Tue, 21 Oct 2025 19:53:21 +0800 Subject: [PATCH 020/110] =?UTF-8?q?=E6=A0=B9=E6=8D=AE10.17=E6=B2=9F?= =?UTF-8?q?=E9=80=9A=E8=B0=83=E6=95=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/StockOrdersController.php | 155 +-- .../controller/StockOrdersFilesController.php | 33 + app/common/logic/StockOrdersFilesLogic.php | 17 + app/common/model/StockOrders.php | 12 +- app/common/model/StockOrdersFiles.php | 60 ++ .../service/StockOrdersFilesService.php | 31 + app/common/service/StockOrdersService.php | 350 +++--- app/common/service/StockOrdersService_bak.php | 997 ------------------ .../approval/service/ApprovalFlowService.php | 3 +- .../utils/pdf/template/stockOrder copy.stub | 35 +- app/common/utils/pdf/template/stockOrder.stub | 84 +- .../utils/pdf/template/stockOrder_bak.stub | 255 ----- .../validate/StockOrdersFilesValidate.php | 29 + app/controller/IndexController.php | 12 +- app/crm/controller/StockOrdersController.php | 135 +-- app/crm/service/StockOrdersService.php | 1 + app/crm/service/StockOrdersService_bak.php | 444 -------- app/crm/service/StockOrdersService_bak2.php | 762 ------------- 18 files changed, 545 insertions(+), 2870 deletions(-) create mode 100644 app/common/controller/StockOrdersFilesController.php create mode 100644 app/common/logic/StockOrdersFilesLogic.php create mode 100644 app/common/model/StockOrdersFiles.php create mode 100644 app/common/service/StockOrdersFilesService.php delete mode 100644 app/common/service/StockOrdersService_bak.php delete mode 100644 app/common/utils/pdf/template/stockOrder_bak.stub create mode 100644 app/common/validate/StockOrdersFilesValidate.php delete mode 100644 app/crm/service/StockOrdersService_bak.php delete mode 100644 app/crm/service/StockOrdersService_bak2.php diff --git a/app/admin/controller/StockOrdersController.php b/app/admin/controller/StockOrdersController.php index 1d854b1..39806a5 100644 --- a/app/admin/controller/StockOrdersController.php +++ b/app/admin/controller/StockOrdersController.php @@ -56,20 +56,60 @@ class StockOrdersController extends BaseController 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['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']); + // 处理文件附件 + $fileTypes = [ + 'contract' => \app\common\model\StockOrdersFiles::CONTRACT, + 'transaction_slip' => \app\common\model\StockOrdersFiles::TRANSACTION_SLIP, + 'pi_doc' => \app\common\model\StockOrdersFiles::PI_DOC + ]; + + // 初始化文件字段 + foreach (array_keys($fileTypes) as $field) { + $data[$field] = []; + $data[$field . '_url'] = []; + } + + // 按file_code分组文件 + if (!empty($data['files'])) { + $groupedFiles = []; + foreach ($data['files'] as $file) { + if (!isset($groupedFiles[$file['file_code']])) { + $groupedFiles[$file['file_code']] = []; + } + $groupedFiles[$file['file_code']][] = $file['file_url']; + } + + // 映射到对应字段 + foreach ($fileTypes as $field => $fileCode) { + if (isset($groupedFiles[$fileCode])) { + // 原始URL数组 + $data[$field] = $groupedFiles[$fileCode]; + + // 完整URL数组 + $fullUrls = []; + foreach ($groupedFiles[$fileCode] as $url) { + $fullUrls[] = (new \app\common\service\OssService)->generatePrivateUrl($url); + } + $data[$field . '_url'] = $fullUrls; + } + } + unset($data['files']); } 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['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) { @@ -153,99 +193,4 @@ class StockOrdersController extends BaseController 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/common/controller/StockOrdersFilesController.php b/app/common/controller/StockOrdersFilesController.php new file mode 100644 index 0000000..dfb6a23 --- /dev/null +++ b/app/common/controller/StockOrdersFilesController.php @@ -0,0 +1,33 @@ +service = new StockOrdersFilesService(); + } + + /** + * 列表页数据格式化 + * + * @param array $data + * @return array + */ + // protected function indexFormat($data) + // { + // if (isset($data['created_at'])) + // $data['created_at'] = date('Y-m-d H:i:s', $data['created_at']); + // if (isset($data['updated_at'])) + // $data['updated_at'] = date('Y-m-d H:i:s', $data['updated_at']); + // return $data; + // } +} \ No newline at end of file diff --git a/app/common/logic/StockOrdersFilesLogic.php b/app/common/logic/StockOrdersFilesLogic.php new file mode 100644 index 0000000..3e6c795 --- /dev/null +++ b/app/common/logic/StockOrdersFilesLogic.php @@ -0,0 +1,17 @@ +hasMany(VehMoneyValue::class, 'object_id', 'id')->where('object_scene', 'stock_orders'); } + /** + * 关联订单文件附件 + */ + public function files() + { + return $this->hasMany(StockOrdersFiles::class, 'order_id', 'id'); + } + public function salesperson() { diff --git a/app/common/model/StockOrdersFiles.php b/app/common/model/StockOrdersFiles.php new file mode 100644 index 0000000..98daa3f --- /dev/null +++ b/app/common/model/StockOrdersFiles.php @@ -0,0 +1,60 @@ + '合同', + self::TRANSACTION_SLIP => '水单', + self::PI_DOC => 'PI', + ]; +} \ No newline at end of file diff --git a/app/common/service/StockOrdersFilesService.php b/app/common/service/StockOrdersFilesService.php new file mode 100644 index 0000000..3b8c91b --- /dev/null +++ b/app/common/service/StockOrdersFilesService.php @@ -0,0 +1,31 @@ +validator; + } +} \ No newline at end of file diff --git a/app/common/service/StockOrdersService.php b/app/common/service/StockOrdersService.php index 730fd02..f243654 100644 --- a/app/common/service/StockOrdersService.php +++ b/app/common/service/StockOrdersService.php @@ -15,6 +15,7 @@ use support\Db; use support\Model; use app\common\model\ApprovalInstances; use app\common\model\ApprovalRecords; +use app\common\model\StockOrdersFiles; // 添加文件附件模型 use support\exception\BusinessException; class StockOrdersService @@ -83,6 +84,7 @@ class StockOrdersService $config['with'][] = 'accounts'; $config['with'][] = 'moneyValues'; $config['with'][] = 'salesperson'; + $config['with'][] = 'files'; // 添加文件附件关联 break; case 'store': break; @@ -328,10 +330,16 @@ class StockOrdersService $data = $this->preProcess('Store', $data, $request); $dl = $this->getDataLimitFromRequest($request); $created = $this->logicCreate($data, $dl); + + // 添加金额 + $created->addMoney('cif_total', $data['cif_total']['currency'], $data['cif_total']['amount'], $data['cif_total']['rate'], '合计到岸价'); // 处理车辆和账户信息 $this->processOrderItems($created, $data); + // 处理文件附件 + $this->processOrderFiles($created, $data); + return $this->postProcess('Store', $created); }); } @@ -410,6 +418,8 @@ class StockOrdersService $order = $this->logicShow($config); // 处理车辆和账户信息 $this->processOrderItems($order, $data, true); + // 处理文件附件 + $this->processOrderFiles($order, $data, true); return $this->postProcess('Update', $order); }); } @@ -434,6 +444,62 @@ class StockOrdersService $this->processAccountItems($order, $data['account_list'] ?? []); } + /** + * 处理订单文件附件 + * @param StockOrders $order 订单对象 + * @param array $data 数据 + * @param bool $isUpdate 是否为更新操作 + */ + private function processOrderFiles($order, array $data, bool $isUpdate = false): void + { + // 如果是更新操作,先删除原有的文件附件 + if ($isUpdate) { + $this->clearExistingOrderFiles($order->id); + } + + // 处理文件附件项 + $this->processFileItems($order, $data); + } + + /** + * (更新时)清除已存在的订单文件附件 + * @param int $orderId 订单ID + */ + private function clearExistingOrderFiles(int $orderId): void + { + StockOrdersFiles::where('order_id', $orderId)->delete(); + } + + /** + * 处理文件附件项 + */ + private function processFileItems($order, array $data): void + { + // 定义支持的文件类型 + $fileTypes = [ + 'contract' => StockOrdersFiles::CONTRACT, + 'transaction_slip' => StockOrdersFiles::TRANSACTION_SLIP, + 'pi_doc' => StockOrdersFiles::PI_DOC + ]; + + // 遍历每种文件类型 + foreach ($fileTypes as $field => $fileCode) { + // 检查数据中是否存在该字段且为数组 + if (!empty($data[$field]) && is_array($data[$field])) { + // 处理该字段下的每个文件 + foreach ($data[$field] as $fileUrl) { + $fileData = [ + 'order_id' => $order->id, + 'file_code' => $fileCode, + 'file_url' => $fileUrl, + 'file_name' => StockOrdersFiles::FILE_CODE[$fileCode], + ]; + StockOrdersFiles::create($fileData); + } + } + } + } + /** * (更新时)清除已存在的订单项 * @param int $orderId 订单ID @@ -473,8 +539,9 @@ class StockOrdersService */ private function processVehicleItems($order, array $vehList): void { - $cifTotalAmount = 0.00; $profitTotalAmount = 0.00; + $profitCurrency = ''; + $profitRate = 0; foreach ($vehList as $veh) { $veh['order_id'] = $order->id; $veh['unique_id'] = $this->generateVehicleUniqueId($veh); @@ -483,23 +550,18 @@ class StockOrdersService // 处理金额信息 $this->processVehiclePrices($item, $veh); - - // 计算利润 - // $profit = $this->calculateProfit($veh, $item->mileage, $order); - // $item->addMoney('profit', $order->settlement_currency, $profit, $order->contract_exchange_rate, '利润'); // 累计到岸价 - $cifTotalAmount += $veh['total_price']['amount'] ?? 0; + // $cifTotalAmount += $veh['total_price']['amount'] ?? 0; + $profitCurrency === '' ? $profitCurrency = $veh['profit']['currency'] ?? '' : ''; + $profitRate === 0 ? $profitRate = $veh['profit']['rate'] ?? 0 : ''; $profitTotalAmount += $veh['profit']['amount'] ?? 0; // 处理改装项 $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, '总利润'); + $order->addMoney('profit_total', $profitCurrency, $profitTotalAmount, $profitRate, '总利润'); } /** @@ -507,13 +569,9 @@ class StockOrdersService */ 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'] ?? []); + $keysToRemove = ['vin', 'remark', 'modify_list', 'order_id']; + $array = array_diff_key($veh, array_flip($keysToRemove)); + $uniqueIdStr = json_encode($array); return md5($uniqueIdStr); } @@ -523,9 +581,29 @@ class StockOrdersService */ private function processVehiclePrices($item, array $veh): void { + // unit_price 单价/成交价/收款价 + // total_price 总价 + // handling_fee 手续费 + // fob_transport_fee FOB运输费 + // port_misc_fee 港杂 + // retrofit_fee 改装费 + // ocean_freight 海运费 + // commission 佣金 + // purchase_tax 购置税 + // tax_refund 预估退税 + // clearance_price 报关价 + // profit 利润 + // official_price 官方指导价 + // discount_price 优惠价 + // listing_price 二手挂牌价 + // reduction_price 二手减价/优惠价 + // retention_price 留款/挂账 + // car_price 车价 + // accessory_price 配件费 + // 基础费用 $priceFields = [ - 'unit_price' => '单价', + 'unit_price' => '收款价', 'total_price' => '总价', 'handling_fee' => '手续费', 'fob_transport_fee' => 'FOB运输费', @@ -536,7 +614,10 @@ class StockOrdersService 'purchase_tax' => '购置税', 'tax_refund' => '预估退税', 'clearance_price' => '报关价', - 'profit' => '利润' + 'profit' => '利润', + 'retention_price' => '挂账', + 'car_price' => '车价', + 'accessory_price' => '配件费' ]; foreach ($priceFields as $field => $description) { @@ -564,7 +645,7 @@ class StockOrdersService $veh['reduction_price']['currency'], (float)$veh['reduction_price']['amount'], $veh['reduction_price']['rate'], - '二手减价' + '二手优惠价' ); } else { // 新车 @@ -585,111 +666,6 @@ class StockOrdersService } } - /** - * 计算利润 - */ - private function calculateProfit(array $veh, int $mileage, $order): float - { - $contractExchangeRate = $order->contract_exchange_rate ?: 1; - - // 提取单价并进行汇率转换(如果需要) - $unitPriceRate = $veh['unit_price']['rate'] ?: 1; - if ($unitPriceRate != $contractExchangeRate) { - $unitPrice = (float)($veh['unit_price']['amount'] ?? 0) / $contractExchangeRate; - } else { - $unitPrice = (float)($veh['unit_price']['amount'] ?? 0); - } - - // 处理主要价格字段,根据是否需要汇率转换来计算 - if ($mileage > 0) { - // 二手车 - $listingPriceRate = $veh['listing_price']['rate'] ?: 1; - if ($listingPriceRate != $contractExchangeRate) { - $ttPrice = ($veh['listing_price']['amount'] ?? 0) / $contractExchangeRate; - } else { - $ttPrice = $veh['listing_price']['amount'] ?? 0; - } - - $reductionPriceRate = $veh['reduction_price']['rate'] ?: 1; - if ($reductionPriceRate != $contractExchangeRate) { - $ddPrice = ($veh['reduction_price']['amount'] ?? 0) / $contractExchangeRate; - } else { - $ddPrice = $veh['reduction_price']['amount'] ?? 0; - } - } else { - // 新车 - $officialPriceRate = $veh['official_price']['rate'] ?: 1; - if ($officialPriceRate != $contractExchangeRate) { - $ttPrice = ($veh['official_price']['amount'] ?? 0) / $contractExchangeRate; - } else { - $ttPrice = $veh['official_price']['amount'] ?? 0; - } - - $discountPriceRate = $veh['discount_price']['rate'] ?: 1; - if ($discountPriceRate != $contractExchangeRate) { - $ddPrice = ($veh['discount_price']['amount'] ?? 0) / $contractExchangeRate; - } else { - $ddPrice = $veh['discount_price']['amount'] ?? 0; - } - } - - // 处理其他费用字段,根据是否需要汇率转换来计算 - $handlingFeeRate = $veh['handling_fee']['rate'] ?: 1; - if ($handlingFeeRate != $contractExchangeRate) { - $handlingFee = (float)($veh['handling_fee']['amount'] ?? 0) / $contractExchangeRate; - } else { - $handlingFee = (float)($veh['handling_fee']['amount'] ?? 0); - } - - $fobTransportFeeRate = $veh['fob_transport_fee']['rate'] ?: 1; - if ($fobTransportFeeRate != $contractExchangeRate) { - $fobTransportFee = (float)($veh['fob_transport_fee']['amount'] ?? 0) / $contractExchangeRate; - } else { - $fobTransportFee = (float)($veh['fob_transport_fee']['amount'] ?? 0); - } - - $portMiscFeeRate = $veh['port_misc_fee']['rate'] ?: 1; - if ($portMiscFeeRate != $contractExchangeRate) { - $portMiscFee = (float)($veh['port_misc_fee']['amount'] ?? 0) / $contractExchangeRate; - } else { - $portMiscFee = (float)($veh['port_misc_fee']['amount'] ?? 0); - } - - $retrofitFeeRate = $veh['retrofit_fee']['rate'] ?: 1; - if ($retrofitFeeRate != $contractExchangeRate) { - $retrofitFee = (float)($veh['retrofit_fee']['amount'] ?? 0) / $contractExchangeRate; - } else { - $retrofitFee = (float)($veh['retrofit_fee']['amount'] ?? 0); - } - - $oceanFreightRate = $veh['ocean_freight']['rate'] ?: 1; - if ($oceanFreightRate != $contractExchangeRate) { - $oceanFreight = (float)($veh['ocean_freight']['amount'] ?? 0) / $contractExchangeRate; - } else { - $oceanFreight = (float)($veh['ocean_freight']['amount'] ?? 0); - } - - $commissionRate = $veh['commission']['rate'] ?: 1; - if ($commissionRate != $contractExchangeRate) { - $commission = (float)($veh['commission']['amount'] ?? 0) / $contractExchangeRate; - } else { - $commission = (float)($veh['commission']['amount'] ?? 0); - } - - $purchaseTaxRate = $veh['purchase_tax']['rate'] ?: 1; - if ($purchaseTaxRate != $contractExchangeRate) { - $purchaseTax = (float)($veh['purchase_tax']['amount'] ?? 0) / $contractExchangeRate; - } else { - $purchaseTax = (float)($veh['purchase_tax']['amount'] ?? 0); - } - - // 预估退税不需要参与汇率转换计算,前端已经计算过了 - $taxRefund = (float)($veh['tax_refund']['amount'] ?? 0); - - // 退税额 是增加利润 - return $unitPrice - $ttPrice + $ddPrice - $handlingFee - $fobTransportFee - $portMiscFee - $retrofitFee - $oceanFreight - $commission - $purchaseTax + $taxRefund; - } - /** * 处理改装项 */ @@ -738,6 +714,9 @@ class StockOrdersService // 提取账户列表信息 $accountList = $this->prepareAccountList($item); + // 提取文件附件列表信息 + $fileList = $this->prepareFileList($item); + // 构建返回数据 $veh = [ // 基本信息 @@ -756,8 +735,8 @@ class StockOrdersService // 订单项 'veh_list' => array_values($vehListData['list']), 'contract_exchange_rate' => $item->contract_exchange_rate, - 'cif_total_amount' => $moneyValues['cif_total_amount'] ?? [], 'profit_total' => $moneyValues['profit_total'] ?? [], + 'cif_total' => $moneyValues['cif_total'] ?? [], // 运输和交付信息 'declarant_name' => $item->declarant_name, @@ -776,7 +755,10 @@ class StockOrdersService 'acc_index' => count($accountList) + 1, 'currency_symbol' => $item->settlement_currency, 'sign_imgs' => $this->queryAppro($item->id), - 'contract_url' => $this->getContractUrl($item->contract ?? null) + 'contract_url' => $this->getContractUrl($item->contract ?? null), + + // 文件附件信息 + 'file_list' => $fileList ]; return $veh; @@ -798,7 +780,7 @@ class StockOrdersService // 拼接 remark 值 if (!empty($v->remark)) { if (!empty($vehList[$v->unique_id]['remark'])) { - $vehList[$v->unique_id]['remark'] .= "; " . $v->remark; + $vehList[$v->unique_id]['remark'] .= "
" . $v->remark; } else { $vehList[$v->unique_id]['remark'] = $v->remark; } @@ -811,12 +793,10 @@ class StockOrdersService $moneyValues = $this->processItemMoneyValues($v); // 处理改装信息 - $modificationText = $this->getModificationText($v->options); + // $modificationText = $this->getModificationText($v->options); - // 计算单项利润 - // $itemProfit = $this->calculateProfit($moneyValues, $v->mileage, $item); - // var_dump($moneyValues); - $itemProfit = empty($moneyValues['profit']['amount']) ? 0 : $moneyValues['profit']['amount']; + // 利润 + // $itemProfit = empty($moneyValues['profit']['amount']) ? 0 : $moneyValues['profit']['amount']; // 构建车辆信息 $vehList[$v->unique_id] = [ @@ -825,17 +805,17 @@ class StockOrdersService 'color_interior_snapshot' => $v->color_interior_snapshot, 'color_exterior_snapshot' => $v->color_exterior_snapshot, 'num' => 1, - 'modify' => $modificationText, + // 'modify' => $modificationText, 'mileage' => $v->mileage, 'remark' => $v->remark, ] + $moneyValues; - + // var_dump($vehList[$v->unique_id]); // 添加利润信息 - $vehList[$v->unique_id]['profit'] = [ - 'currency' => $item->settlement_currency, - 'amount' => $itemProfit, - 'currency_info' => $currencyInfo['currency_symbol'] . number_format($itemProfit, 2, '.', ',') - ]; + // $vehList[$v->unique_id]['profit'] = [ + // 'currency' => $item->settlement_currency, + // 'amount' => $itemProfit, + // 'currency_info' => $currencyInfo['currency_symbol'] . number_format($itemProfit, 2, '.', ',') + // ]; } return ['list' => $vehList]; @@ -877,16 +857,38 @@ class StockOrdersService { $vehicleData['num'] += 1; - // 更新总价显示 - if (isset($vehicleData['total_price']['amount'])) { - $totalAmount = $vehicleData['total_price']['amount'] * $vehicleData['num']; - $vehicleData['total_price']['currency_info'] = $currencyInfo['currency_symbol'] . number_format($totalAmount, 2, '.', ','); - } + // // 更新总价显示 + // if (isset($vehicleData['total_price']['amount'])) { + // $totalAmount = $vehicleData['total_price']['amount'] * $vehicleData['num']; + // $vehicleData['total_price']['currency_info'] = $currencyInfo['currency_symbol'] . number_format($totalAmount, 2, '.', ','); + // } - // 更新利润显示 - if (isset($vehicleData['profit']['amount'])) { - $profitAmount = $vehicleData['profit']['amount'] * $vehicleData['num']; - $vehicleData['profit']['currency_info'] = $currencyInfo['currency_symbol'] . number_format($profitAmount, 2, '.', ','); + // // 更新利润显示 + // if (isset($vehicleData['profit']['amount'])) { + // $profitAmount = $vehicleData['profit']['amount'] * $vehicleData['num']; + // $vehicleData['profit']['currency_info'] = $currencyInfo['currency_symbol'] . number_format($profitAmount, 2, '.', ','); + // } + + // var_dump($vehicleData); + + $fieldList = [ + 'fob_transport_fee', + 'car_price', + 'ocean_freight', + 'commission', + 'retention_price', + 'port_misc_fee', + 'total_price', + 'clearance_price', + 'profit' + ]; + + foreach ($vehicleData as $field => $value) { + if (in_array($field, $fieldList)) { + $amount = $vehicleData[$field]['amount'] * $vehicleData['num']; + $currencySymbol = SysCurrency::where('currency_code', $vehicleData[$field]['currency'])->value('currency_symbol'); + $vehicleData[$field]['currency_info'] = $currencySymbol . number_format($amount, 2, '.', ','); + } } } @@ -927,6 +929,45 @@ class StockOrdersService return $accountList; } + /** + * 准备文件附件列表 + */ + private function prepareFileList($item): array + { + $fileList = []; + + // 按file_code分组文件 + $groupedFiles = []; + foreach ($item->files as $file) { + if (!isset($groupedFiles[$file->file_code])) { + $groupedFiles[$file->file_code] = []; + } + $groupedFiles[$file->file_code][] = [ + 'id' => $file->id, + 'file_url' => $file->file_url, + 'file_full_url' => $this->getFileUrl($file->file_url), + ]; + } + + // 定义支持的文件类型 + $fileTypes = [ + 'contract' => StockOrdersFiles::CONTRACT, + 'transaction_slip' => StockOrdersFiles::TRANSACTION_SLIP, + 'pi_doc' => StockOrdersFiles::PI_DOC + ]; + + // 按前端需要的字段组织数据 + foreach ($fileTypes as $field => $fileCode) { + if (isset($groupedFiles[$fileCode])) { + $fileList[$field] = $groupedFiles[$fileCode]; + } else { + $fileList[$field] = []; + } + } + + return $fileList; + } + /** * 获取货币信息 */ @@ -967,4 +1008,15 @@ class StockOrdersService } return (new \app\common\service\OssService)->generatePrivateUrl($contract); } + + /** + * 获取文件URL + */ + private function getFileUrl(?string $fileUrl): string + { + if (empty($fileUrl)) { + return ''; + } + return (new \app\common\service\OssService)->generatePrivateUrl($fileUrl); + } } \ No newline at end of file diff --git a/app/common/service/StockOrdersService_bak.php b/app/common/service/StockOrdersService_bak.php deleted file mode 100644 index 5ee03a9..0000000 --- a/app/common/service/StockOrdersService_bak.php +++ /dev/null @@ -1,997 +0,0 @@ -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; - // } - - if ($data['is_final'] == 1) { - // 检查是否至少有一个车辆填写了vin - $hasVin = false; - foreach ($data['veh_list'] as $k => $v) { - if ($v['vin'] != '') { - $hasVin = true; - break; - } - } - // 如果没有任何车辆填写vin,则抛出异常 - if (!$hasVin) { - throw new BusinessException('至少需要为一辆车辆填写vin,才能提交审核'); - } - } - - // 检查绑定 - (new UserService())->checkBind($userId); - return $data; - } - - protected function afterServiceStore($res) - { - // 处理原单 - if ($res->is_final == 1 && $res->object_id > 0) { - $order = StockOrders::find($res->object_id); - $order->is_use = 1; - $order->object_id = $res->id; - if (!$order->save()) { - flg('备货单【'.$order->order_no.'】创建终版成功,但更新原单失败'); - } - } - // 启动审批 - if ($res->approval_status == 1) { - $this->startApproval($res); - } - return $res; - } - - /** - * 启动审批 - */ - public function startApproval($item) - { - // 启动审批 - $appro = new \app\common\utils\approval\service\ApprovalFlowService(); - $appro->start('stock_orders', 'stock_orders', $item->id, [], $item->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->formatData($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 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); - $detail = $this->formatData($item); - return $this->postProcess('Detail', $detail); - } - - /** - * 审批 - */ - public function serviceApproval(Request $request) - { - $config = $this->buildQueryConfig($request, 'approval'); - $this->logicUpdateBy($config, [ - 'approval_status' => 1 - ]); - $item = $this->logicShow($config); - $this->startApproval($item); - return $this->postProcess('Approval', $item); - } - - /** - * 作废审批 - */ - public function serviceApprovalNullify(Request $request) - { - $config = $this->buildQueryConfig($request, 'approval'); - $item = $this->logicShow($config); - $this->approvalNullify($item); - return $this->postProcess('Approval', $item); - } - - /** - * 审批作废 - */ - public function approvalNullify($item) - { - $appro = new \app\common\utils\approval\service\ApprovalFlowService(); - $appro->startNullify('stock_orders_nullify', 'stock_orders', $item->id, [], $item->salesperson_id); - } - - /** - * 撤销审批 - */ - public function serviceApprovalRevoke(Request $request) - { - $config = $this->buildQueryConfig($request, 'approval'); - $item = $this->logicShow($config); - $this->approvalRevoke($item); - return $this->postProcess('Approval', $item); - } - - public function approvalRevoke($item) - { - $instance = \app\common\model\ApprovalInstances::where('biz_id', $item->id)->where('flow_type', 'stock_orders')->first(); - $appro = new \app\common\utils\approval\service\ApprovalFlowService(); - $appro->revoke($instance->id, $item->salesperson_id); - } - - /** - * 创建 - * - * @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; - $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, $item->mileage, $order); - // $item->addMoney('profit', $order->settlement_currency, $profit, $order->contract_exchange_rate, '利润'); - - // 累计到岸价 - $cifTotalAmount += $veh['total_price']['amount'] ?? 0; - $profitTotalAmount += $veh['profit']['amount'] ?? 0; - - // 处理改装项 - $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'] ?? []); - - $keysToRemove = ['vin', 'remark', 'modify_list', 'order_id']; - $array = array_diff_key($veh, array_flip($keysToRemove)); - $uniqueIdStr = json_encode($array); - - return md5($uniqueIdStr); - } - - /** - * 处理车辆价格信息 - */ - private function processVehiclePrices($item, array $veh): void - { - // unit_price 单价/成交价/收款价 - // total_price 总价 - // handling_fee 手续费 - // fob_transport_fee FOB运输费 - // port_misc_fee 港杂 - // retrofit_fee 改装费 - // ocean_freight 海运费 - // commission 佣金 - // purchase_tax 购置税 - // tax_refund 预估退税 - // clearance_price 报关价 - // profit 利润 - // official_price 官方指导价 - // discount_price 优惠价 - // listing_price 二手挂牌价 - // reduction_price 二手减价/优惠价 - // retention_price 留款/挂账 - // car_price 车价 - // modify_price 改装价 - - // 基础费用 - $priceFields = [ - 'unit_price' => '收款价', - 'total_price' => '总价', - 'handling_fee' => '手续费', - 'fob_transport_fee' => 'FOB运输费', - 'port_misc_fee' => '港杂', - 'retrofit_fee' => '改装费', - 'ocean_freight' => '海运费', - 'commission' => '佣金', - 'purchase_tax' => '购置税', - 'tax_refund' => '预估退税', - 'clearance_price' => '报关价', - 'profit' => '利润', - 'retention_price' => '挂账', - 'car_price' => '车价', - 'modify_price' => '改装价' - ]; - - foreach ($priceFields as $field => $description) { - $item->addMoney( - $field, - $veh[$field]['currency'], - (float)$veh[$field]['amount'], - $veh[$field]['rate'], - $description - ); - } - - // 根据里程处理新车或二手车价格 - if (!empty($veh['mileage']) && $veh['mileage'] > 0) { - // 二手车 - $item->addMoney( - 'listing_price', - $veh['listing_price']['currency'], - (float)$veh['listing_price']['amount'], - $veh['listing_price']['rate'], - '二手挂牌价' - ); - $item->addMoney( - 'reduction_price', - $veh['reduction_price']['currency'], - (float)$veh['reduction_price']['amount'], - $veh['reduction_price']['rate'], - '优惠价' - ); - } else { - // 新车 - $item->addMoney( - 'official_price', - $veh['official_price']['currency'], - (float)$veh['official_price']['amount'], - $veh['official_price']['rate'], - '官方指导价' - ); - $item->addMoney( - 'discount_price', - $veh['discount_price']['currency'], - (float)$veh['discount_price']['amount'], - $veh['discount_price']['rate'], - '优惠价' - ); - } - } - - /** - * 计算利润 - */ - private function calculateProfit(array $veh, int $mileage, $order): float - { - $contractExchangeRate = $order->contract_exchange_rate ?: 1; - - // 提取单价并进行汇率转换(如果需要) - $unitPriceRate = $veh['unit_price']['rate'] ?: 1; - if ($unitPriceRate != $contractExchangeRate) { - $unitPrice = (float)($veh['unit_price']['amount'] ?? 0) / $contractExchangeRate; - } else { - $unitPrice = (float)($veh['unit_price']['amount'] ?? 0); - } - - // 处理主要价格字段,根据是否需要汇率转换来计算 - if ($mileage > 0) { - // 二手车 - $listingPriceRate = $veh['listing_price']['rate'] ?: 1; - if ($listingPriceRate != $contractExchangeRate) { - $ttPrice = ($veh['listing_price']['amount'] ?? 0) / $contractExchangeRate; - } else { - $ttPrice = $veh['listing_price']['amount'] ?? 0; - } - - $reductionPriceRate = $veh['reduction_price']['rate'] ?: 1; - if ($reductionPriceRate != $contractExchangeRate) { - $ddPrice = ($veh['reduction_price']['amount'] ?? 0) / $contractExchangeRate; - } else { - $ddPrice = $veh['reduction_price']['amount'] ?? 0; - } - } else { - // 新车 - $officialPriceRate = $veh['official_price']['rate'] ?: 1; - if ($officialPriceRate != $contractExchangeRate) { - $ttPrice = ($veh['official_price']['amount'] ?? 0) / $contractExchangeRate; - } else { - $ttPrice = $veh['official_price']['amount'] ?? 0; - } - - $discountPriceRate = $veh['discount_price']['rate'] ?: 1; - if ($discountPriceRate != $contractExchangeRate) { - $ddPrice = ($veh['discount_price']['amount'] ?? 0) / $contractExchangeRate; - } else { - $ddPrice = $veh['discount_price']['amount'] ?? 0; - } - } - - // 处理其他费用字段,根据是否需要汇率转换来计算 - $handlingFeeRate = $veh['handling_fee']['rate'] ?: 1; - if ($handlingFeeRate != $contractExchangeRate) { - $handlingFee = (float)($veh['handling_fee']['amount'] ?? 0) / $contractExchangeRate; - } else { - $handlingFee = (float)($veh['handling_fee']['amount'] ?? 0); - } - - $fobTransportFeeRate = $veh['fob_transport_fee']['rate'] ?: 1; - if ($fobTransportFeeRate != $contractExchangeRate) { - $fobTransportFee = (float)($veh['fob_transport_fee']['amount'] ?? 0) / $contractExchangeRate; - } else { - $fobTransportFee = (float)($veh['fob_transport_fee']['amount'] ?? 0); - } - - $portMiscFeeRate = $veh['port_misc_fee']['rate'] ?: 1; - if ($portMiscFeeRate != $contractExchangeRate) { - $portMiscFee = (float)($veh['port_misc_fee']['amount'] ?? 0) / $contractExchangeRate; - } else { - $portMiscFee = (float)($veh['port_misc_fee']['amount'] ?? 0); - } - - $retrofitFeeRate = $veh['retrofit_fee']['rate'] ?: 1; - if ($retrofitFeeRate != $contractExchangeRate) { - $retrofitFee = (float)($veh['retrofit_fee']['amount'] ?? 0) / $contractExchangeRate; - } else { - $retrofitFee = (float)($veh['retrofit_fee']['amount'] ?? 0); - } - - $oceanFreightRate = $veh['ocean_freight']['rate'] ?: 1; - if ($oceanFreightRate != $contractExchangeRate) { - $oceanFreight = (float)($veh['ocean_freight']['amount'] ?? 0) / $contractExchangeRate; - } else { - $oceanFreight = (float)($veh['ocean_freight']['amount'] ?? 0); - } - - $commissionRate = $veh['commission']['rate'] ?: 1; - if ($commissionRate != $contractExchangeRate) { - $commission = (float)($veh['commission']['amount'] ?? 0) / $contractExchangeRate; - } else { - $commission = (float)($veh['commission']['amount'] ?? 0); - } - - $purchaseTaxRate = $veh['purchase_tax']['rate'] ?: 1; - if ($purchaseTaxRate != $contractExchangeRate) { - $purchaseTax = (float)($veh['purchase_tax']['amount'] ?? 0) / $contractExchangeRate; - } else { - $purchaseTax = (float)($veh['purchase_tax']['amount'] ?? 0); - } - - // 预估退税不需要参与汇率转换计算,前端已经计算过了 - $taxRefund = (float)($veh['tax_refund']['amount'] ?? 0); - - // 退税额 是增加利润 - return $unitPrice - $ttPrice + $ddPrice - $handlingFee - $fobTransportFee - $portMiscFee - $retrofitFee - $oceanFreight - $commission - $purchaseTax + $taxRefund; - } - - /** - * 处理改装项 - */ - 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); - } - } - - - // ====== ======= ====== ======= ====== 信息处理 ======= ====== ======= ====== ======= - - // PDF数据 - public function formatData($item) - { - // 提取车辆列表信息 - $vehListData = $this->prepareVehicleListData($item); - - // 提取订单金额信息 - $moneyValues = $this->prepareOrderMoneyValues($item); - - // 提取账户列表信息 - $accountList = $this->prepareAccountList($item); - - // 构建返回数据 - $veh = [ - // 基本信息 - 'order_no' => $item->order_no, - 'salesperson_snapshot' => $item->salesperson_snapshot, - 'sign_img_url' => $this->getSignatureImageUrl($item->salesperson->sign_img ?? null), - '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($vehListData['list']), - 'contract_exchange_rate' => $item->contract_exchange_rate, - 'cif_total_amount' => $moneyValues['cif_total_amount'] ?? [], - 'profit_total' => $moneyValues['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' => $accountList, - 'acc_index' => count($accountList) + 1, - 'currency_symbol' => $item->settlement_currency, - 'sign_imgs' => $this->queryAppro($item->id), - 'contract_url' => $this->getContractUrl($item->contract ?? null) - ]; - - return $veh; - } - - /** - * 准备车辆列表数据 - */ - private function prepareVehicleListData($item): array - { - $vehList = []; - $index = 0; - $currencyInfo = $this->getCurrencyInfo($item->settlement_currency); - - foreach ($item->items as $v) { - // 如果已存在相同车辆,增加数量 - if (isset($vehList[$v->unique_id])) { - $this->incrementVehicleCount($vehList[$v->unique_id], $currencyInfo); - // 拼接 remark 值 - if (!empty($v->remark)) { - if (!empty($vehList[$v->unique_id]['remark'])) { - $vehList[$v->unique_id]['remark'] .= "; " . $v->remark; - } else { - $vehList[$v->unique_id]['remark'] = $v->remark; - } - } - continue; - } - - $index++; - // 处理金额信息 - $moneyValues = $this->processItemMoneyValues($v); - - // 处理改装信息 - $modificationText = $this->getModificationText($v->options); - - // 计算单项利润 - // $itemProfit = $this->calculateProfit($moneyValues, $v->mileage, $item); - // var_dump($moneyValues); - $itemProfit = empty($moneyValues['profit']['amount']) ? 0 : $moneyValues['profit']['amount']; - - // 构建车辆信息 - $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' => $modificationText, - 'mileage' => $v->mileage, - 'remark' => $v->remark, - ] + $moneyValues; - - // 添加利润信息 - $vehList[$v->unique_id]['profit'] = [ - 'currency' => $item->settlement_currency, - 'amount' => $itemProfit, - 'currency_info' => $currencyInfo['currency_symbol'] . number_format($itemProfit, 2, '.', ',') - ]; - } - - return ['list' => $vehList]; - } - - /** - * 处理单项金额值 - */ - private function processItemMoneyValues($item): array - { - $moneyValues = []; - foreach ($item->moneyValues as $money) { - $moneyValues[$money->field] = [ - 'currency' => $money->currency, - 'amount' => $money->amount, - 'rate' => $money->exchange_rate, - 'currency_info' => $this->formatCurrency($money->amount, $money->currency) - ]; - } - return $moneyValues; - } - - /** - * 获取改装文本 - */ - private function getModificationText($options): string - { - $modificationText = ''; - foreach ($options as $option) { - $modificationText .= $option->option_name_snapshot . ' '; - } - return trim($modificationText); - } - - /** - * 增加车辆数量并更新相关金额显示 - */ - private function incrementVehicleCount(array &$vehicleData, array $currencyInfo): void - { - $vehicleData['num'] += 1; - - // 更新总价显示 - if (isset($vehicleData['total_price']['amount'])) { - $totalAmount = $vehicleData['total_price']['amount'] * $vehicleData['num']; - $vehicleData['total_price']['currency_info'] = $currencyInfo['currency_symbol'] . number_format($totalAmount, 2, '.', ','); - } - - // 更新利润显示 - if (isset($vehicleData['profit']['amount'])) { - $profitAmount = $vehicleData['profit']['amount'] * $vehicleData['num']; - $vehicleData['profit']['currency_info'] = $currencyInfo['currency_symbol'] . number_format($profitAmount, 2, '.', ','); - } - } - - /** - * 准备订单金额值 - */ - private function prepareOrderMoneyValues($item): array - { - $moneyValues = []; - foreach ($item->moneyValues as $money) { - $moneyValues[$money->field] = [ - 'currency' => $money->currency, - 'amount' => $money->amount, - 'rate' => $money->exchange_rate, - 'currency_info' => $this->formatCurrency($money->amount, $money->currency) - ]; - } - return $moneyValues; - } - - /** - * 准备账户列表 - */ - private function prepareAccountList($item): array - { - $accountList = []; - foreach ($item->accounts as $k => $account) { - $accountList[] = [ - 'index' => '收款' . ($k + 1), - 'remit_country_name_snapshot' => $account->remit_country_name_snapshot, - 'remit_date' => date('Y-m-d', $account->remit_date), - 'currency_name_snapshot' => $account->currency_name_snapshot, - 'account_name_snapshot' => $account->account_name_snapshot, - 'remitter_name' => $account->remitter_name, - 'amount' => $account->amount, - ]; - } - return $accountList; - } - - /** - * 获取货币信息 - */ - private function getCurrencyInfo(string $currencyCode): array - { - return SysCurrency::where('currency_code', $currencyCode) - ->first(['currency_symbol', 'currency_name']) - ->toArray(); - } - - /** - * 格式化货币显示 - */ - private function formatCurrency(float $amount, string $currencyCode): string - { - $currencySymbol = SysCurrency::where('currency_code', $currencyCode)->value('currency_symbol'); - return $currencySymbol . number_format($amount, 2, '.', ','); - } - - /** - * 获取签名图片URL - */ - private function getSignatureImageUrl(?string $signImg): string - { - if (empty($signImg)) { - return ''; - } - return (new \app\common\service\OssService)->generatePrivateUrl($signImg); - } - - /** - * 获取合同URL - */ - private function getContractUrl(?string $contract): string - { - if (empty($contract)) { - return ''; - } - return (new \app\common\service\OssService)->generatePrivateUrl($contract); - } -} \ No newline at end of file diff --git a/app/common/utils/approval/service/ApprovalFlowService.php b/app/common/utils/approval/service/ApprovalFlowService.php index 60957f0..6eae8d8 100644 --- a/app/common/utils/approval/service/ApprovalFlowService.php +++ b/app/common/utils/approval/service/ApprovalFlowService.php @@ -146,8 +146,7 @@ class ApprovalFlowService 'initiator_id' => $instance->initiator_id, 'status' => $action ]; - $xx = \Webman\RedisQueue\Redis::send('approval-callback', $approvalcallback); - var_dump($xx); + \Webman\RedisQueue\Redis::send('approval-callback', $approvalcallback); // 2) 聚合本步结论(any / all) $typeRaw = strtolower($step['type'] ?? 'all'); diff --git a/app/common/utils/pdf/template/stockOrder copy.stub b/app/common/utils/pdf/template/stockOrder copy.stub index 8a2dbf5..43f887f 100644 --- a/app/common/utils/pdf/template/stockOrder copy.stub +++ b/app/common/utils/pdf/template/stockOrder copy.stub @@ -110,7 +110,7 @@ 阿里订单号/PI号: 日期: - 关目的国: + 清关目的国: 客户来源: 客户名称: 客户所在国: @@ -144,6 +144,7 @@ 优惠 单价 总价 + 报关价 利润 @@ -163,16 +164,22 @@ 0): ?> - - + + - - + + - - - + + + + + + + + 备注: + @@ -180,8 +187,8 @@ 合同汇率: - - 合计到岸价: + + 合计到岸价: @@ -275,8 +282,8 @@
备发货负责人签字: - - + + @@ -287,8 +294,8 @@
组长签字: - - + + diff --git a/app/common/utils/pdf/template/stockOrder.stub b/app/common/utils/pdf/template/stockOrder.stub index 43f887f..20b10b3 100644 --- a/app/common/utils/pdf/template/stockOrder.stub +++ b/app/common/utils/pdf/template/stockOrder.stub @@ -120,49 +120,40 @@ - - - - - - - - - - - - - - - - - - - - + + + + + + - - + - + - - + + + 0): ?> @@ -170,28 +161,47 @@ - - - - - + + + + + + + + + + + - + + + + + + + + + + - + + - - - +
编号 车型 数量/台 颜色/外内饰改装官方指导价/二手挂牌价优惠单价总价报关价利润颜色/内收款价总价官方指导价/二手挂牌价优惠利润
+ + + 0): ?> () + +
改装费FOB运费车价海运费佣金挂账港杂
- 备注: +
+ 备注: + +
+ 合同汇率: - 合计到岸价: + + 合计到岸价:
diff --git a/app/common/utils/pdf/template/stockOrder_bak.stub b/app/common/utils/pdf/template/stockOrder_bak.stub deleted file mode 100644 index 6b93ecc..0000000 --- a/app/common/utils/pdf/template/stockOrder_bak.stub +++ /dev/null @@ -1,255 +0,0 @@ - - - - - 合塑实业-汽车事业部-备货单(客户下定) - - - -
- -
-

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

-
- 备货单号: - 业务员: -
-
- - - - - - - - - - - - - - - - - - - - - -
阿里订单号/PI号: 日期: 关目的国: 客户来源: 客户名称: 客户所在国:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 0): ?> - - - - - - - - - - - - - - - -
编号车型数量/台颜色/外内饰改装单价总价官方指导价/二手挂牌价优惠/二手减价
- 合同汇率: - - 合计到岸价: -
- - - - - - - - - - - - - - - - - - - - - -
清关人: 预计发运日期: 出运方式: 国内收货港: 贸易条款/清关目的港: /最迟到港时间:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
计划收款信息序号汇款国日期币种账户汇款人金额
- -
-
-
- 下单人: - -
-
-
下单时间:
-
- -
- - \ No newline at end of file diff --git a/app/common/validate/StockOrdersFilesValidate.php b/app/common/validate/StockOrdersFilesValidate.php new file mode 100644 index 0000000..c7fca82 --- /dev/null +++ b/app/common/validate/StockOrdersFilesValidate.php @@ -0,0 +1,29 @@ + [ + + ], + + 'update' => [ + + ] + ]; +} \ No newline at end of file diff --git a/app/controller/IndexController.php b/app/controller/IndexController.php index 10969ac..72532c7 100644 --- a/app/controller/IndexController.php +++ b/app/controller/IndexController.php @@ -74,25 +74,25 @@ class IndexController $options = [ 'controller' => [ 'module' => 'common', - 'name' => 'user_group_members' + 'name' => 'stock_orders_files' ], 'model' => [ 'module' => 'common', - 'name' => 'user_group_members' + 'name' => 'stock_orders_files' ], 'service' => [ 'module' => 'common', - 'name' => 'user_group_members' + 'name' => 'stock_orders_files' ], 'logic' => [ 'module' => 'common', - 'name' => 'user_group_members' + 'name' => 'stock_orders_files' ], 'validate' => [ 'module' => 'common', - 'name' => 'user_group_members' + 'name' => 'stock_orders_files' ], - 'table' => 'user_group_members' + 'table' => 'stock_orders_files' ]; $result = (new Generate($options))->doGenerate(); diff --git a/app/crm/controller/StockOrdersController.php b/app/crm/controller/StockOrdersController.php index b297b51..3716c69 100644 --- a/app/crm/controller/StockOrdersController.php +++ b/app/crm/controller/StockOrdersController.php @@ -61,8 +61,44 @@ class StockOrdersController extends BaseController 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']); + // 处理文件附件 + $fileTypes = [ + 'contract' => \app\common\model\StockOrdersFiles::CONTRACT, + 'transaction_slip' => \app\common\model\StockOrdersFiles::TRANSACTION_SLIP, + 'pi_doc' => \app\common\model\StockOrdersFiles::PI_DOC + ]; + + // 初始化文件字段 + foreach (array_keys($fileTypes) as $field) { + $data[$field] = []; + $data[$field . '_url'] = []; + } + + // 按file_code分组文件 + if (!empty($data['files'])) { + $groupedFiles = []; + foreach ($data['files'] as $file) { + if (!isset($groupedFiles[$file['file_code']])) { + $groupedFiles[$file['file_code']] = []; + } + $groupedFiles[$file['file_code']][] = $file['file_url']; + } + + // 映射到对应字段 + foreach ($fileTypes as $field => $fileCode) { + if (isset($groupedFiles[$fileCode])) { + // 原始URL数组 + $data[$field] = $groupedFiles[$fileCode]; + + // 完整URL数组 + $fullUrls = []; + foreach ($groupedFiles[$fileCode] as $url) { + $fullUrls[] = (new \app\common\service\OssService)->generatePrivateUrl($url); + } + $data[$field . '_url'] = $fullUrls; + } + } + unset($data['files']); } if (count($data['moneyValues']) > 0) { @@ -223,99 +259,4 @@ class StockOrdersController extends BaseController return $this->error($e->getMessage()); } } - - public function desc(Request $request) - { - - $data = [ - 'id' => '', //'主键', - 'order_no' => '', //'备货单编号(业务唯一)', - 'pi_no' => '', //'PI号'/'阿里订单号', - 'biz_date' => '', //'单据日期(时间戳)', - 'salesperson_id' => '', //'业务员ID(关联数据)', - 'salesperson_snapshot' => '', //'业务员姓名快照', - 'clearance_country_id' => '', //'清关目的国ID(关联数据)', - 'clearance_country_snapshot' => '', //'清关目的国名称快照', - // 'ali_order_no' => '', //'阿里订单号', - 'customer_source_id' => '', //'客户来源ID(关联数据)', - 'customer_source_snapshot' => '', //'客户来源名称快照', - 'customer_id' => '', //'客户ID(关联数据)', - 'customer_snapshot' => '', //'客户名称快照', - 'customer_country_id' => '', //'客户所在国ID(关联数据)', - 'customer_country_snapshot' => '', //'客户所在国名称快照', - 'declarant_name' => '', //'清关人(文本)', - 'eta_ship_date' => '', //'预计发运日(时间戳)', - 'shipping_method' => '', //'出运方式(如海运/空运/铁路等)', - 'cif_total_amount' => '', //'0.00' COMMENT '合计到岸价(CIF)', - 'domestic_port_id' => '', //'国内收货港ID(关联数据)', - 'domestic_port_snapshot' => '', //'国内收货港名称快照', - 'trade_term_id' => '', //'贸易条款ID(关联数据,EXW/FOB/CIF等)', - 'trade_term_snapshot' => '', //'贸易条款名称快照', - 'clearance_port_id' => '', //'清关目的港ID(关联数据)', - 'clearance_port_snapshot' => '', //'清关目的港名称快照', - 'latest_arrival_date' => '', //'最迟到港时间(时间戳)', - 'contract_exchange_rate' => '', //'合同汇率(下单锁定)', - 'settlement_currency' => '', //'CNY' COMMENT '结算币种', - 'remark' => '', //'备注', - // 'real_veh' => '', //'0' COMMENT '实车ID', - 'is_final' => '', //'0' COMMENT '是否终版1是0否', - 'status' => '', //'draft' COMMENT '单据状态:draft/submitted/approved/void/reversed等', - ]; - - // (?<=\x60 )[^\r\n']*(?=') - - $items = [ - // 'id' => '', // '主键', - // 'order_id' => '', // '所属备货单ID', - // 'product_code' => '', // '商品编号(可作外部编码)', - // 'sku_snapshot' => '', // '商品/车型名称快照', - // 'unit_price_value_id' => '', // '单价', - // 'handling_fee_value_id' => '', // '操作费', - // 'fob_transport_fee_value_id' => '', // 'FOB内陆运费', - // 'port_misc_fee_value_id' => '', // '港杂费', - // 'retrofit_fee_value_id' => '', // '改装费', - // 'total_price_value_id' => '', // '合计(按订单结算币计算)', - - 'vin' => '', // '车架号', - 'sku_id' => '', // '商品/车型ID(关联数据)', - 'model_snapshot' => '', // '车型(若与sku一致可复用;此处为显示快照)', - 'quantity' => '', // '1.000000' COMMENT '数量', - 'color_interior_id' => '', // '颜色内饰ID(关联数据)', - 'color_interior_snapshot' => '', // '颜色内饰名称快照', - 'color_exterior_id' => '', // '颜色外饰ID(关联数据)', - 'color_exterior_snapshot' => '', // '颜色外饰名称快照', - 'remark' => '', // '备注', - 'unit_price' => '', // '0.000000' COMMENT '单价(含/不含税按业务口径约定)', - 'handling_fee' => '', // '0.000000' COMMENT '手续费(按单车)', - 'fob_transport_fee' => '', // '0.000000' COMMENT 'FOB运输费(按单车)', - 'port_misc_fee' => '', // '0.000000' COMMENT '港杂(按单车)', - 'retrofit_fee' => '', // '0.000000' COMMENT '改装费(按单车)', - 'total_price' => '', // '0.000000' COMMENT '总价(quantity*unit_price,写时落地)', - 'official_price' => '', // 官方指导价 - 'discount_price' => '', // 优惠价 - - - 'modify_list' => [ - // 'order_item_id' => '', // '所属明细ID', - 'option_id' => '', // '配置项ID(关联数据)', - 'option_name_snapshot' => '', // '配置项名称快照', - 'option_price' => '', // '配置项价格(可选,写时快照)', - ] - ]; - - - $account = [ - 'order_id' => '', // '所属备货单ID', - // 'seq_no' => '', // '1' COMMENT '账户序号(行号)', - 'remit_country_id' => '', // '汇款国ID(关联数据)', - 'remit_country_name_snapshot' => '', // '汇款国名称快照', - 'remit_date' => '', // '汇款日期(时间戳)', - 'currency_code' => '', // 'USD' COMMENT '币种代码(如USD/CNY/AED等)', - 'currency_name_snapshot' => '', // '币种名称快照(可选)', - 'account_id' => '', // '账户ID(关联数据)', - 'account_name_snapshot' => '', // '账户名称/开户名快照', - 'remitter_name' => '', // '汇款人', - 'amount' => '', // '0.00' COMMENT '金额', - ]; - } } \ No newline at end of file diff --git a/app/crm/service/StockOrdersService.php b/app/crm/service/StockOrdersService.php index 9a98e6f..492ffd3 100644 --- a/app/crm/service/StockOrdersService.php +++ b/app/crm/service/StockOrdersService.php @@ -50,6 +50,7 @@ class StockOrdersService extends \app\common\service\StockOrdersService $config['with'][] = 'accounts'; $config['with'][] = 'moneyValues'; $config['with'][] = 'salesperson'; + $config['with'][] = 'files'; break; case 'store': break; diff --git a/app/crm/service/StockOrdersService_bak.php b/app/crm/service/StockOrdersService_bak.php deleted file mode 100644 index 6e1f287..0000000 --- a/app/crm/service/StockOrdersService_bak.php +++ /dev/null @@ -1,444 +0,0 @@ -validator; - } - - - protected function buildQueryConfig(Request $request, string $scene = 'index'): array - { - $p = $request->all(); - - $config = []; - $config['model'] = $this->modelClass; - switch ($scene) { - case 'index': - - // ====== ====== 筛选 ====== ====== - // 业务id - if (isset($p['salesperson_id']) && $p['salesperson_id']) { - $config['where']['salesperson_id'] = $p['salesperson_id']; - } - // 初、终版 - $config['where']['is_final'] = $p['is_final'] ?? 0; - // 关键字 - if (isset($p['keyword']) && $p['keyword']) { - $k = '%' . $p['keyword'] . '%'; - foreach (['pi_no', 'order_no', 'customer_snapshot'] as $col) { - $config['orWhere'][] = [$col, 'like', $k]; - } - } - - $config['order']['id'] = 'desc'; - - $config['where']['salesperson_id'] = $request->user->uid; - break; - case 'print': - $config['where']['id'] = $p['id']; - break; - case 'show': - $config['where']['id'] = $p['id']; - - $config['with'][] = 'items'; - $config['with'][] = 'items.options'; - $config['with'][] = 'items.moneyValues'; - $config['with'][] = 'accounts'; - $config['with'][] = 'moneyValues'; - $config['with'][] = 'salesperson'; - break; - case 'store': - break; - case 'update': - $config['where']['id'] = $p['id']; - break; - case 'destroy': - $config['where']['id'] = $p['id']; - break; - } - - return $config; - } - - - public function serviceStore(Request $request, array $data = []) - { - // 关联表较多,增加事务处理 - return Db::transaction(function () use ($request, $data) { - $data = $this->preProcess('Store', $data, $request); - $dl = $this->getDataLimitFromRequest($request); - $created = $this->logicCreate($data, $dl); // 表1 - - // 车辆 - $cifTotalAmount = 0; - $cifTotalAmountRate = 0; - $cifTotalAmountCurrency = 'CNY'; - // $profit = 0; - foreach ($data['veh_list'] as $veh) { - $veh['order_id'] = $created->id; - - // 唯一标识 - $uniqueIdStr = ""; - $uniqueIdStr .= $veh['sku_id']; - $uniqueIdStr .= $veh['model_snapshot']; - $uniqueIdStr .= $veh['color_interior_code']; - $uniqueIdStr .= $veh['color_exterior_code']; - $uniqueIdStr .= $veh['mileage']; - $uniqueIdStr .= json($veh['modify_list']); - $veh['unique_id'] = md5($uniqueIdStr); - $item = StockOrderItems::create($veh); // 表2 - - // 金额 // 表3 - // $item->addMoney('official_price', $veh['official_price']['currency'], $veh['official_price']['amount'], $veh['official_price']['rate'], '官方指导价'); - // $item->addMoney('discount_price', $veh['discount_price']['currency'], $veh['discount_price']['amount'], $veh['discount_price']['rate'], '优惠价'); - if ($veh['unit_price']['amount']) { - $item->addMoney('unit_price', $veh['unit_price']['currency'], $veh['unit_price']['amount'], $veh['unit_price']['rate'], '单价'); - } - if ($veh['total_price']['amount']) { - $item->addMoney('total_price', $veh['total_price']['currency'], $veh['total_price']['amount'], $veh['total_price']['rate'], '总价'); - } - if ($veh['handling_fee']['amount']) { - $item->addMoney('handling_fee', $veh['handling_fee']['currency'], $veh['handling_fee']['amount'], $veh['handling_fee']['rate'], '手续费'); - } - if ($veh['fob_transport_fee']['amount']) { - $item->addMoney('fob_transport_fee', $veh['fob_transport_fee']['currency'], $veh['fob_transport_fee']['amount'], $veh['fob_transport_fee']['rate'], 'FOB运输费'); - } - if ($veh['port_misc_fee']['amount']) { - $item->addMoney('port_misc_fee', $veh['port_misc_fee']['currency'], $veh['port_misc_fee']['amount'], $veh['port_misc_fee']['rate'], '港杂'); - } - if ($veh['retrofit_fee']['amount']) { - $item->addMoney('retrofit_fee', $veh['retrofit_fee']['currency'], $veh['retrofit_fee']['amount'], $veh['retrofit_fee']['rate'], '改装费'); - } - if ($veh['ocean_freight']['amount']) { - $item->addMoney('ocean_freight', $veh['ocean_freight']['currency'], $veh['ocean_freight']['amount'], $veh['ocean_freight']['rate'], '海运费'); - } - - // 二手车 or 新车 - if ($veh['mileage'] > 0) { - if ($veh['listing_price']['amount']) { - $item->addMoney('listing_price', $veh['listing_price']['currency'], $veh['listing_price']['amount'], $veh['listing_price']['rate'], '二手挂牌价'); - } - if ($veh['reduction_price']['amount']) { - $item->addMoney('reduction_price', $veh['reduction_price']['currency'], $veh['reduction_price']['amount'], $veh['reduction_price']['rate'], '二手减价'); - } - - $ttPrice = ($veh['listing_price']['amount'] / $created->contract_exchange_rate); - $ddPrice = $veh['reduction_price']['amount']; - } else { - if ($veh['official_price']['amount']) { - $item->addMoney('official_price', $veh['official_price']['currency'], $veh['official_price']['amount'], $veh['official_price']['rate'], '官方指导价'); - } - if ($veh['discount_price']['amount'] ) { - $item->addMoney('discount_price', $veh['discount_price']['currency'], $veh['discount_price']['amount'], $veh['discount_price']['rate'], '优惠价'); - } - - $ttPrice = ($veh['official_price']['amount'] / $created->contract_exchange_rate); - $ddPrice = $veh['discount_price']['amount']; - } - - // 利润 - $profit = (float)$veh['unit_price']['amount'] - - (float)$ttPrice - - (float)$ddPrice - - (float)$veh['handling_fee']['amount'] - - (float)$veh['fob_transport_fee']['amount'] - - (float)$veh['port_misc_fee']['amount'] - - (float)$veh['retrofit_fee']['amount'] - - (float)$veh['ocean_freight']['amount']; - $item->addMoney('profit', $veh['unit_price']['currency'], $profit, $veh['unit_price']['rate'], '利润'); - - // cif_total_amount - $cifTotalAmount += $veh['total_price']['amount']; - $cifTotalAmountRate = $veh['retrofit_fee']['rate']; - $cifTotalAmountCurrency = $veh['retrofit_fee']['currency']; - - // 改装 - if (count($veh['modify_list']) > 0) { - foreach ($veh['modify_list'] as $modify) { - $modify['order_item_id'] = $item->id; - StockOrderItemOptions::create($modify); // 表4 - } - } - } - - // 计算&&插入 表1 - $created->addMoney('cif_total_amount', $cifTotalAmountCurrency, $cifTotalAmount, $cifTotalAmountRate, '合计到岸价'); - - // 账户 - if (count($data['account_list']) > 0) { - foreach ($data['account_list'] as $account) { - $account['order_id'] = $created->id; - $account['remit_date'] = strtotime($account['remit_date']); - StockOrderAccounts::create($account); // 表5 - } - } - - return $this->postProcess('Store', $created); - }); - } - - public function beforeServiceStore($data, Request $request) - { - $userId = $request->user->uid; - - $data['order_no'] = generateStockNo(); - $data['biz_date'] = strtotime($data['biz_date']); - $data['eta_ship_date'] = strtotime($data['eta_ship_date']); - $data['latest_arrival_date'] = strtotime($data['latest_arrival_date']); - $data['salesperson_id'] = $userId; - $data['salesperson_snapshot'] = User::where('id', $userId)->value('name') ?? ''; - - return $data; - } - - protected function afterServiceStore($res) - { - // 处理原单 - if ($res->is_final == 1 && $res->object_id > 0) { - $order = StockOrders::find($res->object_id); - $order->is_use = 1; - $order->object_id = $res->id; - if (!$order->save()) { - flg('备货单【'.$order->order_no.'】创建终版成功,但更新原单失败'); - } - } - // 启动审批 - $appro = new \app\common\utils\approval\service\ApprovalFlowService(); - $result = $appro->start('stock_orders', 'stock_orders', $res->id, $res->toArray(), $res->salesperson_id); - var_dump($result); - - // 异步生成pdf文件 - \Webman\RedisQueue\Redis::send('stock-order-pdf', ['id'=>$res->id]); - return $res; - } - - - public function createdPdfFile($id) - { - $config = []; - $config['model'] = $this->modelClass; - $config['where']['id'] = $id; - $config['with'][] = 'items'; - $config['with'][] = 'items.options'; - $config['with'][] = 'items.moneyValues'; - $config['with'][] = 'accounts'; - $config['with'][] = 'moneyValues'; - $config['with'][] = 'salesperson'; - $item = $this->logicShow($config); - $pdfData = $this->pdfData($item); - - // 渲染模板为最终HTML - $tplPath = app_path('common/utils/pdf/template/stockOrder.stub'); - $html = \app\common\utils\pdf\service\PdfHelper::render($tplPath, ['data' => $pdfData]); - - // 生成PDF - $driver = new \app\common\utils\pdf\service\MpdfDriver(); - $pdfBin = $driver->htmlToPdf($html); - - // 存储 - $dir = public_path('pdf').'/'; - $fileName = ($pdfData['order_no'] ?? date('YmdHis')) . '.pdf'; - - // 本地保存 - \app\common\utils\pdf\service\PdfStorage::saveBinary( - $pdfBin, - $dir, - $fileName - ); - - // // 在线预览 - // return new \support\Response(200, [ - // 'Content-Type' => 'application/pdf', - // 'Content-Disposition' => 'inline; filename="stock-order.pdf"', - // ], $pdfBin); - - // $url = [ - // 'file_name' => $fileName, - // 'file_url' => getenv('MAIN_DOMAIN').'/pdf/'.$fileName - // ]; - - // 上传云端 - $param = [ - 'path' => $dir . $fileName, - 'scene' => 'stock_orders', - 'category' => 'pdf', - 'original_name' => $fileName - ]; - $up = new \app\common\service\UploadService(); - $res = $up->uploadLocalFile($param); - - @unlink($dir . $fileName); - - // 更新文件地址 - $item->file_url = $res['file_name']; - $item->save(); - - return $res; - } - - public function servicePrint(Request $request) - { - $config = $this->buildQueryConfig($request, 'show'); - $item = $this->logicShow($config); - return $this->postProcess('Print', $item); - } - - - public function pdfData($item) - { - $vehList = []; - $index = 0; - foreach ($item->items as $k => $v) { - // 存在则数量加1 - if (count($vehList) > 0 && isset($vehList[$v->unique_id])) { - $vehList[$v->unique_id]['num'] += 1; - $vehList[$v->unique_id]['total_price'] += $vehList[$v->unique_id]['unit_price']; - $vehList[$v->unique_id]['total_price_text'] = $vehList[$v->unique_id]['total_price_symbol'].number_format($vehList[$v->unique_id]['total_price'], 2, '.', ','); - continue; - } - $index += 1; - // 处理金额 - $mv = []; - foreach ($v->moneyValues as $money) { - $mv[$money->field] = [ - 'currency' => $money->currency, - 'amount' => $money->amount, - 'rate' => $money->exchange_rate, - 'currency_symbol' => SysCurrency::where('currency_code', $money->currency)->value('currency_symbol'), - ]; - } - // 处理改装 - $mfy = ''; - foreach ($v->options as $my) { - $mfy .= $my->option_name_snapshot.' '; - } - - // 处理结果集 - $vehList[$v->unique_id] = [ - 'index' => $index, - 'model_snapshot' => $v->model_snapshot, - 'color_interior_snapshot' => $v->color_interior_snapshot, - 'color_exterior_snapshot' => $v->color_exterior_snapshot, - 'num' => 1, - 'modify' => $mfy, - 'mileage' => $v->mileage, - 'unit_price' => $mv['unit_price']['amount'], - 'total_price' => $mv['total_price']['amount'], - 'unit_price_symbol' => $mv['unit_price']['currency_symbol'], - 'total_price_symbol' => $mv['total_price']['currency_symbol'], - 'unit_price_text' => $mv['unit_price']['currency_symbol'].number_format($mv['unit_price']['amount'], 2, '.', ','), - 'total_price_text' => $mv['total_price']['currency_symbol'].number_format($mv['total_price']['amount'], 2, '.', ',') - ]; - - // 根据里程处理 新车or二手车 - if ($v->mileage > 0) { - if (isset($mv['reduction_price'])) { - $vehList[$v->unique_id]['reduction_price'] = $mv['reduction_price']['amount']; - $vehList[$v->unique_id]['reduction_price_symbol'] = $mv['reduction_price']['currency_symbol']; - $vehList[$v->unique_id]['reduction_price_text'] = $mv['reduction_price']['currency_symbol'].number_format($mv['reduction_price']['amount'], 2, '.', ','); - } - $vehList[$v->unique_id]['listing_price'] = $mv['listing_price']['amount']; - $vehList[$v->unique_id]['listing_price_symbol'] = $mv['listing_price']['currency_symbol']; - $vehList[$v->unique_id]['listing_price_text'] = $mv['listing_price']['currency_symbol'].number_format($mv['listing_price']['amount'], 2, '.', ','); - } else { - if (isset($mv['discount_price'])) { - $vehList[$v->unique_id]['discount_price'] = $mv['discount_price']['amount']; - $vehList[$v->unique_id]['discount_price_symbol'] = $mv['discount_price']['currency_symbol']; - $vehList[$v->unique_id]['discount_price_text'] = $mv['discount_price']['currency_symbol'].number_format($mv['discount_price']['amount'], 2, '.', ','); - } - $vehList[$v->unique_id]['official_price'] = $mv['official_price']['amount']; - $vehList[$v->unique_id]['official_price_symbol'] = $mv['official_price']['currency_symbol']; - $vehList[$v->unique_id]['official_price_text'] = $mv['official_price']['currency_symbol'].number_format($mv['official_price']['amount'], 2, '.', ','); - } - } - - $accList = []; - $accIndex = 1; - foreach ($item->accounts as $k => $v) { - $accList[] = [ - 'index' => '收款'.$k + 1, - 'remit_country_name_snapshot' => $v->remit_country_name_snapshot, - 'remit_date' => date('Y-m-d', $v->remit_date), - 'currency_name_snapshot' => $v->currency_name_snapshot, - 'account_name_snapshot' => $v->account_name_snapshot, - 'remitter_name' => $v->remitter_name, - 'amount' => $v->amount, - ]; - $accIndex += 1; - } - - $cifTotalAmount = 0.00; - if ($item->moneyValues) { - $cifTotalAmountSysboml = SysCurrency::where('currency_code', $item->moneyValues->currency)->value('currency_symbol'); - $cifTotalAmount = $cifTotalAmountSysboml.sprintf("%.2f", $item->moneyValues->amount, $item->moneyValues->exchange_rate); - } - - $veh = [ - 'order_no' => $item->order_no, - 'salesperson_snapshot' => $item->salesperson_snapshot, - // 'sign_img' => $item->salesperson->sign_img, - 'sign_img_url' => (!empty($item->salesperson->sign_img))?(new \app\common\service\OssService)->generatePrivateUrl($item->salesperson->sign_img):'', - 'created_at' => date('Y-m-d', strtotime($item->created_at)), - - 'pi_no' => $item->pi_no, - 'biz_date' => date('Y-m-d', $item->biz_date), - 'clearance_country_snapshot' => $item->clearance_country_snapshot, - 'customer_source_snapshot' => $item->customer_source_snapshot, - 'customer_snapshot' => $item->customer_snapshot, - 'customer_country_snapshot' => $item->customer_country_snapshot, - - // 订单项 - 'veh_list' => $vehList, - - 'contract_exchange_rate' => $item->contract_exchange_rate, - 'cif_total_amount' => $item->cif_total_amount, - - 'declarant_name' => $item->declarant_name, - 'eta_ship_date' => date('Y-m-d', $item->eta_ship_date), - 'shipping_method_snapshot' => $item->shipping_method_snapshot, - 'domestic_port_snapshot' => $item->domestic_port_snapshot, - 'trade_term_snapshot' => $item->trade_term_snapshot, - 'clearance_port_snapshot' => $item->clearance_port_snapshot, - 'latest_arrival_date' => date('Y-m-d', $item->latest_arrival_date), - - // 账户 - 'account_list' => $accList, - 'acc_index' => $accIndex, - 'cif_total_amount' => $cifTotalAmount - ]; - - return $veh; - } -} \ No newline at end of file diff --git a/app/crm/service/StockOrdersService_bak2.php b/app/crm/service/StockOrdersService_bak2.php deleted file mode 100644 index 1c4f7ea..0000000 --- a/app/crm/service/StockOrdersService_bak2.php +++ /dev/null @@ -1,762 +0,0 @@ -validator; - } - - - protected function buildQueryConfig(Request $request, string $scene = 'index'): array - { - $p = $request->all(); - - $config = []; - $config['model'] = $this->modelClass; - switch ($scene) { - case 'index': - - // ====== ====== 筛选 ====== ====== - // 业务id - if (isset($p['salesperson_id']) && $p['salesperson_id']) { - $config['where']['salesperson_id'] = $p['salesperson_id']; - } - // 初、终版 - $config['where']['is_final'] = $p['is_final'] ?? 0; - // 关键字 - if (isset($p['keyword']) && $p['keyword']) { - $k = '%' . $p['keyword'] . '%'; - foreach (['pi_no', 'order_no', 'customer_snapshot'] as $col) { - $config['orWhere'][] = [$col, 'like', $k]; - } - } - - $config['order']['id'] = 'desc'; - - $config['where']['salesperson_id'] = $request->user->uid; - break; - case 'print': - case 'approval': - $config['where']['id'] = $p['id']; - break; - case 'detail': - case 'show': - $config['where']['id'] = $p['id']; - - $config['with'][] = 'items'; - $config['with'][] = 'items.options'; - $config['with'][] = 'items.moneyValues'; - $config['with'][] = 'accounts'; - $config['with'][] = 'moneyValues'; - $config['with'][] = 'salesperson'; - break; - case 'store': - break; - case 'update': - $config['where']['id'] = $p['id']; - break; - case 'destroy': - $config['where']['id'] = $p['id']; - break; - } - - return $config; - } - - public function beforeServiceStore($data, Request $request) - { - $userId = $request->user->uid; - - $data['order_no'] = generateStockNo(); - $data['biz_date'] = strtotime($data['biz_date']); - $data['eta_ship_date'] = strtotime($data['eta_ship_date']); - $data['latest_arrival_date'] = strtotime($data['latest_arrival_date']); - $data['salesperson_id'] = $userId; - $data['salesperson_snapshot'] = User::where('id', $userId)->value('name') ?? ''; - $data['approval_status'] = $data['approval_status'] == 1 ? 1 : 0; - - // if ($data['is_final'] == 1) { - $data['pick_up_date'] = $data['pick_up_date'] ? strtotime($data['pick_up_date']) : 0; - $data['collect_all_date'] = $data['collect_all_date'] ? strtotime($data['collect_all_date']) : 0; - // } - return $data; - } - - protected function afterServiceStore($res) - { - // 处理原单 - if ($res->is_final == 1 && $res->object_id > 0) { - $order = StockOrders::find($res->object_id); - $order->is_use = 1; - $order->object_id = $res->id; - if (!$order->save()) { - flg('备货单【'.$order->order_no.'】创建终版成功,但更新原单失败'); - } - } - // 启动审批 - if ($res->approval_status == 1) { - $this->startApproval($res); - } - return $res; - } - - /** - * 启动审批 - */ - public function startApproval($item) - { - // 启动审批 - $appro = new \app\common\utils\approval\service\ApprovalFlowService(); - $appro->start('stock_orders', 'stock_orders', $item->id, [], $item->salesperson_id); - // 异步生成pdf文件 - \Webman\RedisQueue\Redis::send('stock-order-pdf', ['id'=>$item->id]); - } - - /** - * 创建PDF文件 - */ - public function createdPdfFile($id) - { - $config = []; - $config['model'] = $this->modelClass; - $config['where']['id'] = $id; - $config['with'][] = 'items'; - $config['with'][] = 'items.options'; - $config['with'][] = 'items.moneyValues'; - $config['with'][] = 'accounts'; - $config['with'][] = 'moneyValues'; - $config['with'][] = 'salesperson'; - $item = $this->logicShow($config); - $pdfData = $this->pdfData($item); - - // 渲染模板为最终HTML - $tplPath = app_path('common/utils/pdf/template/stockOrder.stub'); - $html = \app\common\utils\pdf\service\PdfHelper::render($tplPath, ['data' => $pdfData]); - - // 生成PDF - $driver = new \app\common\utils\pdf\service\MpdfDriver(); - $pdfBin = $driver->htmlToPdf($html); - - // 存储 - $dir = public_path('pdf').'/'; - $fileName = ($pdfData['order_no'] ?? date('YmdHis')) . '.pdf'; - - // 本地保存 - \app\common\utils\pdf\service\PdfStorage::saveBinary( - $pdfBin, - $dir, - $fileName - ); - - // 上传云端 - $param = [ - 'path' => $dir . $fileName, - 'scene' => 'stock_orders', - 'category' => 'pdf', - 'original_name' => $fileName - ]; - $up = new \app\common\service\UploadService(); - $res = $up->uploadLocalFile($param); - - @unlink($dir . $fileName); - - // 更新文件地址 - $item->file_url = $res['file_name']; - $item->save(); - - return $res; - } - - public function pdfData($item) - { - $vehList = []; - $index = 0; - $totalProfit = 0; // 总利润 - - // $priceFields = [ - // 'official_price', // '官方指导价', - // 'discount_price', // '优惠', - // 'listing_price', // '二手挂牌价价', - // 'reduction_price', // '优惠', - - // 'handling_fee', // '手续费', - // 'retrofit_fee', // '改装费', - // 'fob_transport_fee', // 'FOB运输费', - // 'port_misc_fee', // '港杂', - // 'ocean_freight', // '海运费', - - // 'unit_price', // '单价', - // 'total_price', // '总价', - // // 'profit', // '利润' - // ]; - - foreach ($item->items as $k => $v) { - $currencyInfo = SysCurrency::where('currency_code', $item->settlement_currency)->first(['currency_symbol', 'currency_name'])->toArray(); - - // 存在则数量加1 - if (count($vehList) > 0 && isset($vehList[$v->unique_id])) { - $vehList[$v->unique_id]['num'] += 1; - $vehList[$v->unique_id]['total_price']['currency_info'] = $currencyInfo['currency_symbol'].number_format($vehList[$v->unique_id]['total_price']['amount']*$vehList[$v->unique_id]['num'], 2, '.', ','); - $vehList[$v->unique_id]['profit']['currency_info'] = $currencyInfo['currency_symbol'].number_format($vehList[$v->unique_id]['profit']['amount']*$vehList[$v->unique_id]['num'], 2, '.', ','); - continue; - } - - $index += 1; - - // 处理金额 (每次循环重新初始化) - $mv = []; - foreach ($v->moneyValues as $money) { - $mv[$money->field] = [ - 'currency' => $money->currency, - 'amount' => $money->amount, - 'rate' => $money->exchange_rate, - 'currency_info' => SysCurrency::where('currency_code', $money->currency)->value('currency_symbol').number_format($money->amount, 2, '.', ',') - ]; - } - - // 处理改装 (每次循环重新初始化) - $mfy = ''; - foreach ($v->options as $my) { - $mfy .= $my->option_name_snapshot.' '; - } - - // 计算单项利润并累加到总利润 - $itemProfit = $this->calculateProfit($mv, $item); - $totalProfit += $itemProfit; - - // 处理结果集 - $vehListItem = [ - 'index' => $index, - 'model_snapshot' => $v->model_snapshot, - 'color_interior_snapshot' => $v->color_interior_snapshot, - 'color_exterior_snapshot' => $v->color_exterior_snapshot, - 'num' => 1, - 'modify' => $mfy, - 'mileage' => $v->mileage - ]; - - $vehList[$v->unique_id] = $vehListItem + $mv; - } - - $mvs = []; - foreach ($item->moneyValues as $moneys) { - $mvs[$moneys->field] = [ - 'currency' => $moneys->currency, - 'amount' => $moneys->amount, - 'rate' => $moneys->exchange_rate, - 'currency_info' => SysCurrency::where('currency_code', $moneys->currency)->value('currency_symbol').number_format($moneys->amount, 2, '.', ','), - ]; - } - - $accList = []; - foreach ($item->accounts as $k => $v) { - $accList[] = [ - 'index' => '收款'.($k + 1), - 'remit_country_name_snapshot' => $v->remit_country_name_snapshot, - 'remit_date' => date('Y-m-d', $v->remit_date), - 'currency_name_snapshot' => $v->currency_name_snapshot, - 'account_name_snapshot' => $v->account_name_snapshot, - 'remitter_name' => $v->remitter_name, - 'amount' => $v->amount, - ]; - } - - - - $veh = [ - 'order_no' => $item->order_no, - 'salesperson_snapshot' => $item->salesperson_snapshot, - 'sign_img_url' => (!empty($item->salesperson->sign_img)) ? - (new \app\common\service\OssService)->generatePrivateUrl($item->salesperson->sign_img) : '', - 'created_at' => date('Y-m-d', strtotime($item->created_at)), - - 'pi_no' => $item->pi_no, - 'biz_date' => date('Y-m-d', $item->biz_date), - 'clearance_country_snapshot' => $item->clearance_country_snapshot, - 'customer_source_snapshot' => $item->customer_source_snapshot, - 'customer_snapshot' => $item->customer_snapshot, - 'customer_country_snapshot' => $item->customer_country_snapshot, - - // 订单项 - 'veh_list' => array_values($vehList), // 重新索引数组 - - 'contract_exchange_rate' => $item->contract_exchange_rate, - 'cif_total_amount' => $mvs['cif_total_amount'], - 'profit_total' => $mvs['profit_total'], - - 'declarant_name' => $item->declarant_name, - 'eta_ship_date' => date('Y-m-d', $item->eta_ship_date), - 'shipping_method_snapshot' => $item->shipping_method_snapshot, - 'domestic_port_snapshot' => $item->domestic_port_snapshot, - 'trade_term_snapshot' => $item->trade_term_snapshot, - 'clearance_port_snapshot' => $item->clearance_port_snapshot, - 'latest_arrival_date' => date('Y-m-d', $item->latest_arrival_date), - 'pick_up_date' => date('Y-m-d', $item->pick_up_date), - 'collect_all_date' => date('Y-m-d', $item->collect_all_date), - 'is_final' => $item->is_final, - - // 账户 - 'account_list' => $accList, - 'acc_index' => count($accList)+1, - 'currency_symbol' => $item->settlement_currency, - 'sign_imgs' => $this->queryAppro($item->id), - 'contract_url' => (!empty($item->contract)) ? (new \app\common\service\OssService)->generatePrivateUrl($item->contract) : '' - ]; - - return $veh; - } - - - public function queryAppro($bizId) - { - $instances = ApprovalInstances::where('biz_id', $bizId)->where('biz_type', 'stock_orders')->first(); - if (!$instances) { - return []; - } - - $records = ApprovalRecords::where('instance_id', $instances->id)->where('action', 'approve')->orderBy('step_index', 'asc')->pluck('approver_id')->toArray(); - if (count($records) == 0) { - return []; - } - - $signImgs = []; - foreach ($records as $k => $v) { - $signImg = User::where('id', $v)->value('sign_img'); - $signImgs[$k] = (!empty($signImg))?(new \app\common\service\OssService)->generatePrivateUrl($signImg):''; - } - - return $signImgs; - } - - /** - * 打印数据 - */ - public function servicePrint(Request $request) - { - $config = $this->buildQueryConfig($request, 'show'); - $item = $this->logicShow($config); - return $this->postProcess('Print', $item); - } - - public function serviceDetail(Request $request) - { - $config = $this->buildQueryConfig($request, 'detail'); - $item = $this->logicShow($config); - $pdfData = $this->pdfData($item); - return $this->postProcess('Detail', $pdfData); - } - - /** - * 审批 - */ - public function serviceApproval(Request $request) - { - $config = $this->buildQueryConfig($request, 'approval'); - $this->logicUpdateBy($config, [ - 'approval_status' => 1 - ]); - $item = $this->logicShow($config); - $this->startApproval($item); - return $this->postProcess('Approval', $item); - } - - /** - * 最终审批 - */ - public function serviceUltimateApproval(Request $request) - { - $config = $this->buildQueryConfig($request, 'detail'); - $item = $this->logicShow($config); - foreach ($item->items as $k => $v) { - if ($v->vin === '') { - throw new BusinessException('存在未填写vin的车辆,无法完成提交审核'); - } - } - $this->logicUpdateBy($config, [ - 'approval_status' => 1 - ]); - $this->startApproval($item); - return $this->postProcess('UltimateApproval', $item); - } - - /** - * 创建 - * - * @param Request $request - * @param array $data - * @return mixed - */ - public function serviceStore(Request $request, array $data = []) - { - // 关联表较多,增加事务处理 - return Db::transaction(function () use ($request, $data) { - $data = $this->preProcess('Store', $data, $request); - $dl = $this->getDataLimitFromRequest($request); - $created = $this->logicCreate($data, $dl); - - // 处理车辆和账户信息 - $this->processOrderItems($created, $data); - - return $this->postProcess('Store', $created); - }); - } - - protected function onlyFillableData(Model|string $model, array $data, array $extraWhitelist = []): array - { - // 兼容传类名或实例 - $m = \is_string($model) ? new $model : $model; - // 1) 优先使用 fillable - $keys = $m->getFillable(); - // 2) 若没配置 fillable(常见:用 guarded),回退到完整表字段,避免误伤 - if (empty($keys)) { - $keys = $m->getConnection() - ->getSchemaBuilder() - ->getColumnListing($m->getTable()); - } - // 3) 允许额外白名单(如虚拟字段、JSON映射字段) - if ($extraWhitelist) { - $keys = array_merge($keys, $extraWhitelist); - } - return array_intersect_key($data, array_flip($keys)); - } - - /** - * 更新前处理 - * - * @param array $data - * @param Request $request - * @return array - */ - public function beforeServiceUpdate($data, Request $request) - { - $data['biz_date'] = strtotime($data['biz_date']); - $data['eta_ship_date'] = strtotime($data['eta_ship_date']); - $data['latest_arrival_date'] = strtotime($data['latest_arrival_date']); - $data['approval_status'] = $data['approval_status'] == 1 ? 1 : 0; - - $data['pick_up_date'] = $data['pick_up_date'] ? strtotime($data['pick_up_date']) : 0; - $data['collect_all_date'] = $data['collect_all_date'] ? strtotime($data['collect_all_date']) : 0; - return $data; - } - - /** - * 更新后处理 - * - * @param mixed $res - * @return mixed - */ - protected function afterServiceUpdate($res) - { - // 启动审批 - if ($res->approval_status == 1) { - $this->startApproval($res); - } - return $res; - } - - /** - * 更新 - * - * @param Request $request - * @param array $data - * @return mixed - */ - public function serviceUpdate(Request $request, array $data = []) - { - // 关联表较多,增加事务处理 - return Db::transaction(function () use ($request, $data) { - $data = $this->preProcess('Update', $data, $request); - $config = $this->buildQueryConfig($request, 'update'); - // 过滤掉非库字段 - $dbData = $this->onlyFillableData($config['model'], $data); - // 更新主表 - $updated = $this->logicUpdateBy($config, $dbData); - // 获取更新后的数据 - $order = $this->logicShow($config); - // 处理车辆和账户信息 - $this->processOrderItems($order, $data, true); - return $this->postProcess('Update', $order); - }); - } - - /** - * 处理订单项(车辆和账户) - * @param StockOrders $order 订单对象 - * @param array $data 数据 - * @param bool $isUpdate 是否为更新操作 - */ - private function processOrderItems($order, array $data, bool $isUpdate = false): void - { - // 如果是更新操作,先删除原有的关联数据 - if ($isUpdate) { - $this->clearExistingOrderItems($order->id); - } - - // 处理车辆项 - $this->processVehicleItems($order, $data['veh_list'] ?? []); - - // 处理账户项 - $this->processAccountItems($order, $data['account_list'] ?? []); - } - - /** - * (更新时)清除已存在的订单项 - * @param int $orderId 订单ID - */ - private function clearExistingOrderItems(int $orderId): void - { - // 删除车辆改装项 - StockOrderItemOptions::whereHas('item', function ($query) use ($orderId) { - $query->where('order_id', $orderId); - })->delete(); - - // 删除车辆项及其相关金额记录 - $orderItems = StockOrderItems::where('order_id', $orderId)->get(); - foreach ($orderItems as $item) { - // 删除车辆项的金额记录 - Db::table('veh_money_value') - ->where('object_scene', 'stock_order_items') - ->where('object_id', $item->id) - ->delete(); - } - - // 删除车辆项 - StockOrderItems::where('order_id', $orderId)->delete(); - - // 删除订单本身的金额记录 - Db::table('veh_money_value') - ->where('object_scene', 'stock_orders') - ->where('object_id', $orderId) - ->delete(); - - // 删除账户项 - StockOrderAccounts::where('order_id', $orderId)->delete(); - } - - /** - * 处理车辆项 - */ - private function processVehicleItems($order, array $vehList): void - { - $cifTotalAmount = 0.00; - // $cifTotalAmountRate = 0; - // $cifTotalAmountCurrency = 'CNY'; - - $profitTotalAmount = 0.00; - foreach ($vehList as $veh) { - $veh['order_id'] = $order->id; - $veh['unique_id'] = $this->generateVehicleUniqueId($veh); - - $item = StockOrderItems::create($veh); - - // 处理金额信息 - $this->processVehiclePrices($item, $veh); - - // 计算利润 - $profit = $this->calculateProfit($veh, $order); - $item->addMoney('profit', $order->settlement_currency, $profit, $order->contract_exchange_rate, '利润'); - - // 累计到岸价 - $cifTotalAmount += $veh['total_price']['amount'] ?? 0; - $profitTotalAmount += $profit; - - // if (isset($veh['retrofit_fee']['rate'])) { - // $cifTotalAmountRate = $veh['retrofit_fee']['rate']; - // } - // if (isset($veh['retrofit_fee']['currency'])) { - // $cifTotalAmountCurrency = $veh['retrofit_fee']['currency']; - // } - - // 处理改装项 - $this->processModificationItems($item->id, $veh['modify_list'] ?? []); - } - - // 保存合计到岸价 - $order->addMoney('cif_total_amount', $order->settlement_currency, $cifTotalAmount, $order->contract_exchange_rate, '合计到岸价'); - // 利润 - $order->addMoney('profit_total', $order->settlement_currency, $profitTotalAmount, $order->contract_exchange_rate, '总利润'); - } - - /** - * 生成车辆唯一标识 - */ - private function generateVehicleUniqueId(array $veh): string - { - $uniqueIdStr = ""; - $uniqueIdStr .= $veh['sku_id'] ?? ''; - $uniqueIdStr .= $veh['model_snapshot'] ?? ''; - $uniqueIdStr .= $veh['color_interior_code'] ?? ''; - $uniqueIdStr .= $veh['color_exterior_code'] ?? ''; - $uniqueIdStr .= $veh['mileage'] ?? ''; - $uniqueIdStr .= json_encode($veh['modify_list'] ?? []); - - return md5($uniqueIdStr); - } - - /** - * 处理车辆价格信息 - */ - private function processVehiclePrices($item, array $veh): void - { - // 基础费用 - $priceFields = [ - 'unit_price' => '单价', - 'total_price' => '总价', - 'handling_fee' => '手续费', - 'fob_transport_fee' => 'FOB运输费', - 'port_misc_fee' => '港杂', - 'retrofit_fee' => '改装费', - 'ocean_freight' => '海运费' - ]; - - foreach ($priceFields as $field => $description) { - if (!empty($veh[$field]['amount'])) { - $item->addMoney( - $field, - $veh[$field]['currency'], - $veh[$field]['amount'], - $veh[$field]['rate'], - $description - ); - } - } - - // 根据里程处理新车或二手车价格 - if (!empty($veh['mileage']) && $veh['mileage'] > 0) { - // 二手车 - if (!empty($veh['listing_price']['amount'])) { - $item->addMoney( - 'listing_price', - $veh['listing_price']['currency'], - $veh['listing_price']['amount'], - $veh['listing_price']['rate'], - '二手挂牌价' - ); - } - - if (!empty($veh['reduction_price']['amount'])) { - $item->addMoney( - 'reduction_price', - $veh['reduction_price']['currency'], - $veh['reduction_price']['amount'], - $veh['reduction_price']['rate'], - '二手减价' - ); - } - } else { - // 新车 - if (!empty($veh['official_price']['amount'])) { - $item->addMoney( - 'official_price', - $veh['official_price']['currency'], - $veh['official_price']['amount'], - $veh['official_price']['rate'], - '官方指导价' - ); - } - - if (!empty($veh['discount_price']['amount'])) { - $item->addMoney( - 'discount_price', - $veh['discount_price']['currency'], - $veh['discount_price']['amount'], - $veh['discount_price']['rate'], - '优惠价' - ); - } - } - } - - /** - * 计算利润 - */ - private function calculateProfit(array $veh, $order): float - { - $unitPrice = (float)($veh['unit_price']['amount'] ?? 0); - - if (!empty($veh['mileage']) && $veh['mileage'] > 0) { - // 二手车 - $ttPrice = ($veh['listing_price']['amount'] ?? 0) / ($order->contract_exchange_rate ?: 1); - $ddPrice = $veh['reduction_price']['amount'] ?? 0; - } else { - // 新车 - $ttPrice = ($veh['official_price']['amount'] ?? 0) / ($order->contract_exchange_rate ?: 1); - $ddPrice = $veh['discount_price']['amount'] ?? 0; - } - - $handlingFee = (float)($veh['handling_fee']['amount'] ?? 0); - $fobTransportFee = (float)($veh['fob_transport_fee']['amount'] ?? 0); - $portMiscFee = (float)($veh['port_misc_fee']['amount'] ?? 0); - $retrofitFee = (float)($veh['retrofit_fee']['amount'] ?? 0); - $oceanFreight = (float)($veh['ocean_freight']['amount'] ?? 0); - - return $unitPrice - $ttPrice - $ddPrice - $handlingFee - $fobTransportFee - $portMiscFee - $retrofitFee - $oceanFreight; - } - - /** - * 处理改装项 - */ - private function processModificationItems(int $orderItemId, array $modifyList): void - { - if (empty($modifyList)) { - return; - } - - foreach ($modifyList as $modify) { - $modify['order_item_id'] = $orderItemId; - StockOrderItemOptions::create($modify); - } - } - - /** - * 处理账户项 - */ - private function processAccountItems($order, array $accountList): void - { - if (empty($accountList)) { - return; - } - - foreach ($accountList as $account) { - $account['order_id'] = $order->id; - if (!empty($account['remit_date'])) { - $account['remit_date'] = strtotime($account['remit_date']); - } - StockOrderAccounts::create($account); - } - } -} \ No newline at end of file From 74868034680dbb04a919715c5ba8e06b2a0bc902 Mon Sep 17 00:00:00 2001 From: mzl Date: Wed, 22 Oct 2025 16:15:08 +0800 Subject: [PATCH 021/110] =?UTF-8?q?=E8=B0=83=E6=95=B4pdf=E6=A8=A1=E6=9D=BF?= =?UTF-8?q?=E6=96=87=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../utils/pdf/template/stockOrder copy 2.stub | 336 ++++++++++++++++++ app/common/utils/pdf/template/stockOrder.stub | 165 +++++---- 2 files changed, 436 insertions(+), 65 deletions(-) create mode 100644 app/common/utils/pdf/template/stockOrder copy 2.stub diff --git a/app/common/utils/pdf/template/stockOrder copy 2.stub b/app/common/utils/pdf/template/stockOrder copy 2.stub new file mode 100644 index 0000000..20b10b3 --- /dev/null +++ b/app/common/utils/pdf/template/stockOrder copy 2.stub @@ -0,0 +1,336 @@ + + + + + 合塑实业-汽车事业部-备货单(客户下定) + + + +
+ +
+

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

+
+ 备货单号: + 业务员: +
+
+ + + + + + + + + + + + + + + + + + + + + +
阿里订单号/PI号: 日期: 清关目的国: 客户来源: 客户名称: 客户所在国:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + 0): ?> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
编号车型数量/台颜色/外颜色/内收款价总价官方指导价/二手挂牌价优惠利润
+ + + + 0): ?> + () + + + +
改装费FOB运费车价海运费佣金挂账港杂
+ 备注: + + +
+ 合同汇率: + + 合计到岸价: +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
清关人: 全款收齐日: 预计发运日期: 出运方式: 国内收货港: 贸易条款/清关目的港: /提车日期: 最迟到港时间:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
计划收款信息序号汇款国日期币种账户汇款人金额
+ + +
+ +
+ +
+ 业务员签字: + + + + + +
+ +
+ 备发货负责人签字: + + + + + +
+
+ + +
+
+ 组长签字: + + + + + +
+ +
+ 日期: + +
+
+ +
+
+ + +
+ + \ No newline at end of file diff --git a/app/common/utils/pdf/template/stockOrder.stub b/app/common/utils/pdf/template/stockOrder.stub index 20b10b3..697b8eb 100644 --- a/app/common/utils/pdf/template/stockOrder.stub +++ b/app/common/utils/pdf/template/stockOrder.stub @@ -124,81 +124,116 @@ 编号 车型 - 数量/台 - 颜色/外 - 颜色/内 - 收款价 - 总价 - 官方指导价/二手挂牌价 - 优惠 - 利润 + 数量 + 颜色 + 费用明细 + - - - - - - - 0): ?> - () + + + + + + + + + + + + 0): ?> + (km) + + + + + + + + + + + 外饰:

+ 内饰: + + + + 改装费 + 配件费 + 收款价 + 总价 + 官方指导价/二手挂牌价 + 优惠 + 利润 + + + + + + + + + + 0): ?> + + + + + + + - - - - - - - - - - 0): ?> - - - - - - - - - - - 改装费 - FOB运费 - 车价 - 海运费 - 佣金 - 挂账 - 港杂 - - - - - - - - - - - - - 备注: - - - - - + + + + + + + 报关价 + FOB运费 + 车价 + 海运费 + 佣金 + 挂账 + 港杂 + + + + + + + + + + + + + + + + 备注 + + + + + + + + - - 合同汇率: + + + 合同汇率: + - - 合计到岸价: + + 合计到岸价: + From 6644cc1c58e01c3f8fb60cd4fe5eeb8832b7c4a6 Mon Sep 17 00:00:00 2001 From: "WIN-C27C5MSVTPO\\16961" <1696136552@qq.com> Date: Thu, 23 Oct 2025 11:27:24 +0800 Subject: [PATCH 022/110] =?UTF-8?q?=E6=94=B6=E6=AC=BE=E8=AE=B0=E5=BD=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/common/controller/FinPaymentApplyController.php | 2 +- app/common/controller/FinReceiptController.php | 10 ++++++++++ app/common/service/FinReceiptService.php | 2 +- 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/app/common/controller/FinPaymentApplyController.php b/app/common/controller/FinPaymentApplyController.php index e916df4..57c4b75 100644 --- a/app/common/controller/FinPaymentApplyController.php +++ b/app/common/controller/FinPaymentApplyController.php @@ -75,7 +75,7 @@ class FinPaymentApplyController extends BaseController * @param array $data * @return array */ - protected function showFormat($data) + public function showFormat($data) { if (isset($data['doc_user_id'])) //内勤 $data['doc_user_name'] = User::where('id', $data['doc_user_id'])->value('name')??''; diff --git a/app/common/controller/FinReceiptController.php b/app/common/controller/FinReceiptController.php index ddddfac..c558d44 100644 --- a/app/common/controller/FinReceiptController.php +++ b/app/common/controller/FinReceiptController.php @@ -3,8 +3,12 @@ namespace app\common\controller; use app\common\controller\BaseController; use app\common\model\Country; +use app\common\model\FinPaymentApply; +use app\common\model\FinPaymentStock; use app\common\model\SysAccount; use app\common\model\SysCurrency; +use app\common\model\SysDictItem; +use app\common\model\User; use app\common\service\OssService; use app\common\traits\crud\CrudControllerTrait; use app\common\service\FinReceiptService; @@ -81,6 +85,12 @@ class FinReceiptController extends BaseController } elseif (isset($data['fx_receipt_file'])) { $data['fx_receipt_file_full'] = ''; } + + if (!empty($data['payment_apply_id'])) { + $paymentApply = FinPaymentApply::where('id', $data['payment_apply_id'])->first()->toArray(); + $paymentApply = (new FinPaymentApplyController())->showFormat($paymentApply); + $data['payment_apply'] = $paymentApply; + } return $data; } } \ No newline at end of file diff --git a/app/common/service/FinReceiptService.php b/app/common/service/FinReceiptService.php index 2f77af5..155aecd 100644 --- a/app/common/service/FinReceiptService.php +++ b/app/common/service/FinReceiptService.php @@ -70,7 +70,7 @@ class FinReceiptService } // 关键字 - if (isset($p['$query']) && $p['keyword']) { + if (isset($p['keyword']) && $p['keyword']) { $k = '%' . $p['keyword'] . '%'; $config['orWhere'][] = ['remitter', 'like', $k]; } From 0f505d5c567d1a70ec0a73126be73044e3921b3d Mon Sep 17 00:00:00 2001 From: "WIN-C27C5MSVTPO\\16961" <1696136552@qq.com> Date: Thu, 23 Oct 2025 16:25:02 +0800 Subject: [PATCH 023/110] =?UTF-8?q?=E6=94=B6=E6=AC=BE=E8=AE=B0=E5=BD=95?= =?UTF-8?q?=E8=81=94=E6=9F=A5=E4=BB=98=E6=AC=BE=E5=8D=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/common/controller/FinReceiptController.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/common/controller/FinReceiptController.php b/app/common/controller/FinReceiptController.php index c558d44..ebfd956 100644 --- a/app/common/controller/FinReceiptController.php +++ b/app/common/controller/FinReceiptController.php @@ -87,7 +87,10 @@ class FinReceiptController extends BaseController } if (!empty($data['payment_apply_id'])) { - $paymentApply = FinPaymentApply::where('id', $data['payment_apply_id'])->first()->toArray(); + $paymentApply = FinPaymentApply::where('id', $data['payment_apply_id'])->first(); + if(!$paymentApply) + return $data; + $paymentApply = $paymentApply->toArray(); $paymentApply = (new FinPaymentApplyController())->showFormat($paymentApply); $data['payment_apply'] = $paymentApply; } From 7036e3b849803d8d2100836a77b5f3f6f55bfa9c Mon Sep 17 00:00:00 2001 From: "WIN-C27C5MSVTPO\\16961" <1696136552@qq.com> Date: Thu, 23 Oct 2025 17:09:40 +0800 Subject: [PATCH 024/110] =?UTF-8?q?=E6=8E=92=E5=BA=8F=E8=A7=84=E5=88=99?= =?UTF-8?q?=E8=B0=83=E6=95=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/common/controller/FinReceiptController.php | 16 +++++++++++++++- app/common/service/FinPaymentApplyService.php | 6 ++++-- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/app/common/controller/FinReceiptController.php b/app/common/controller/FinReceiptController.php index ebfd956..56d7bfb 100644 --- a/app/common/controller/FinReceiptController.php +++ b/app/common/controller/FinReceiptController.php @@ -12,6 +12,7 @@ use app\common\model\User; use app\common\service\OssService; use app\common\traits\crud\CrudControllerTrait; use app\common\service\FinReceiptService; +use support\Request; class FinReceiptController extends BaseController { @@ -23,7 +24,18 @@ class FinReceiptController extends BaseController { $this->service = new FinReceiptService(); } - + public function show(Request $request) + { + try { + $raw = $this->service->serviceShow($request); + $data = $this->transformData($raw, method_exists($this, 'showFormat') + ? [$this, 'showFormat'] + : null); + return $this->success($data); + } catch (\Throwable $e) { + return $this->error($e->getMessage()); + } + } /** * 列表页数据格式化 * @@ -91,6 +103,8 @@ class FinReceiptController extends BaseController if(!$paymentApply) return $data; $paymentApply = $paymentApply->toArray(); + $paymentApply['created_at'] = strtotime($paymentApply['created_at']); + $paymentApply['updated_at'] = strtotime($paymentApply['updated_at']); $paymentApply = (new FinPaymentApplyController())->showFormat($paymentApply); $data['payment_apply'] = $paymentApply; } diff --git a/app/common/service/FinPaymentApplyService.php b/app/common/service/FinPaymentApplyService.php index 294bc01..f451ce4 100644 --- a/app/common/service/FinPaymentApplyService.php +++ b/app/common/service/FinPaymentApplyService.php @@ -217,14 +217,16 @@ class FinPaymentApplyService $orderByClause = "CASE WHEN created_at >= {$sevenDaysAgo} THEN (" . implode(' + ', $similarityOrder) . ") - ELSE 999999 + ELSE 0 END ASC, "; } $orderByClause .= "CASE WHEN created_at < {$sevenDaysAgo} THEN id - ELSE 0 + ELSE 999999 END DESC"; + + $orderByClause .= ", id DESC"; $query->orderByRaw($orderByClause); From 4326bcd68b7798e59e180338d5644ffe69f2abff Mon Sep 17 00:00:00 2001 From: mzl Date: Fri, 24 Oct 2025 10:43:36 +0800 Subject: [PATCH 025/110] =?UTF-8?q?=E8=B0=83=E6=95=B4=E5=A4=87=E8=B4=A7?= =?UTF-8?q?=E5=8D=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/common/service/StockOrdersService.php | 57 ++++------------------- 1 file changed, 8 insertions(+), 49 deletions(-) diff --git a/app/common/service/StockOrdersService.php b/app/common/service/StockOrdersService.php index f243654..76d2ab5 100644 --- a/app/common/service/StockOrdersService.php +++ b/app/common/service/StockOrdersService.php @@ -330,33 +330,25 @@ class StockOrdersService $data = $this->preProcess('Store', $data, $request); $dl = $this->getDataLimitFromRequest($request); $created = $this->logicCreate($data, $dl); - // 添加金额 $created->addMoney('cif_total', $data['cif_total']['currency'], $data['cif_total']['amount'], $data['cif_total']['rate'], '合计到岸价'); - // 处理车辆和账户信息 $this->processOrderItems($created, $data); - // 处理文件附件 $this->processOrderFiles($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); } @@ -413,11 +405,13 @@ class StockOrdersService // 过滤掉非库字段 $dbData = $this->onlyFillableData($config['model'], $data); // 更新主表 - $updated = $this->logicUpdateBy($config, $dbData); + $this->logicUpdateBy($config, $dbData); // 获取更新后的数据 $order = $this->logicShow($config); // 处理车辆和账户信息 $this->processOrderItems($order, $data, true); + // 添加金额 + $order->addMoney('cif_total', $data['cif_total']['currency'], $data['cif_total']['amount'], $data['cif_total']['rate'], '合计到岸价'); // 处理文件附件 $this->processOrderFiles($order, $data, true); return $this->postProcess('Update', $order); @@ -436,10 +430,8 @@ class StockOrdersService if ($isUpdate) { $this->clearExistingOrderItems($order->id); } - // 处理车辆项 $this->processVehicleItems($order, $data['veh_list'] ?? []); - // 处理账户项 $this->processAccountItems($order, $data['account_list'] ?? []); } @@ -552,7 +544,6 @@ class StockOrdersService $this->processVehiclePrices($item, $veh); // 累计到岸价 - // $cifTotalAmount += $veh['total_price']['amount'] ?? 0; $profitCurrency === '' ? $profitCurrency = $veh['profit']['currency'] ?? '' : ''; $profitRate === 0 ? $profitRate = $veh['profit']['rate'] ?? 0 : ''; $profitTotalAmount += $veh['profit']['amount'] ?? 0; @@ -572,7 +563,6 @@ class StockOrdersService $keysToRemove = ['vin', 'remark', 'modify_list', 'order_id']; $array = array_diff_key($veh, array_flip($keysToRemove)); $uniqueIdStr = json_encode($array); - return md5($uniqueIdStr); } @@ -600,6 +590,7 @@ class StockOrdersService // retention_price 留款/挂账 // car_price 车价 // accessory_price 配件费 + // increase_price 加价 // 基础费用 $priceFields = [ @@ -617,7 +608,8 @@ class StockOrdersService 'profit' => '利润', 'retention_price' => '挂账', 'car_price' => '车价', - 'accessory_price' => '配件费' + 'accessory_price' => '配件费', + 'increase_price' => '加价' ]; foreach ($priceFields as $field => $description) { @@ -771,12 +763,10 @@ class StockOrdersService { $vehList = []; $index = 0; - $currencyInfo = $this->getCurrencyInfo($item->settlement_currency); - foreach ($item->items as $v) { // 如果已存在相同车辆,增加数量 if (isset($vehList[$v->unique_id])) { - $this->incrementVehicleCount($vehList[$v->unique_id], $currencyInfo); + $this->incrementVehicleCount($vehList[$v->unique_id]); // 拼接 remark 值 if (!empty($v->remark)) { if (!empty($vehList[$v->unique_id]['remark'])) { @@ -787,17 +777,9 @@ class StockOrdersService } continue; } - $index++; // 处理金额信息 $moneyValues = $this->processItemMoneyValues($v); - - // 处理改装信息 - // $modificationText = $this->getModificationText($v->options); - - // 利润 - // $itemProfit = empty($moneyValues['profit']['amount']) ? 0 : $moneyValues['profit']['amount']; - // 构建车辆信息 $vehList[$v->unique_id] = [ 'index' => $index, @@ -805,19 +787,10 @@ class StockOrdersService 'color_interior_snapshot' => $v->color_interior_snapshot, 'color_exterior_snapshot' => $v->color_exterior_snapshot, 'num' => 1, - // 'modify' => $modificationText, 'mileage' => $v->mileage, 'remark' => $v->remark, ] + $moneyValues; - // var_dump($vehList[$v->unique_id]); - // 添加利润信息 - // $vehList[$v->unique_id]['profit'] = [ - // 'currency' => $item->settlement_currency, - // 'amount' => $itemProfit, - // 'currency_info' => $currencyInfo['currency_symbol'] . number_format($itemProfit, 2, '.', ',') - // ]; } - return ['list' => $vehList]; } @@ -853,23 +826,9 @@ class StockOrdersService /** * 增加车辆数量并更新相关金额显示 */ - private function incrementVehicleCount(array &$vehicleData, array $currencyInfo): void + private function incrementVehicleCount(array &$vehicleData): void { $vehicleData['num'] += 1; - - // // 更新总价显示 - // if (isset($vehicleData['total_price']['amount'])) { - // $totalAmount = $vehicleData['total_price']['amount'] * $vehicleData['num']; - // $vehicleData['total_price']['currency_info'] = $currencyInfo['currency_symbol'] . number_format($totalAmount, 2, '.', ','); - // } - - // // 更新利润显示 - // if (isset($vehicleData['profit']['amount'])) { - // $profitAmount = $vehicleData['profit']['amount'] * $vehicleData['num']; - // $vehicleData['profit']['currency_info'] = $currencyInfo['currency_symbol'] . number_format($profitAmount, 2, '.', ','); - // } - - // var_dump($vehicleData); $fieldList = [ 'fob_transport_fee', From c1ab40031dc49ab2eb965356188edc73184586df Mon Sep 17 00:00:00 2001 From: mzl Date: Fri, 24 Oct 2025 11:02:54 +0800 Subject: [PATCH 026/110] =?UTF-8?q?=E8=B0=83=E6=95=B4=E9=87=87=E8=B4=AD?= =?UTF-8?q?=E5=8D=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/common/logic/PurchaseOrdersLogic.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/common/logic/PurchaseOrdersLogic.php b/app/common/logic/PurchaseOrdersLogic.php index 500379d..0914f43 100644 --- a/app/common/logic/PurchaseOrdersLogic.php +++ b/app/common/logic/PurchaseOrdersLogic.php @@ -24,7 +24,7 @@ class PurchaseOrdersLogic public function getPiList() { return (new StockOrders())->where('pi_no', '!=', '') - ->select('pi_no') + ->select('pi_no', 'salesperson_snapshot') ->distinct() ->get(); } @@ -35,6 +35,7 @@ class PurchaseOrdersLogic public function getOrderNoListByPiNo($piNo) { return (new StockOrders())->where('pi_no', $piNo) + ->where('approval_status', 2) ->select('order_no', 'biz_date', 'salesperson_snapshot', 'id as stock_orders_id', 'salesperson_id') ->get(); } From 6bd4ed9ec32c826cb57a60187e0c4e10b932c6ab Mon Sep 17 00:00:00 2001 From: mzl Date: Fri, 24 Oct 2025 15:46:20 +0800 Subject: [PATCH 027/110] =?UTF-8?q?ts=E5=A4=87=E8=B4=A7=E5=8D=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/common/service/ApprovalRecordsService.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/common/service/ApprovalRecordsService.php b/app/common/service/ApprovalRecordsService.php index 3b37365..61c026e 100644 --- a/app/common/service/ApprovalRecordsService.php +++ b/app/common/service/ApprovalRecordsService.php @@ -56,9 +56,9 @@ class ApprovalRecordsService case 'todo': if ($p['scene'] === 'todo') { - // if (!in_array(1, $request->user->role_ids)) { - // $config['where']['approver_id'] = $request->user->uid; - // } + if (!in_array(1, $request->user->role_ids)) { + $config['where']['approver_id'] = $request->user->uid; + } $config['where']['status'] = 'pending'; $config['where']['instance.status'] = 'processing'; } From 83d76b16b4a154ae11ca388e904ea1c548ca6ede Mon Sep 17 00:00:00 2001 From: "WIN-C27C5MSVTPO\\16961" <1696136552@qq.com> Date: Mon, 27 Oct 2025 10:57:47 +0800 Subject: [PATCH 028/110] =?UTF-8?q?=E7=A1=AE=E8=AE=A4=E5=85=A5=E8=B4=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/FinReceiptController.php | 17 +++++++++++++++++ app/common/logic/FinReceiptLogic.php | 4 ++++ app/common/service/FinReceiptService.php | 19 +++++++++++++++++++ config/route.php | 1 + 4 files changed, 41 insertions(+) diff --git a/app/common/controller/FinReceiptController.php b/app/common/controller/FinReceiptController.php index 56d7bfb..486bfde 100644 --- a/app/common/controller/FinReceiptController.php +++ b/app/common/controller/FinReceiptController.php @@ -24,6 +24,23 @@ class FinReceiptController extends BaseController { $this->service = new FinReceiptService(); } + + /** + * 确认入账 + * @param Request $request + * @return \support\Response + */ + public function credit(Request $request) + { + try { + $id = $request->input('id'); + $raw = $this->service->credit($id); + return $this->success($raw); + } catch (\Throwable $e) { + return $this->error($e->getMessage()); + } + } + public function show(Request $request) { try { diff --git a/app/common/logic/FinReceiptLogic.php b/app/common/logic/FinReceiptLogic.php index 5d8f5f3..62db50f 100644 --- a/app/common/logic/FinReceiptLogic.php +++ b/app/common/logic/FinReceiptLogic.php @@ -14,4 +14,8 @@ class FinReceiptLogic */ protected string $modelClass = FinReceipt::class; + public function credit($id) + { + return FinReceipt::where('id', $id)->update(['received_at' => time()]); + } } \ No newline at end of file diff --git a/app/common/service/FinReceiptService.php b/app/common/service/FinReceiptService.php index 155aecd..e2611d7 100644 --- a/app/common/service/FinReceiptService.php +++ b/app/common/service/FinReceiptService.php @@ -2,10 +2,12 @@ namespace app\common\service; +use app\common\logic\FinReceiptLogic; use app\common\model\FinReceiptHis; use app\common\validate\FinReceiptValidate; use app\common\traits\crud\CrudServiceTrait; use app\common\model\FinReceipt; +use support\exception\BusinessException; use support\Request; class FinReceiptService @@ -23,6 +25,14 @@ class FinReceiptService */ protected $validator = FinReceiptValidate::class; + + protected $logic; + + public function __construct() + { + $this->logic = (new FinReceiptLogic()); + } + /** * 获取验证器 */ @@ -251,4 +261,13 @@ class FinReceiptService return $query->paginate($limit, ['*'], 'page', $page); } + + public function credit($id) + { + $finReceipt = FinReceipt::find($id); + if(!empty($finReceipt->received_at)){ + throw new BusinessException('该记录已将确认入账'); + } + return $this->logic->credit($id); + } } \ No newline at end of file diff --git a/config/route.php b/config/route.php index 3f7477f..ba10155 100644 --- a/config/route.php +++ b/config/route.php @@ -360,6 +360,7 @@ Route::group('/admin', function () { Route::any('/update', [app\common\controller\FinReceiptController::class, 'update']); Route::any('/payment', [app\common\controller\FinReceiptController::class, 'update']); Route::any('/destroy', [app\common\controller\FinReceiptController::class, 'destroy']); + Route::any('/credit', [app\common\controller\FinReceiptController::class, 'credit']); }); // 收款记录历史 Route::group('/receipt/his', function () { From 19cb169abdbd6365ae93382f19f5e600123df312 Mon Sep 17 00:00:00 2001 From: "WIN-C27C5MSVTPO\\16961" <1696136552@qq.com> Date: Mon, 27 Oct 2025 11:57:00 +0800 Subject: [PATCH 029/110] =?UTF-8?q?=E5=8E=86=E5=8F=B2=E8=AE=B0=E5=BD=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/common/service/FinReceiptService.php | 1 + 1 file changed, 1 insertion(+) diff --git a/app/common/service/FinReceiptService.php b/app/common/service/FinReceiptService.php index e2611d7..7154bfa 100644 --- a/app/common/service/FinReceiptService.php +++ b/app/common/service/FinReceiptService.php @@ -268,6 +268,7 @@ class FinReceiptService if(!empty($finReceipt->received_at)){ throw new BusinessException('该记录已将确认入账'); } + $this->afterServiceUpdate(['id'=>$id]); return $this->logic->credit($id); } } \ No newline at end of file From 1bd2eee8db7922ef3a8e40437a9e55a63043ff2c Mon Sep 17 00:00:00 2001 From: "WIN-C27C5MSVTPO\\16961" <1696136552@qq.com> Date: Mon, 27 Oct 2025 14:52:04 +0800 Subject: [PATCH 030/110] =?UTF-8?q?=E5=85=A5=E8=B4=A6=E6=97=B6=E9=97=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/common/service/FinReceiptService.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/common/service/FinReceiptService.php b/app/common/service/FinReceiptService.php index 7154bfa..bc9ff6a 100644 --- a/app/common/service/FinReceiptService.php +++ b/app/common/service/FinReceiptService.php @@ -268,7 +268,8 @@ class FinReceiptService if(!empty($finReceipt->received_at)){ throw new BusinessException('该记录已将确认入账'); } + $res = $this->logic->credit($id); $this->afterServiceUpdate(['id'=>$id]); - return $this->logic->credit($id); + return $res; } } \ No newline at end of file From 77dad7daed4639335465c56cb0a694a9a6cdce6b Mon Sep 17 00:00:00 2001 From: "WIN-C27C5MSVTPO\\16961" <1696136552@qq.com> Date: Tue, 28 Oct 2025 16:19:37 +0800 Subject: [PATCH 031/110] =?UTF-8?q?=E6=A8=A1=E7=B3=8A=E6=8A=93=E5=8F=96?= =?UTF-8?q?=E8=BD=A6=E5=9E=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/admin/service/BrandSeriesService.php | 147 +++++++++++------- .../controller/BrandSeriesController.php | 25 ++- app/common/service/CrawlService.php | 14 ++ config/route.php | 1 + 4 files changed, 129 insertions(+), 58 deletions(-) diff --git a/app/admin/service/BrandSeriesService.php b/app/admin/service/BrandSeriesService.php index f1526d3..0cf4901 100644 --- a/app/admin/service/BrandSeriesService.php +++ b/app/admin/service/BrandSeriesService.php @@ -168,50 +168,8 @@ class BrandSeriesService throw new \Exception('抓取失败,未找到该系列车型'); } - $client = new Client([ - 'verify' => false, // 禁用 SSL 验证 - 'timeout' => 10 - ]); try { - $response = $client->get('https://www.dongchedi.com/motor/pc/car/series/car_list', [ - 'query' => ['series_id' => $seriesId], - 'headers' => [ - 'User-Agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36' - ] - ]); - $data = json_decode($response->getBody()->getContents(), true); - if($data['status'] != 0 || (!isset($data['data']) && !isset($data['data']['tab_list']))){ - Log::error('抓取车型数据失败' . $data['message'], $data); - throw new \Exception('抓取车型数据失败' . $data['message']); - } - $models = []; - foreach ($data['data']['tab_list'] as $item) { - if($item['tab_text'] == '在售' || $item['tab_text'] == '停售'){ - unset($item['data'][0]); - $models = array_merge($models, $item['data']); - } - } - foreach ($models as $key => $model) { - if(!isset($model["info"]['car_name']) || $model['type'] != "1115"){ - unset($models[$key]); - continue; - } - $official_price = 0; - if(!isset($model["info"]['official_price'])){ - $official_price = (int)(str_replace("万", "", $model["info"]['official_price_str']) * 10000) ?? 0; - }else{ - $official_price = $model["info"]['official_price'] * 10000; - } - $models[$key] = [ - 'title' => $model["info"]['year'] . '款 ' .$model["info"]['car_name'], - 'brand_name' => $model["info"]['brand_name'], - 'series_name' => $model["info"]['series_name'], - 'year' => $model["info"]['year'], - 'msrp' => $official_price, - 'car_id' => $model["info"]['id'], - ]; - } - return array_values($models); + return $this->crawlSeriesModel($seriesId); } catch (\Exception $e) { throw new \Exception($e->getMessage()); } @@ -356,22 +314,101 @@ class BrandSeriesService /** - * 从车型名中提取年份 - * @param string $carModel 车型名称 - * @return string|null 提取到的年份或null + * 抓取车型数据(通过接口) + * @param Request $request + * @return array + * @throws \Exception */ - function extractYearFromCarModel($carModel) + public function crawlKeyword(Request $request): array { - // 匹配4位数字 + "款" 的模式 - if (preg_match('/(\d{4})款/', $carModel, $matches)) { - return $matches[1]; + $keyword = $request->get('keyword'); + + // 执行抓取 + $crawlService = new CrawlService(); + + //懂车帝模糊搜索返回车系地址 + $href = $crawlService->modelSearch($keyword); + $seriesId = $this->extractSeriesId($href); + + if (empty($seriesId)) { + throw new \Exception('抓取失败,未找到该系列车型'); } - // 匹配4位数字 + 空格或结尾的模式 - if (preg_match('/^(\d{4})(?=\s|$)/', $carModel, $matches)) { - return $matches[1]; - } + //抓取品牌与系列 + $brandSeries = $crawlService->crawlBrandSeries($seriesId); - return null; + + // 验证品牌是否存在 + $brand = Brand::where('brand_name', $brandSeries['brand'])->first(); + // 获取品牌信息 + $series = BrandSeries::where('series_name', $brandSeries['series'])->first(); + $result['brand'] = ['brand_name' => $brandSeries['brand'], 'exists' => $brand?1:0]; + $result['series'] = ['series_name' => $brandSeries['series'], 'exists' => $series?1:0]; + + try { + //获取车型数据 + $result['model'] = $this->crawlSeriesModel($seriesId); + return $result; + } catch (\Exception $e) { + throw new \Exception($e->getMessage()); + } + } + + /** + * 获取车系下车型数据 + * @return array + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function crawlSeriesModel($seriesId) + { + $client = new Client([ + 'verify' => false, // 禁用 SSL 验证 + 'timeout' => 10 + ]); + try { + $response = $client->get('https://www.dongchedi.com/motor/pc/car/series/car_list', [ + 'query' => ['series_id' => $seriesId], + 'headers' => [ + 'User-Agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36' + ] + ]); + $data = json_decode($response->getBody()->getContents(), true); + if($data['status'] != 0 || (!isset($data['data']) && !isset($data['data']['tab_list']))){ + Log::error('抓取车型数据失败' . $data['message'], $data); + throw new \Exception('抓取车型数据失败' . $data['message']); + } + $models = []; + foreach ($data['data']['tab_list'] as $item) { + if($item['tab_text'] == '在售' || $item['tab_text'] == '停售'){ + unset($item['data'][0]); + $models = array_merge($models, $item['data']); + } + } + foreach ($models as $key => $model) { + if(!isset($model["info"]['car_name']) || $model['type'] != "1115"){ + unset($models[$key]); + continue; + } + $official_price = 0; + if(!isset($model["info"]['official_price'])){ + $official_price = (int)(str_replace("万", "", $model["info"]['official_price_str']) * 10000) ?? 0; + }else{ + $official_price = $model["info"]['official_price'] * 10000; + } + $models[$key] = [ + 'title' => $model["info"]['year'] . '款 ' .$model["info"]['car_name'], + 'brand_name' => $model["info"]['brand_name'], + 'series_name' => $model["info"]['series_name'], + 'year' => $model["info"]['year'], + 'msrp' => $official_price, + 'car_id' => $model["info"]['id'], + ]; + } + + return array_values($models); + + } catch (\Exception $e) { + throw new \Exception($e->getMessage()); + } } } \ No newline at end of file diff --git a/app/common/controller/BrandSeriesController.php b/app/common/controller/BrandSeriesController.php index 00faa00..61c1b37 100644 --- a/app/common/controller/BrandSeriesController.php +++ b/app/common/controller/BrandSeriesController.php @@ -48,7 +48,7 @@ class BrandSeriesController extends BaseController } /** - * 异步抓取系列车型 + * 抓取系列车型 * @param Request $request * @return \support\Response */ @@ -58,7 +58,7 @@ class BrandSeriesController extends BaseController if($result){ return $this->success($result,'抓取成功'); }else{ - return $this->error('抓取任务提交失败'); + return $this->error('抓取任务失败'); } } catch (\Exception $e) { return $this->error($e->getMessage()); @@ -66,7 +66,7 @@ class BrandSeriesController extends BaseController } /** - * 异步抓取系列车型 + * 保存抓取的系列车型 * @param Request $request * @return \support\Response */ @@ -82,4 +82,23 @@ class BrandSeriesController extends BaseController return $this->error($e->getMessage()); } } + + + /** + * 关键词抓取品牌、车系、车型 + * @param Request $request + * @return \support\Response + */ + public function crawlKeyword(Request $request){ + try { + $result = $this->service->crawlKeyword($request); + if($result){ + return $this->success($result,'抓取成功'); + }else{ + return $this->error('抓取任务失败'); + } + } catch (\Exception $e) { + return $this->error($e->getMessage()); + } + } } \ No newline at end of file diff --git a/app/common/service/CrawlService.php b/app/common/service/CrawlService.php index a2c69d4..53e7026 100644 --- a/app/common/service/CrawlService.php +++ b/app/common/service/CrawlService.php @@ -73,4 +73,18 @@ class CrawlService }); return $href; } + + /** + * 关键词搜索的更多页面,通过面包屑抓取品牌与系列 + * @param $series + * @return mixed|string + */ + public function crawlBrandSeries($series) + { + $url = "https://www.dongchedi.com/auto/series/" . $series . "?anchorId=carModels"; + $ql = QueryList::get($url); + $result = $ql->find('div.breadcrumb_wrap__2GLeU span')->texts()->all(); + + return ['brand' => $result[2], 'series' => $result[3]]; + } } \ No newline at end of file diff --git a/config/route.php b/config/route.php index 2fafd16..6fa5100 100644 --- a/config/route.php +++ b/config/route.php @@ -109,6 +109,7 @@ Route::group('/common', function () { Route::any('/destroy', [app\common\controller\BrandSeriesController::class, 'destroy']); Route::any('/crawlModels', [app\common\controller\BrandSeriesController::class, 'crawlModels']); Route::any('/crawlModelCreate', [app\common\controller\BrandSeriesController::class, 'crawlModelCreate']); + Route::any('/crawlKeyword', [app\common\controller\BrandSeriesController::class, 'crawlKeyword']); }); }); From 21274732909ba0b3f7a5981309db574d9d9eb3b3 Mon Sep 17 00:00:00 2001 From: "WIN-C27C5MSVTPO\\16961" <1696136552@qq.com> Date: Tue, 28 Oct 2025 16:59:51 +0800 Subject: [PATCH 032/110] =?UTF-8?q?=E7=B3=BB=E7=BB=9F=E8=AE=BE=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/admin/controller/SysConfigController.php | 48 +++++++++++++ app/admin/logic/SysConfigLogic.php | 17 +++++ app/admin/service/SysConfigService.php | 76 ++++++++++++++++++++ app/admin/validate/SysConfigValidate.php | 29 ++++++++ app/common/logic/SysConfigLogic.php | 22 ++++++ app/common/model/SysConfig.php | 49 +++++++++++++ app/common/service/SysConfigService.php | 37 ++++++++++ 7 files changed, 278 insertions(+) create mode 100644 app/admin/controller/SysConfigController.php create mode 100644 app/admin/logic/SysConfigLogic.php create mode 100644 app/admin/service/SysConfigService.php create mode 100644 app/admin/validate/SysConfigValidate.php create mode 100644 app/common/logic/SysConfigLogic.php create mode 100644 app/common/model/SysConfig.php create mode 100644 app/common/service/SysConfigService.php diff --git a/app/admin/controller/SysConfigController.php b/app/admin/controller/SysConfigController.php new file mode 100644 index 0000000..3534079 --- /dev/null +++ b/app/admin/controller/SysConfigController.php @@ -0,0 +1,48 @@ +service = new SysConfigService(); + } + + /** + * 列表页数据格式化 + * + * @param array $data + * @return array + */ + protected function indexFormat($data) + { + if (isset($data['created_at'])) + $data['created_at'] = date('Y-m-d H:i:s', $data['created_at']); + if (isset($data['updated_at'])) + $data['updated_at'] = date('Y-m-d H:i:s', $data['updated_at']); + return $data; + } + + /** + * 数据格式化 + * + * @param array $data + * @return array + */ + protected function showFormat($data) + { + if (isset($data['created_at'])) + $data['created_at'] = date('Y-m-d H:i:s', $data['created_at']); + if (isset($data['updated_at'])) + $data['updated_at'] = date('Y-m-d H:i:s', $data['updated_at']); + return $data; + } +} \ No newline at end of file diff --git a/app/admin/logic/SysConfigLogic.php b/app/admin/logic/SysConfigLogic.php new file mode 100644 index 0000000..60be4a8 --- /dev/null +++ b/app/admin/logic/SysConfigLogic.php @@ -0,0 +1,17 @@ +validator; + } + + /** + * 构建查询条件 + */ + protected function buildQueryConfig(Request $request, string $scene = 'default'): array + { + $p = $request->all(); + + $config = []; + $config['model'] = $this->modelClass; + switch ($scene) { + case 'index': + // ====== ====== 排序 ====== ====== + $config['order']['id'] = 'desc'; + + // ====== ====== 筛选 ====== ====== + // 状态 + if (isset($p['status']) && in_array($p['status'], [0,1])) { + $config['where']['status'] = $p['status']; + } + // 关键字 + if (isset($p['keyword']) && $p['keyword']) { + $k = '%' . $p['keyword'] . '%'; + foreach (['config_key','config_value','description'] as $col) { + $config['orWhere'][] = [$col, 'like', $k]; + } + } + break; + case 'show': + $config['where']['id'] = $p['id']; + //$config['where']['status'] = 1; + break; + case 'store': + break; + case 'update': + $config['where']['id'] = $p['id']; + break; + case 'destroy': + $config['where']['id'] = $p['id']; + break; + } + + return $config; + } +} \ No newline at end of file diff --git a/app/admin/validate/SysConfigValidate.php b/app/admin/validate/SysConfigValidate.php new file mode 100644 index 0000000..2bb978a --- /dev/null +++ b/app/admin/validate/SysConfigValidate.php @@ -0,0 +1,29 @@ + [ + + ], + + 'update' => [ + + ] + ]; +} \ No newline at end of file diff --git a/app/common/logic/SysConfigLogic.php b/app/common/logic/SysConfigLogic.php new file mode 100644 index 0000000..633d177 --- /dev/null +++ b/app/common/logic/SysConfigLogic.php @@ -0,0 +1,22 @@ +where('config_key', $key)->get(['id', 'config_key', 'config_value']); + } +} \ No newline at end of file diff --git a/app/common/model/SysConfig.php b/app/common/model/SysConfig.php new file mode 100644 index 0000000..29b4577 --- /dev/null +++ b/app/common/model/SysConfig.php @@ -0,0 +1,49 @@ +logic = (new SysConfigLogic()); + } + + /** + * 获取系统list + */ + public static function getConfig(string $config_key) + { + $value = Redis::get($config_key); + if (empty($value)) { + $config = (new SysConfigLogic())->findByKey($config_key); + if (empty($config)) { + return null; + } + $value = $config->config_value; + Redis::set($config->config_key, $value, 'EX', 7200); + } + + return $value; + } +} \ No newline at end of file From 8a1b2f236936b8e943798db3b0742c7ad2648527 Mon Sep 17 00:00:00 2001 From: "WIN-C27C5MSVTPO\\16961" <1696136552@qq.com> Date: Tue, 28 Oct 2025 17:05:23 +0800 Subject: [PATCH 033/110] =?UTF-8?q?=E7=B3=BB=E7=BB=9F=E8=AE=BE=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/admin/service/SysConfigService.php | 7 +++++++ config/route.php | 9 +++++++++ 2 files changed, 16 insertions(+) diff --git a/app/admin/service/SysConfigService.php b/app/admin/service/SysConfigService.php index cedca5b..62e4812 100644 --- a/app/admin/service/SysConfigService.php +++ b/app/admin/service/SysConfigService.php @@ -5,6 +5,7 @@ namespace app\admin\service; use app\admin\validate\SysConfigValidate; use app\common\traits\crud\CrudServiceTrait; use app\common\model\SysConfig; +use support\Redis; use support\Request; class SysConfigService @@ -73,4 +74,10 @@ class SysConfigService return $config; } + + + public function beforeServiceUpdate($data, Request $request) + { + Redis::del($data['config_key']); + } } \ No newline at end of file diff --git a/config/route.php b/config/route.php index ba10155..29c4e6e 100644 --- a/config/route.php +++ b/config/route.php @@ -376,6 +376,15 @@ Route::group('/admin', function () { Route::any('/update', [app\common\controller\FinPaymentApplyController::class, 'update']); Route::any('/destroy', [app\common\controller\FinPaymentApplyController::class, 'destroy']); }); + + // 系统配置 + Route::group('/config', function () { + Route::any('/list', [app\admin\controller\SysConfigController::class, 'index']); + Route::any('/show', [app\admin\controller\SysConfigController::class, 'show']); + Route::any('/store', [app\admin\controller\SysConfigController::class, 'store']); + Route::any('/update', [app\admin\controller\SysConfigController::class, 'update']); + Route::any('/destroy', [app\admin\controller\SysConfigController::class, 'destroy']); + }); }); From 07c0b2a2f3ab249a6e48451b57e4cacd21ceed0a Mon Sep 17 00:00:00 2001 From: "WIN-C27C5MSVTPO\\16961" <1696136552@qq.com> Date: Wed, 29 Oct 2025 14:14:40 +0800 Subject: [PATCH 034/110] =?UTF-8?q?=E6=94=B6=E4=BB=98=E6=AC=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/FinPaymentApplyController.php | 2 ++ .../controller/FinReceiptController.php | 22 +++++++++++++++- app/common/model/FinPaymentApply.php | 2 +- app/common/service/FinReceiptService.php | 26 +++++++++++++++++++ config/route.php | 2 +- 5 files changed, 51 insertions(+), 3 deletions(-) diff --git a/app/common/controller/FinPaymentApplyController.php b/app/common/controller/FinPaymentApplyController.php index 57c4b75..37fc4d7 100644 --- a/app/common/controller/FinPaymentApplyController.php +++ b/app/common/controller/FinPaymentApplyController.php @@ -66,6 +66,8 @@ class FinPaymentApplyController extends BaseController $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['claimed_at'])) + $data['claimed_at'] = date('Y-m-d H:i:s', $data['claimed_at']); return $data; } diff --git a/app/common/controller/FinReceiptController.php b/app/common/controller/FinReceiptController.php index 486bfde..d8ef9c2 100644 --- a/app/common/controller/FinReceiptController.php +++ b/app/common/controller/FinReceiptController.php @@ -53,6 +53,24 @@ class FinReceiptController extends BaseController return $this->error($e->getMessage()); } } + + /** + * 更新 + * + * @param Request $request 请求对象 + * @return mixed 返回更新成功的数据或信息 + */ + public function payment(Request $request) + { + try { + $data = $this->validate($this->service->getValidator(), $request->post(), 'update'); + $res = $this->service->serviceUpdatePayment($request, $data); + return $this->success([], '更新成功'); + } catch (\Throwable $e) { + return $this->error($e->getMessage()); + } + } + /** * 列表页数据格式化 * @@ -61,8 +79,10 @@ class FinReceiptController extends BaseController */ protected function indexFormat($data) { - if (isset($data['created_at'])) + if (isset($data['created_at'])){ + $data['claim_timeout'] = (($data['created_at'] < (time()-86400)) && empty($data['payment_apply_id'])) ?1:0; $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['arrived_at'])) diff --git a/app/common/model/FinPaymentApply.php b/app/common/model/FinPaymentApply.php index f02f137..82f81cb 100644 --- a/app/common/model/FinPaymentApply.php +++ b/app/common/model/FinPaymentApply.php @@ -40,7 +40,7 @@ class FinPaymentApply extends Model /** * 允许自动填充模型字段 */ - protected $fillable = ['id', 'apply_no', 'doc_user_id', 'sales_user_id', 'corp_id', 'account_id', 'currency_id', 'amount', 'swift', 'remit_bank', 'country_id', 'remitter', 'exchange_rate', 'total_amount_cny', 'voucher_file', 'voucher_name', 'submit_files', 'created_at', 'updated_at']; + protected $fillable = ['id', 'apply_no', 'doc_user_id', 'sales_user_id', 'corp_id', 'account_id', 'payment_account', 'payment_entity', 'entity_type', 'currency_id', 'amount', 'swift', 'remit_bank', 'country_id', 'remitter', 'exchange_rate', 'total_amount_cny', 'voucher_file', 'voucher_name', 'submit_files','receipt_id','claimed_at', 'created_at', 'updated_at']; /** * 不允许自动填充模型字段 diff --git a/app/common/service/FinReceiptService.php b/app/common/service/FinReceiptService.php index bc9ff6a..6e7b7e3 100644 --- a/app/common/service/FinReceiptService.php +++ b/app/common/service/FinReceiptService.php @@ -3,6 +3,7 @@ namespace app\common\service; use app\common\logic\FinReceiptLogic; +use app\common\model\FinPaymentApply; use app\common\model\FinReceiptHis; use app\common\validate\FinReceiptValidate; use app\common\traits\crud\CrudServiceTrait; @@ -89,6 +90,7 @@ class FinReceiptService $config['where']['id'] = $p['id']; break; case 'update': + case 'payment': case 'destroy': $config['where']['id'] = $p['id']; break; @@ -272,4 +274,28 @@ class FinReceiptService $this->afterServiceUpdate(['id'=>$id]); return $res; } + + /** + * 更新计算折让金额 + * @param Request $request + * @param array $data + * @param string $scene + * @return mixed + */ + public function serviceUpdatePayment(Request $request, array $data = [], string $scene = 'payment') + { + $payment_apply_id = $request->input('payment_apply_id'); + $id = $request->input('id'); + $data = $this->preProcess(ucfirst($scene), $data, $request); + $config = $this->buildQueryConfig($request, $scene); + FinPaymentApply::where('id', $payment_apply_id)->update(['receipt_id' => $id, 'claimed_at' => time()]); + + //折让金额 + $payment = FinPaymentApply::where('id', $payment_apply_id)->first(); + $receipt = FinReceipt::find($id); + $data['discount_amount'] = $payment->amount - $payment->amount ?? 0; + + $updated = $this->logicUpdateBy($config, $data); + return $this->postProcess(ucfirst($scene), $data); + } } \ No newline at end of file diff --git a/config/route.php b/config/route.php index 29c4e6e..b962ee9 100644 --- a/config/route.php +++ b/config/route.php @@ -358,7 +358,7 @@ Route::group('/admin', function () { Route::any('/show', [app\common\controller\FinReceiptController::class, 'show']); Route::any('/store', [app\common\controller\FinReceiptController::class, 'store']); Route::any('/update', [app\common\controller\FinReceiptController::class, 'update']); - Route::any('/payment', [app\common\controller\FinReceiptController::class, 'update']); + Route::any('/payment', [app\common\controller\FinReceiptController::class, 'payment']); Route::any('/destroy', [app\common\controller\FinReceiptController::class, 'destroy']); Route::any('/credit', [app\common\controller\FinReceiptController::class, 'credit']); }); From b440c032410ad41e76b05dfd2a139be8c954fbc3 Mon Sep 17 00:00:00 2001 From: "WIN-C27C5MSVTPO\\16961" <1696136552@qq.com> Date: Wed, 29 Oct 2025 15:47:51 +0800 Subject: [PATCH 035/110] =?UTF-8?q?=E8=B6=85=E6=97=B6=E6=9C=AA=E8=AE=A4?= =?UTF-8?q?=E9=A2=86=E7=AD=9B=E9=80=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/common/service/FinReceiptService.php | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/app/common/service/FinReceiptService.php b/app/common/service/FinReceiptService.php index 6e7b7e3..48f9832 100644 --- a/app/common/service/FinReceiptService.php +++ b/app/common/service/FinReceiptService.php @@ -64,10 +64,13 @@ class FinReceiptService } // 没有付款记录的收款记录 - if (isset($p['payment_apply']) && $p['payment_apply'] == 'not') { + if (isset($p['payment_apply']) && $p['payment_apply'] == 'not') { //未认领 $config['where']['payment_apply_id'] = ['null']; - } elseif (isset($p['payment_apply']) && $p['payment_apply'] == 'already') { + } elseif (isset($p['payment_apply']) && $p['payment_apply'] == 'already') { //已认领 $config['where']['payment_apply_id'] = ['not null']; + } elseif (isset($p['payment_apply']) && $p['payment_apply'] == 'timeout') { //超时未认领 + $config['where']['payment_apply_id'] = ['not null']; + $config['where']['created_at'] = ['<' ,time() - 24*60*60]; } // 收款申请筛选 From cf45acbf0f7ae47a3bfa124dbb323dc4401eb024 Mon Sep 17 00:00:00 2001 From: "WIN-C27C5MSVTPO\\16961" <1696136552@qq.com> Date: Wed, 29 Oct 2025 15:58:23 +0800 Subject: [PATCH 036/110] =?UTF-8?q?=E5=85=A5=E8=B4=A6=E8=B5=84=E6=96=99?= =?UTF-8?q?=E6=97=B6=E9=97=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/common/controller/FinPaymentApplyController.php | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app/common/controller/FinPaymentApplyController.php b/app/common/controller/FinPaymentApplyController.php index 37fc4d7..3bf4b15 100644 --- a/app/common/controller/FinPaymentApplyController.php +++ b/app/common/controller/FinPaymentApplyController.php @@ -66,8 +66,10 @@ class FinPaymentApplyController extends BaseController $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['claimed_at'])) - $data['claimed_at'] = date('Y-m-d H:i:s', $data['claimed_at']); + if (isset($data['claimed_at'])){ + $data['enter_file_last'] = date('Y-m-d H:i:s', ($data['claimed_at']+86400)); //入账资料最后提交时间 + $data['claimed_at'] = date('Y-m-d H:i:s', $data['claimed_at']); //认领时间 + } return $data; } From 346d67c084c1dc6063d7680869c2f78b291213d4 Mon Sep 17 00:00:00 2001 From: "WIN-C27C5MSVTPO\\16961" <1696136552@qq.com> Date: Wed, 29 Oct 2025 18:05:23 +0800 Subject: [PATCH 037/110] =?UTF-8?q?=E8=B6=85=E8=BF=8724=E5=B0=8F=E6=97=B6?= =?UTF-8?q?=E9=82=AE=E4=BB=B6=E9=80=9A=E7=9F=A5=E6=8F=90=E4=BA=A4=E5=85=A5?= =?UTF-8?q?=E8=B4=A6=E8=B5=84=E6=96=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/common/model/FinPaymentApply.php | 2 +- app/common/service/FinReceiptService.php | 2 +- .../scaffold/templates/paymentFiles.stub | 103 ++++++++++++++++++ app/functions.php | 3 +- app/process/Task.php | 44 +++++++- 5 files changed, 149 insertions(+), 5 deletions(-) create mode 100644 app/common/utils/scaffold/templates/paymentFiles.stub diff --git a/app/common/model/FinPaymentApply.php b/app/common/model/FinPaymentApply.php index 82f81cb..0f4d157 100644 --- a/app/common/model/FinPaymentApply.php +++ b/app/common/model/FinPaymentApply.php @@ -40,7 +40,7 @@ class FinPaymentApply extends Model /** * 允许自动填充模型字段 */ - protected $fillable = ['id', 'apply_no', 'doc_user_id', 'sales_user_id', 'corp_id', 'account_id', 'payment_account', 'payment_entity', 'entity_type', 'currency_id', 'amount', 'swift', 'remit_bank', 'country_id', 'remitter', 'exchange_rate', 'total_amount_cny', 'voucher_file', 'voucher_name', 'submit_files','receipt_id','claimed_at', 'created_at', 'updated_at']; + protected $fillable = ['id', 'apply_no', 'doc_user_id', 'sales_user_id', 'corp_id', 'account_id', 'payment_account', 'payment_entity', 'entity_type', 'currency_id', 'amount', 'swift', 'remit_bank', 'country_id', 'remitter', 'exchange_rate', 'total_amount_cny', 'voucher_file', 'voucher_name', 'submit_files','receipt_id','claimed_at','warn', 'created_at', 'updated_at', 'claimed_by',]; /** * 不允许自动填充模型字段 diff --git a/app/common/service/FinReceiptService.php b/app/common/service/FinReceiptService.php index 48f9832..91db094 100644 --- a/app/common/service/FinReceiptService.php +++ b/app/common/service/FinReceiptService.php @@ -291,7 +291,7 @@ class FinReceiptService $id = $request->input('id'); $data = $this->preProcess(ucfirst($scene), $data, $request); $config = $this->buildQueryConfig($request, $scene); - FinPaymentApply::where('id', $payment_apply_id)->update(['receipt_id' => $id, 'claimed_at' => time()]); + FinPaymentApply::where('id', $payment_apply_id)->update(['receipt_id' => $id, 'claimed_at' => time(), 'claimed_by' => $request->user->uid]); //折让金额 $payment = FinPaymentApply::where('id', $payment_apply_id)->first(); diff --git a/app/common/utils/scaffold/templates/paymentFiles.stub b/app/common/utils/scaffold/templates/paymentFiles.stub new file mode 100644 index 0000000..6c9cded --- /dev/null +++ b/app/common/utils/scaffold/templates/paymentFiles.stub @@ -0,0 +1,103 @@ + + + + + + 邮箱提醒 + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/functions.php b/app/functions.php index 7ae0d0a..075dfbf 100644 --- a/app/functions.php +++ b/app/functions.php @@ -179,7 +179,8 @@ if (!function_exists('tplUrl')) { { $urlArray = [ 'email' => 'common/utils/scaffold/templates/Email.stub', - 'work_order' => 'common/utils/scaffold/templates/workOrder.stub' + 'work_order' => 'common/utils/scaffold/templates/workOrder.stub', + 'payment_files' => 'common/utils/scaffold/templates/paymentFiles.stub', ]; return $urlArray[$tplKey]; } diff --git a/app/process/Task.php b/app/process/Task.php index 910b5ad..47dbb93 100644 --- a/app/process/Task.php +++ b/app/process/Task.php @@ -2,6 +2,8 @@ namespace app\process; +use app\common\model\FinPaymentApply; +use app\common\model\User; use app\crm\model\Customer; use support\Log; use Workerman\Crontab\Crontab; @@ -11,7 +13,7 @@ class Task public function onWorkerStart() { - // 每半小时执行一次 + // 每半小时执行一次,通知待跟进客户 new Crontab('0 */30 * * * *', function(){ echo date('Y-m-d H:i:s') . "\n"; Log::info('send follow start',[time()]); @@ -34,7 +36,45 @@ class Task $item->save(); } - Log::info('send follow start',[time()]); + Log::info('send follow end',[time()]); + }); + + // 检查入账资料是否提交 + new Crontab('* */70 * * * *', function () { + $G = date('G'); + if ($G > 22 || $G < 8) { + return; + } + Log::info('fin_payment_apply submit_files start', [time()]); + $orWhere = [ + ['submit_files', '=', '[]'], + ['submit_files', '=', ''], + ]; + $where = [ + ['warn', '<', 2], + ['created_at', '>', time() - 60 * 60 * 48], + ['created_at', '<', time() - 60 * 60 * 24], + ]; + $payment = FinPaymentApply::where($where)->orWhere($orWhere)->orderBy('id', 'desc')->get(); + foreach ($payment as $item) { + Log::info('fin_payment_apply submit_files ', [$item->toArray()]); + $notice = [ + 'code' => $item->apply_no, + 'payment_entity' => $item->payment_entity ?? $item->remitter, + 'content' => '认领收款单后请在24小时内提交入账资料', + 'reply' => '', + ]; + $email = []; + $email[] = User::where('id', $item->claimed_by)->value('email'); + $email[] = User::where('id', $item->doc_user_id)->value('email'); + $email = array_unique($email); + \app\common\utils\email\MailSend::send($email, '【XYH】付款记录', replaceTpl($notice, 'payment_files')); + + $item->warn = ++$item->warn; + $item->save(); + } + + Log::info('fin_payment_apply submit_files end', [time()]); }); } From f3b1c9bae43074430c2a97972261f63ace668280 Mon Sep 17 00:00:00 2001 From: "WIN-C27C5MSVTPO\\16961" <1696136552@qq.com> Date: Wed, 29 Oct 2025 18:38:37 +0800 Subject: [PATCH 038/110] =?UTF-8?q?=E4=BB=98=E6=AC=BE=E8=AE=B0=E5=BD=95?= =?UTF-8?q?=E6=8E=92=E5=BA=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/common/service/FinPaymentApplyService.php | 27 +++++++++++-------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/app/common/service/FinPaymentApplyService.php b/app/common/service/FinPaymentApplyService.php index f451ce4..19a2f3f 100644 --- a/app/common/service/FinPaymentApplyService.php +++ b/app/common/service/FinPaymentApplyService.php @@ -174,11 +174,11 @@ class FinPaymentApplyService // 构建查询 $query = $this->modelClass::where($where); - // 应用是否有关联收款记录的筛选 + // 是否已关联收款记录的筛选 $correlation = $param['correlation']??''; - if ($correlation === 'receipt') { + if ($correlation === 'receipt') { //已关联 $query->whereHas('receipt'); - } elseif ($correlation === 'unbind') { + } elseif ($correlation === 'unbind') { //未关联 $query->whereDoesntHave('receipt'); } @@ -186,7 +186,7 @@ class FinPaymentApplyService return $query->paginate($limit, ['*'], 'page', $page); } - // 计算七天前的时间戳 + // 计算十天前的时间戳 $sevenDaysAgo = time() - (10 * 24 * 60 * 60); // 构建相似度排序逻辑 @@ -197,8 +197,18 @@ class FinPaymentApplyService $similarityOrder[] = "CASE WHEN account_id = {$param['account_id']} THEN 0 ELSE 1 END"; } - if (isset($param['country_id']) && !empty($param['country_id'])) { - $similarityOrder[] = "CASE WHEN country_id = {$param['country_id']} THEN 0 ELSE 1 END"; + if (isset($param['currency_id']) && !empty($param['currency_id'])) { + $similarityOrder[] = "CASE WHEN currency_id = {$param['currency_id']} THEN 0 ELSE 1 END"; + } + + if (isset($param['remitter']) && !empty($param['remitter'])) { + $similarityOrder[] = "CASE WHEN remitter = {$param['remitter']} THEN 0 ELSE 1 END"; + } + + if (isset($param['amount']) && !empty($param['amount'])) { + $amount = (float)$param['amount']; + $similarityOrder[] = "ABS(amount - {$amount}) ASC"; + $similarityOrder[] = "CASE WHEN (ABS(amount - {$amount}) / {$amount} * 100) <= 5 THEN 0 ELSE 1 END"; } if (isset($param['keyword']) && $param['keyword']) { @@ -206,11 +216,6 @@ class FinPaymentApplyService $similarityOrder[] = "CASE WHEN remitter LIKE '%{$keyword}%' THEN 0 ELSE 1 END"; } - if (isset($param['amount']) && !empty($param['amount'])) { - $amount = (float)$param['amount']; - $similarityOrder[] = "ABS(amount - {$amount})"; - } - // 构建排序SQL $orderByClause = ""; if (!empty($similarityOrder)) { From 9f944ae92cc86093b7b8e8bb750c4e088c8155f2 Mon Sep 17 00:00:00 2001 From: "WIN-C27C5MSVTPO\\16961" <1696136552@qq.com> Date: Thu, 30 Oct 2025 09:52:20 +0800 Subject: [PATCH 039/110] =?UTF-8?q?=E5=8C=B9=E9=85=8D=E6=8E=92=E5=BA=8F?= =?UTF-8?q?=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/common/service/FinPaymentApplyService.php | 38 ++++++++++--------- 1 file changed, 20 insertions(+), 18 deletions(-) diff --git a/app/common/service/FinPaymentApplyService.php b/app/common/service/FinPaymentApplyService.php index 19a2f3f..e5091fd 100644 --- a/app/common/service/FinPaymentApplyService.php +++ b/app/common/service/FinPaymentApplyService.php @@ -3,6 +3,7 @@ namespace app\common\service; use app\common\model\FinPaymentStock; +use app\common\model\FinReceipt; use app\common\validate\FinPaymentApplyValidate; use app\common\traits\crud\CrudServiceTrait; use app\common\model\FinPaymentApply; @@ -182,10 +183,16 @@ class FinPaymentApplyService $query->whereDoesntHave('receipt'); } - if (isset($param['scene']) && $param['scene'] == 'list') { - return $query->paginate($limit, ['*'], 'page', $page); + if ((isset($param['scene']) && $param['scene'] == 'list') || empty($param['receipt_id'])) { + return $query->orderBy('id','desc')->paginate($limit, ['*'], 'page', $page); } + $receipt = FinReceipt::where('id', $param['receipt_id'])->first(); + $account_id = $receipt->account_id; + $currency_id = $receipt->currency_id; + $remitter = $receipt->remitter; + $amount = $receipt->amount; + // 计算十天前的时间戳 $sevenDaysAgo = time() - (10 * 24 * 60 * 60); @@ -193,29 +200,24 @@ class FinPaymentApplyService $similarityOrder = []; // 如果有传入参数,计算相似度 - if (isset($param['account_id']) && !empty($param['account_id'])) { - $similarityOrder[] = "CASE WHEN account_id = {$param['account_id']} THEN 0 ELSE 1 END"; + if (!empty($account_id)) { + $similarityOrder[] = "CASE WHEN account_id = {$account_id} THEN 0 ELSE 1 END"; } - if (isset($param['currency_id']) && !empty($param['currency_id'])) { - $similarityOrder[] = "CASE WHEN currency_id = {$param['currency_id']} THEN 0 ELSE 1 END"; + if (!empty($currency_id)) { + $similarityOrder[] = "CASE WHEN currency_id = {$currency_id} THEN 0 ELSE 1 END"; } - if (isset($param['remitter']) && !empty($param['remitter'])) { - $similarityOrder[] = "CASE WHEN remitter = {$param['remitter']} THEN 0 ELSE 1 END"; + if (!empty($remitter)) { + $similarityOrder[] = "CASE WHEN remitter = {$remitter} THEN 0 ELSE 1 END"; } - if (isset($param['amount']) && !empty($param['amount'])) { - $amount = (float)$param['amount']; + if (!empty($amount)) { + $amount = (float)$amount; $similarityOrder[] = "ABS(amount - {$amount}) ASC"; $similarityOrder[] = "CASE WHEN (ABS(amount - {$amount}) / {$amount} * 100) <= 5 THEN 0 ELSE 1 END"; } - if (isset($param['keyword']) && $param['keyword']) { - $keyword = addslashes($param['keyword']); - $similarityOrder[] = "CASE WHEN remitter LIKE '%{$keyword}%' THEN 0 ELSE 1 END"; - } - // 构建排序SQL $orderByClause = ""; if (!empty($similarityOrder)) { @@ -227,11 +229,11 @@ class FinPaymentApplyService } $orderByClause .= "CASE - WHEN created_at < {$sevenDaysAgo} THEN id + WHEN created_at > {$sevenDaysAgo} THEN id ELSE 999999 - END DESC"; + END DESC, "; - $orderByClause .= ", id DESC"; + $orderByClause .= "id DESC"; $query->orderByRaw($orderByClause); From 672258e594d1f2634f81f890458ffcc0e4966a5a Mon Sep 17 00:00:00 2001 From: "WIN-C27C5MSVTPO\\16961" <1696136552@qq.com> Date: Thu, 30 Oct 2025 13:35:53 +0800 Subject: [PATCH 040/110] =?UTF-8?q?=E6=94=B6=E6=AC=BE=E7=BB=9F=E8=AE=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dashboard/FinStatisticsController.php | 44 +++++++++++++++++++ .../controller/FinReceiptController.php | 15 +++++++ app/common/service/FinReceiptService.php | 19 +++++++- config/route.php | 4 ++ 4 files changed, 80 insertions(+), 2 deletions(-) create mode 100644 app/admin/controller/dashboard/FinStatisticsController.php diff --git a/app/admin/controller/dashboard/FinStatisticsController.php b/app/admin/controller/dashboard/FinStatisticsController.php new file mode 100644 index 0000000..a3d3f9b --- /dev/null +++ b/app/admin/controller/dashboard/FinStatisticsController.php @@ -0,0 +1,44 @@ +',0); + + $result = []; + //已认领数量 + $result['claimed_num'] = $query->count(); + //已认领金额 + $result['claimed_amount'] = $query->sum('amount'); + + + $query = FinReceipt::whereNull('payment_apply_id'); + + //未认领数量 + $result['unclaimed_num'] = $query->count(); + //未认领金额 + $result['unclaimed_amount'] = $query->sum('amount'); + + //已超时未认领数量 + $result['timeout'] = $query->where('created_at','<',time()-24*3600)->count(); + + return $this->success($result); + } catch (\Throwable $e) { + return $this->error($e->getMessage()); + } + } + +} \ No newline at end of file diff --git a/app/common/controller/FinReceiptController.php b/app/common/controller/FinReceiptController.php index d8ef9c2..4e9c574 100644 --- a/app/common/controller/FinReceiptController.php +++ b/app/common/controller/FinReceiptController.php @@ -147,4 +147,19 @@ class FinReceiptController extends BaseController } return $data; } + + /** + * 收款记录统计 + * @param Request $request + * @return \support\Response + */ + public function statistics(Request $request) + { + try { + $raw = $this->service->statistics(); + return $this->success($raw); + } catch (\Throwable $e) { + return $this->error($e->getMessage()); + } + } } \ No newline at end of file diff --git a/app/common/service/FinReceiptService.php b/app/common/service/FinReceiptService.php index 91db094..1f1c731 100644 --- a/app/common/service/FinReceiptService.php +++ b/app/common/service/FinReceiptService.php @@ -279,7 +279,7 @@ class FinReceiptService } /** - * 更新计算折让金额 + * 更新:绑定收款记录,计算折让金额 * @param Request $request * @param array $data * @param string $scene @@ -296,9 +296,24 @@ class FinReceiptService //折让金额 $payment = FinPaymentApply::where('id', $payment_apply_id)->first(); $receipt = FinReceipt::find($id); - $data['discount_amount'] = $payment->amount - $payment->amount ?? 0; + $data['discount_amount'] = $payment->amount - $receipt->amount ?? 0; $updated = $this->logicUpdateBy($config, $data); return $this->postProcess(ucfirst($scene), $data); } + + /** + * 收款记录统计 + * @return bool|int + */ + public function statistics() + { + $finReceipt = FinReceipt::find($id); + if(!empty($finReceipt->received_at)){ + throw new BusinessException('该记录已将确认入账'); + } + $res = $this->logic->credit($id); + $this->afterServiceUpdate(['id'=>$id]); + return $res; + } } \ No newline at end of file diff --git a/config/route.php b/config/route.php index b962ee9..ec7d821 100644 --- a/config/route.php +++ b/config/route.php @@ -368,6 +368,10 @@ Route::group('/admin', function () { Route::any('/show', [app\common\controller\FinReceiptHisController::class, 'show']); }); + Route::group('/fin', function () { + Route::any('/statistics', [app\admin\controller\dashboard\FinStatisticsController::class, 'statistics']); + }); + // 付款申请 Route::group('/payment', function () { Route::any('/list', [app\common\controller\FinPaymentApplyController::class, 'index']); From 25f8017b3ca9e23af20f77c15b3d0f5ba7c53124 Mon Sep 17 00:00:00 2001 From: "WIN-C27C5MSVTPO\\16961" <1696136552@qq.com> Date: Thu, 30 Oct 2025 14:15:18 +0800 Subject: [PATCH 041/110] =?UTF-8?q?=E5=85=B3=E9=94=AE=E8=AF=8D=E6=8A=93?= =?UTF-8?q?=E5=8F=96=E8=BD=A6=E5=9E=8B=E5=90=8E=E5=88=9B=E5=BB=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/admin/service/BrandSeriesService.php | 100 +++++++++++++++++- .../controller/BrandSeriesController.php | 19 +++- config/route.php | 1 + 3 files changed, 118 insertions(+), 2 deletions(-) diff --git a/app/admin/service/BrandSeriesService.php b/app/admin/service/BrandSeriesService.php index 0cf4901..323e117 100644 --- a/app/admin/service/BrandSeriesService.php +++ b/app/admin/service/BrandSeriesService.php @@ -312,7 +312,6 @@ class BrandSeriesService return $msg; } - /** * 抓取车型数据(通过接口) * @param Request $request @@ -343,7 +342,16 @@ class BrandSeriesService // 获取品牌信息 $series = BrandSeries::where('series_name', $brandSeries['series'])->first(); $result['brand'] = ['brand_name' => $brandSeries['brand'], 'exists' => $brand?1:0]; + if($brand){ + $result['brand']['id'] = $brand['id']; + } $result['series'] = ['series_name' => $brandSeries['series'], 'exists' => $series?1:0]; + if($series){ + $result['series']['id'] = $series['id']; + if(empty($result['brand']['id'])){ + $result['brand']['id'] = $series['brand_id']; + } + } try { //获取车型数据 @@ -354,6 +362,96 @@ class BrandSeriesService } } + /** + * @param Request $request + * @return array|string + * @throws \Exception + */ + public function crawlKeywordCreate(Request $request) + { + $series_id = $request->input('series_id'); + $brand_id = $request->input('brand_id'); + $series_name = $request->input('series_name'); + $models = $request->input('models'); + + if(empty($brand_id)){ + throw new \Exception("请选择品牌"); + } + if(empty($series_id) && empty($series_name)){ + throw new \Exception("请选择车系"); + } + + if (empty($series_id)) { + $series = BrandSeries::where('brand_id', $brand_id)->where('series_name', $series_name)->first(); + if (empty($brandseries)) { //如果没有该车系则创建 + $seriesService = new \app\admin\service\BrandSeriesService(); + $seriesData = [ + 'brand_id' => $brand_id, + 'series_code' => $seriesService->generateUniqueModificationCode($brand_id), + 'series_name' => $series_name, + ]; + $series = CrudUtil::create(BrandSeries::class, $seriesData); + } + $series_id = $series->id; + }else{ + $series = BrandSeries::where('id', $series_id)->first(); + } + // 获取品牌信息 + $brand = Brand::where('id', $series->brand_id)->first(); + + // 保存车型数据 + $createdCount = 0; + $repeat = 0; + $fail = 0; + $msg = ''; + + foreach ($models as $item) { + $modelData = [ + 'brand_code' => $brand->brand_code, + 'brand_name' => $brand->brand_name, + 'series_code' => $series->series_code, + 'series_name' => $series->series_name, + 'model_name' => $item['title'], + 'model_year' => $item['year'], + 'msrp' => $item['msrp'], + ]; + //指纹数组 + $fingerprintArr = [ + 'brand_name' => $modelData['brand_name'], + 'series_name' => $modelData['series_name'], + 'model_name' => $modelData['model_name'], + 'model_year' => $modelData['model_year'], + 'msrp' => $modelData['msrp'], + ]; + // 生成指纹 + $modelData['fingerprint'] = Fingerprint::makeFromArray($fingerprintArr); + + $modelData['model_code'] = ModelNaming::buildModelCode($modelData); + $modelData['sku_code'] = SkuCodeBuilder::build($modelData); + + try { + $model = VehVehicleSku::where('brand_name', $brand->brand_name)->where('series_name', $series->series_name)->where('model_name', $item['title'])->first(); + if (!empty($model)) { + $repeat++; + continue; + } + CrudUtil::create(VehVehicleSku::class, $modelData); + $createdCount++; + } catch (\Exception $e) { + // 记录错误但继续处理其他车型 + $msg .= "创建车型失败: " . $e->getMessage() . "\n"; + Log::error("创建车型失败:" . $msg, ['series_id' => $series_id, 'model' => $item]); + $fail++; + } + } + + Log::info('series-crawl-' . $series_id . ' ' . $msg); + $msg = '创建车型成功' . $createdCount . '条'; + $msg .= (!empty($repeat)) ? ',重复' . $repeat . '条' : ''; + $msg .= (!empty($fail)) ? ',失败' . $fail . '条' : ''; + return $msg; + } + /** * 获取车系下车型数据 * @return array diff --git a/app/common/controller/BrandSeriesController.php b/app/common/controller/BrandSeriesController.php index 61c1b37..3cd32d3 100644 --- a/app/common/controller/BrandSeriesController.php +++ b/app/common/controller/BrandSeriesController.php @@ -83,7 +83,6 @@ class BrandSeriesController extends BaseController } } - /** * 关键词抓取品牌、车系、车型 * @param Request $request @@ -101,4 +100,22 @@ class BrandSeriesController extends BaseController return $this->error($e->getMessage()); } } + + /** + * 保存抓取的系列车型 + * @param Request $request + * @return \support\Response + */ + public function crawlKeywordCreate(Request $request){ + try { + $result = $this->service->crawlKeywordCreate($request); + if($result){ + return $this->success($result,'成功'); + }else{ + return $this->error('创建失败'); + } + } catch (\Exception $e) { + return $this->error($e->getMessage()); + } + } } \ No newline at end of file diff --git a/config/route.php b/config/route.php index 6fa5100..fb407ac 100644 --- a/config/route.php +++ b/config/route.php @@ -110,6 +110,7 @@ Route::group('/common', function () { Route::any('/crawlModels', [app\common\controller\BrandSeriesController::class, 'crawlModels']); Route::any('/crawlModelCreate', [app\common\controller\BrandSeriesController::class, 'crawlModelCreate']); Route::any('/crawlKeyword', [app\common\controller\BrandSeriesController::class, 'crawlKeyword']); + Route::any('/crawlKeywordCreate', [app\common\controller\BrandSeriesController::class, 'crawlKeywordCreate']); }); }); From ea3d511c4d486e56b133a7e0c9b84074a480c97d Mon Sep 17 00:00:00 2001 From: "WIN-C27C5MSVTPO\\16961" <1696136552@qq.com> Date: Thu, 30 Oct 2025 14:26:59 +0800 Subject: [PATCH 042/110] =?UTF-8?q?=E6=9C=AA=E5=85=A5=E8=B4=A6=E9=87=91?= =?UTF-8?q?=E9=A2=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/admin/controller/dashboard/FinStatisticsController.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/admin/controller/dashboard/FinStatisticsController.php b/app/admin/controller/dashboard/FinStatisticsController.php index a3d3f9b..95cf09d 100644 --- a/app/admin/controller/dashboard/FinStatisticsController.php +++ b/app/admin/controller/dashboard/FinStatisticsController.php @@ -24,6 +24,10 @@ class FinStatisticsController extends BaseController //已认领金额 $result['claimed_amount'] = $query->sum('amount'); + //未入账数量 + $result['unreceived_num'] = FinReceipt::whereNull('received_at')->count(); + //未入账金额 + $result['unreceived_amount'] = FinReceipt::whereNull('received_at')->sum('amount'); $query = FinReceipt::whereNull('payment_apply_id'); From a7d623656d18da446b29c81efd3295e7e7f912e3 Mon Sep 17 00:00:00 2001 From: mzl Date: Thu, 30 Oct 2025 14:37:21 +0800 Subject: [PATCH 043/110] =?UTF-8?q?=E5=86=85=E5=8B=A4=E4=BB=AA=E8=A1=A8?= =?UTF-8?q?=E7=9B=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dashboard/OfficeClerkController.php | 164 ++++++++++++++++++ config/route.php | 11 ++ 2 files changed, 175 insertions(+) create mode 100644 app/admin/controller/dashboard/OfficeClerkController.php diff --git a/app/admin/controller/dashboard/OfficeClerkController.php b/app/admin/controller/dashboard/OfficeClerkController.php new file mode 100644 index 0000000..2bd2bcc --- /dev/null +++ b/app/admin/controller/dashboard/OfficeClerkController.php @@ -0,0 +1,164 @@ +where('approver_id', $request->user->uid) + ->where('status', 'approved') + ->count('id'); + + $pending = Db::table('approval_records') + ->where('approver_id', $request->user->uid) + ->where('status', 'pending') + ->count('id'); + + $unclaimed = Db::table('fin_payment_apply') + ->where('doc_user_id', $request->user->uid) + ->where('receipt_id', 0) + ->count('id'); + + $claimed = Db::table('fin_payment_apply') + ->where('doc_user_id', $request->user->uid) + ->where('receipt_id', '>', 0) + ->count('id'); + + $twentyFourHoursAgo = time() - (24 * 3600); + $timeout = Db::table('fin_payment_apply') + ->where('doc_user_id', $request->user->uid) + ->whereNotNull('submit_files') + ->whereJsonLength('submit_files', '>', 0) + ->where('receipt_id', '>', 0) + ->where('claimed_at', '<', $twentyFourHoursAgo) + ->count('id'); + + return $this->success([ + 'approved' => $approved, + 'pending' => $pending, + 'unclaimed' => $unclaimed, + 'claimed' => $claimed, + 'timeout' => $timeout + ]); + } + + // 曲线 + public function curve() + { + $groupIds = Db::table('user_group') + ->where('scenario_code', 'stock_orders') + ->pluck('id'); + $userIds = Db::table('user_group_members') + ->whereIn('group_id', $groupIds) + ->pluck('user_id'); + $list = Db::table('stock_orders') + ->join('user', 'stock_orders.salesperson_id', '=', 'user.id') + ->whereIn('stock_orders.salesperson_id', $userIds) + ->whereIn('stock_orders.approval_status', [0, 1, 2]) + ->whereNull('stock_orders.deleted_at') + ->groupBy('stock_orders.salesperson_id', 'user.name') + ->selectRaw('user.name, count(*) as count') + ->pluck('count', 'name') + ->toArray(); + $optionType = []; + $counts = []; + foreach ($list as $key=>$val) { + $optionType[] = $key; + $counts[] = $val; + } + $arr = [ + "optionType" => $optionType, + "counts" => $counts + ]; + return $this->success($arr); + } + + // 审批 + public function approval(Request $request) + { + $list = Db::table('approval_records as ar') + ->join('approval_instances as ai', 'ar.instance_id', '=', 'ai.id') + ->join('stock_orders as so', 'ai.biz_id', '=', 'so.id') + ->where('ar.approver_id', $request->user->uid) + ->where('ar.status', 'pending') + ->where('ai.flow_type', 'stock_orders') + ->where('ai.biz_type', 'stock_orders') + ->where('ai.status', 'processing') + ->whereNull('so.deleted_at') + ->select( + 'so.order_no', + 'ai.created_at as approval_created_at', + 'so.salesperson_snapshot as salesperson_name' + ) + ->get() + ->map(function($item) { + return [ + 'order_no' => $item->order_no, + 'approval_created_at' => date('Y-m-d H:i:s', strtotime($item->approval_created_at)), // 格式化时间 + 'salesperson_name' => $item->salesperson_name + ]; + }) + ->toArray(); + return $this->success($list); + } + + // 认领 + public function claim(Request $request) + { + $type = $request->get('type'); + if ($type == 'unclaimed') { + $list = Db::table('fin_payment_apply') + ->where('doc_user_id', $request->user->uid) + ->where('receipt_id', 0) + ->select( + 'apply_no', + 'amount', + 'created_at' + ) + ->get() + ->map(function($item) { + return [ + 'apply_no' => $item->apply_no, + 'amount' => $item->amount, + 'created_at' => date('Y-m-d H:i:s', $item->created_at) + ]; + }) + ->toArray(); + } else { + $twentyFourHoursAgo = time() - (24 * 3600); + $list = Db::table('fin_payment_apply') + ->where('doc_user_id', $request->user->uid) + ->whereNotNull('submit_files') + ->whereJsonLength('submit_files', '>', 0) + ->where('receipt_id', '>', 0) + ->where('claimed_at', '<', $twentyFourHoursAgo) + ->select( + 'apply_no', + 'amount', + 'created_at' + ) + ->get() + ->map(function($item) { + return [ + 'apply_no' => $item->apply_no, + 'amount' => $item->amount, + 'created_at' => date('Y-m-d H:i:s', $item->created_at) + ]; + }) + ->toArray(); + } + return $this->success($list); + } +} \ No newline at end of file diff --git a/config/route.php b/config/route.php index 2fafd16..a19797e 100644 --- a/config/route.php +++ b/config/route.php @@ -422,6 +422,17 @@ Route::group('/admin', function () { Route::any('/update', [app\common\controller\SysUpdateLogController::class, 'update']); Route::any('/destroy', [app\common\controller\SysUpdateLogController::class, 'destroy']); }); + + // 仪表盘 + Route::group('/dashboard', function () { + // 内勤 + Route::group('/officeclerk', function () { + Route::any('/card', [app\admin\controller\dashboard\OfficeClerkController::class, 'card']); + Route::any('/curve', [app\admin\controller\dashboard\OfficeClerkController::class, 'curve']); + Route::any('/approval', [app\admin\controller\dashboard\OfficeClerkController::class, 'approval']); + Route::any('/claim', [app\admin\controller\dashboard\OfficeClerkController::class, 'claim']); + }); + }); }); From 8c3a445a39b1d7c54d061966445f80a461efc3cc Mon Sep 17 00:00:00 2001 From: mzl Date: Thu, 30 Oct 2025 14:39:10 +0800 Subject: [PATCH 044/110] Merge branch 'develop' into feature/mzl_dev # Conflicts: # config/route.php --- app/admin/controller/SysConfigController.php | 48 +++ .../dashboard/FinStatisticsController.php | 48 +++ app/admin/logic/SysConfigLogic.php | 17 + app/admin/service/SysConfigService.php | 83 +++++ app/admin/validate/SysConfigValidate.php | 29 ++ .../controller/FinPaymentApplyController.php | 128 +++++++ .../controller/FinReceiptController.php | 165 +++++++++ .../controller/FinReceiptHisController.php | 98 ++++++ app/common/controller/UploadController.php | 7 +- app/common/logic/FinPaymentApplyLogic.php | 17 + app/common/logic/FinReceiptHisLogic.php | 17 + app/common/logic/FinReceiptLogic.php | 21 ++ app/common/logic/SysConfigLogic.php | 22 ++ app/common/model/FinPaymentApply.php | 65 ++++ app/common/model/FinPaymentStock.php | 67 ++++ app/common/model/FinReceipt.php | 65 ++++ app/common/model/FinReceiptHis.php | 49 +++ app/common/model/StockOrders.php | 16 + app/common/model/SysConfig.php | 49 +++ app/common/service/FinPaymentApplyService.php | 243 +++++++++++++ app/common/service/FinReceiptHisService.php | 62 ++++ app/common/service/FinReceiptService.php | 319 ++++++++++++++++++ app/common/service/OssService.php | 34 ++ app/common/service/SysConfigService.php | 37 ++ app/common/service/SysOpLogService.php | 2 +- .../scaffold/templates/paymentFiles.stub | 103 ++++++ .../validate/FinPaymentApplyValidate.php | 29 ++ app/common/validate/FinReceiptHisValidate.php | 29 ++ app/common/validate/FinReceiptValidate.php | 56 +++ app/functions.php | 3 +- app/process/Task.php | 44 ++- config/route.php | 39 ++- 32 files changed, 2004 insertions(+), 7 deletions(-) create mode 100644 app/admin/controller/SysConfigController.php create mode 100644 app/admin/controller/dashboard/FinStatisticsController.php create mode 100644 app/admin/logic/SysConfigLogic.php create mode 100644 app/admin/service/SysConfigService.php create mode 100644 app/admin/validate/SysConfigValidate.php create mode 100644 app/common/controller/FinPaymentApplyController.php create mode 100644 app/common/controller/FinReceiptController.php create mode 100644 app/common/controller/FinReceiptHisController.php create mode 100644 app/common/logic/FinPaymentApplyLogic.php create mode 100644 app/common/logic/FinReceiptHisLogic.php create mode 100644 app/common/logic/FinReceiptLogic.php create mode 100644 app/common/logic/SysConfigLogic.php create mode 100644 app/common/model/FinPaymentApply.php create mode 100644 app/common/model/FinPaymentStock.php create mode 100644 app/common/model/FinReceipt.php create mode 100644 app/common/model/FinReceiptHis.php create mode 100644 app/common/model/SysConfig.php create mode 100644 app/common/service/FinPaymentApplyService.php create mode 100644 app/common/service/FinReceiptHisService.php create mode 100644 app/common/service/FinReceiptService.php create mode 100644 app/common/service/SysConfigService.php create mode 100644 app/common/utils/scaffold/templates/paymentFiles.stub create mode 100644 app/common/validate/FinPaymentApplyValidate.php create mode 100644 app/common/validate/FinReceiptHisValidate.php create mode 100644 app/common/validate/FinReceiptValidate.php diff --git a/app/admin/controller/SysConfigController.php b/app/admin/controller/SysConfigController.php new file mode 100644 index 0000000..3534079 --- /dev/null +++ b/app/admin/controller/SysConfigController.php @@ -0,0 +1,48 @@ +service = new SysConfigService(); + } + + /** + * 列表页数据格式化 + * + * @param array $data + * @return array + */ + protected function indexFormat($data) + { + if (isset($data['created_at'])) + $data['created_at'] = date('Y-m-d H:i:s', $data['created_at']); + if (isset($data['updated_at'])) + $data['updated_at'] = date('Y-m-d H:i:s', $data['updated_at']); + return $data; + } + + /** + * 数据格式化 + * + * @param array $data + * @return array + */ + protected function showFormat($data) + { + if (isset($data['created_at'])) + $data['created_at'] = date('Y-m-d H:i:s', $data['created_at']); + if (isset($data['updated_at'])) + $data['updated_at'] = date('Y-m-d H:i:s', $data['updated_at']); + return $data; + } +} \ No newline at end of file diff --git a/app/admin/controller/dashboard/FinStatisticsController.php b/app/admin/controller/dashboard/FinStatisticsController.php new file mode 100644 index 0000000..95cf09d --- /dev/null +++ b/app/admin/controller/dashboard/FinStatisticsController.php @@ -0,0 +1,48 @@ +',0); + + $result = []; + //已认领数量 + $result['claimed_num'] = $query->count(); + //已认领金额 + $result['claimed_amount'] = $query->sum('amount'); + + //未入账数量 + $result['unreceived_num'] = FinReceipt::whereNull('received_at')->count(); + //未入账金额 + $result['unreceived_amount'] = FinReceipt::whereNull('received_at')->sum('amount'); + + $query = FinReceipt::whereNull('payment_apply_id'); + + //未认领数量 + $result['unclaimed_num'] = $query->count(); + //未认领金额 + $result['unclaimed_amount'] = $query->sum('amount'); + + //已超时未认领数量 + $result['timeout'] = $query->where('created_at','<',time()-24*3600)->count(); + + return $this->success($result); + } catch (\Throwable $e) { + return $this->error($e->getMessage()); + } + } + +} \ No newline at end of file diff --git a/app/admin/logic/SysConfigLogic.php b/app/admin/logic/SysConfigLogic.php new file mode 100644 index 0000000..60be4a8 --- /dev/null +++ b/app/admin/logic/SysConfigLogic.php @@ -0,0 +1,17 @@ +validator; + } + + /** + * 构建查询条件 + */ + protected function buildQueryConfig(Request $request, string $scene = 'default'): array + { + $p = $request->all(); + + $config = []; + $config['model'] = $this->modelClass; + switch ($scene) { + case 'index': + // ====== ====== 排序 ====== ====== + $config['order']['id'] = 'desc'; + + // ====== ====== 筛选 ====== ====== + // 状态 + if (isset($p['status']) && in_array($p['status'], [0,1])) { + $config['where']['status'] = $p['status']; + } + // 关键字 + if (isset($p['keyword']) && $p['keyword']) { + $k = '%' . $p['keyword'] . '%'; + foreach (['config_key','config_value','description'] as $col) { + $config['orWhere'][] = [$col, 'like', $k]; + } + } + break; + case 'show': + $config['where']['id'] = $p['id']; + //$config['where']['status'] = 1; + break; + case 'store': + break; + case 'update': + $config['where']['id'] = $p['id']; + break; + case 'destroy': + $config['where']['id'] = $p['id']; + break; + } + + return $config; + } + + + public function beforeServiceUpdate($data, Request $request) + { + Redis::del($data['config_key']); + } +} \ No newline at end of file diff --git a/app/admin/validate/SysConfigValidate.php b/app/admin/validate/SysConfigValidate.php new file mode 100644 index 0000000..2bb978a --- /dev/null +++ b/app/admin/validate/SysConfigValidate.php @@ -0,0 +1,29 @@ + [ + + ], + + 'update' => [ + + ] + ]; +} \ No newline at end of file diff --git a/app/common/controller/FinPaymentApplyController.php b/app/common/controller/FinPaymentApplyController.php new file mode 100644 index 0000000..3bf4b15 --- /dev/null +++ b/app/common/controller/FinPaymentApplyController.php @@ -0,0 +1,128 @@ +service = new FinPaymentApplyService(); + } + + /** + * 列表页数据格式化 + * + * @param array $data + * @return array + */ + protected function indexFormat($data) + { + if (isset($data['doc_user_id'])) //内勤 + $data['doc_user_name'] = User::where('id', $data['doc_user_id'])->value('name')??''; + if (isset($data['sales_user_id'])) //业务员 + $data['sales_user_name'] = User::where('id', $data['doc_user_id'])->value('name')??''; + if (isset($data['corp_id'])) //公司(渠道) + $data['corp_name'] = SysDictItem::where('id', $data['corp_id'])->value('item_label')??''; + if (isset($data['account_id'])){ //收款账号 + $data['account_name'] = SysAccount::where('id', $data['account_id'])->value('account_name')??''; + $data['account_short_desc'] = SysAccount::where('id', $data['account_id'])->value('short_desc')??''; + } + if (isset($data['country_id'])) //付款来源国家 + $data['country_name'] = Country::where('id', $data['country_id'])->value('cn_addr')??''; + if (isset($data['currency_id'])) //付款币种 + $data['currency_name'] = SysCurrency::where('id', $data['currency_id'])->value('currency_name')??''; + $OssService = new OssService(); + if (isset($data['submit_files'])){ + $data['submit_files'] = json_decode($data['submit_files'], true); + foreach ($data['submit_files'] as $key => $value) { + $data['submit_files'][$key] = [ + 'file_src' => $value['file_src'], + 'file_full' => $OssService->generatePrivateUrl($value['file_src']), + 'file_name' => $value['file_name'], + ]; + } + } + if (!empty($data['voucher_file'])) { + $data['voucher_file_full'] = $OssService->generatePrivateUrl($data['voucher_file']); + } elseif (isset($data['voucher_file'])) { + $data['voucher_file_full'] = ''; + } + 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['claimed_at'])){ + $data['enter_file_last'] = date('Y-m-d H:i:s', ($data['claimed_at']+86400)); //入账资料最后提交时间 + $data['claimed_at'] = date('Y-m-d H:i:s', $data['claimed_at']); //认领时间 + } + return $data; + } + + /** + * 数据格式化 + * + * @param array $data + * @return array + */ + public function showFormat($data) + { + if (isset($data['doc_user_id'])) //内勤 + $data['doc_user_name'] = User::where('id', $data['doc_user_id'])->value('name')??''; + if (isset($data['sales_user_id'])) //业务员 + $data['sales_user_name'] = User::where('id', $data['doc_user_id'])->value('name')??''; + if (isset($data['corp_id'])) //公司(渠道) + $data['corp_name'] = SysDictItem::where('id', $data['corp_id'])->value('item_label')??''; + if (isset($data['account_id'])){ //收款账号 + $data['account_name'] = SysAccount::where('id', $data['account_id'])->value('account_name')??''; + $data['account_short_desc'] = SysAccount::where('id', $data['account_id'])->value('short_desc')??''; + } + if (isset($data['country_id'])) //付款来源国家 + $data['country_name'] = Country::where('id', $data['country_id'])->value('cn_addr')??''; + if (isset($data['currency_id'])) //付款币种 + $data['currency_name'] = SysCurrency::where('id', $data['currency_id'])->value('currency_name')??''; + $OssService = new OssService(); + if (isset($data['submit_files'])){ + $data['submit_files'] = json_decode($data['submit_files'], true); + foreach ($data['submit_files'] as $key => $value) { + $data['submit_files'][$key] = [ + 'file_src' => $value['file_src'], + 'file_full' => $OssService->generatePrivateUrl($value['file_src']), + 'file_name' => $value['file_name'], + ]; + } + } + if (!empty($data['voucher_file'])) { + $data['voucher_file_full'] = $OssService->generatePrivateUrl($data['voucher_file']); + } elseif (isset($data['voucher_file'])) { + $data['voucher_file_full'] = ''; + } + 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']); + + $finPaymentStock = FinPaymentStock::where('payment_apply_id', $data['id'])->get(); + $stock = []; + foreach ($finPaymentStock as $v) { + $stock[$v['pi_no']]['pi_no'] = $v['pi_no']; + $stock[$v['pi_no']]['stock_id'][] = [$v['stock_id']]; + } + $data['stock'] = array_values($stock); + return $data; + } +} \ No newline at end of file diff --git a/app/common/controller/FinReceiptController.php b/app/common/controller/FinReceiptController.php new file mode 100644 index 0000000..4e9c574 --- /dev/null +++ b/app/common/controller/FinReceiptController.php @@ -0,0 +1,165 @@ +service = new FinReceiptService(); + } + + /** + * 确认入账 + * @param Request $request + * @return \support\Response + */ + public function credit(Request $request) + { + try { + $id = $request->input('id'); + $raw = $this->service->credit($id); + return $this->success($raw); + } catch (\Throwable $e) { + return $this->error($e->getMessage()); + } + } + + public function show(Request $request) + { + try { + $raw = $this->service->serviceShow($request); + $data = $this->transformData($raw, method_exists($this, 'showFormat') + ? [$this, 'showFormat'] + : null); + return $this->success($data); + } catch (\Throwable $e) { + return $this->error($e->getMessage()); + } + } + + /** + * 更新 + * + * @param Request $request 请求对象 + * @return mixed 返回更新成功的数据或信息 + */ + public function payment(Request $request) + { + try { + $data = $this->validate($this->service->getValidator(), $request->post(), 'update'); + $res = $this->service->serviceUpdatePayment($request, $data); + return $this->success([], '更新成功'); + } catch (\Throwable $e) { + return $this->error($e->getMessage()); + } + } + + /** + * 列表页数据格式化 + * + * @param array $data + * @return array + */ + protected function indexFormat($data) + { + if (isset($data['created_at'])){ + $data['claim_timeout'] = (($data['created_at'] < (time()-86400)) && empty($data['payment_apply_id'])) ?1:0; + $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['arrived_at'])) + $data['arrived_at'] = date('Y-m-d H:i:s', $data['arrived_at']); + if (isset($data['received_at'])) + $data['received_at'] = date('Y-m-d H:i:s', $data['received_at']); + if (isset($data['account_id'])) { //收款账号 + $data['account_name'] = SysAccount::where('id', $data['account_id'])->value('account_name')??''; + $data['account_short_desc'] = SysAccount::where('id', $data['account_id'])->value('short_desc')??''; + } + if (isset($data['country_id'])) //付款来源国家 + $data['country_name'] = Country::where('id', $data['country_id'])->value('cn_addr')??''; + if (isset($data['currency_id'])) //付款币种 + $data['currency_name'] = SysCurrency::where('id', $data['currency_id'])->value('currency_name')??''; + if (!empty($data['fx_receipt_file'])){ + $data['fx_receipt_file_full'] = (new OssService)->generatePrivateUrl($data['fx_receipt_file']); + }elseif (isset($data['fx_receipt_file'])){ + $data['fx_receipt_file_full'] = ''; + } + return $data; + } + + /** + * 数据格式化 + * + * @param array $data + * @return array + */ + protected function showFormat($data) + { + if (isset($data['created_at'])) + $data['created_at'] = date('Y-m-d H:i:s', $data['created_at']); + if (isset($data['updated_at'])) + $data['updated_at'] = date('Y-m-d H:i:s', $data['updated_at']); + if (isset($data['arrived_at'])) + $data['arrived_at'] = date('Y-m-d H:i:s', $data['arrived_at']); + if (isset($data['received_at'])) + $data['received_at'] = date('Y-m-d H:i:s', $data['received_at']); + if (isset($data['account_id'])) { //收款账号 + $data['account_name'] = SysAccount::where('id', $data['account_id'])->value('account_name')??''; + $data['account_short_desc'] = SysAccount::where('id', $data['account_id'])->value('short_desc')??''; + } + if (isset($data['country_id'])) //付款来源国家 + $data['country_name'] = Country::where('id', $data['country_id'])->value('cn_addr')??''; + if (isset($data['currency_id'])) //付款币种 + $data['currency_name'] = SysCurrency::where('id', $data['currency_id'])->value('currency_name')??''; + if (!empty($data['fx_receipt_file'])) { + $data['fx_receipt_file_full'] = (new OssService)->generatePrivateUrl($data['fx_receipt_file']); + } elseif (isset($data['fx_receipt_file'])) { + $data['fx_receipt_file_full'] = ''; + } + + if (!empty($data['payment_apply_id'])) { + $paymentApply = FinPaymentApply::where('id', $data['payment_apply_id'])->first(); + if(!$paymentApply) + return $data; + $paymentApply = $paymentApply->toArray(); + $paymentApply['created_at'] = strtotime($paymentApply['created_at']); + $paymentApply['updated_at'] = strtotime($paymentApply['updated_at']); + $paymentApply = (new FinPaymentApplyController())->showFormat($paymentApply); + $data['payment_apply'] = $paymentApply; + } + return $data; + } + + /** + * 收款记录统计 + * @param Request $request + * @return \support\Response + */ + public function statistics(Request $request) + { + try { + $raw = $this->service->statistics(); + return $this->success($raw); + } catch (\Throwable $e) { + return $this->error($e->getMessage()); + } + } +} \ No newline at end of file diff --git a/app/common/controller/FinReceiptHisController.php b/app/common/controller/FinReceiptHisController.php new file mode 100644 index 0000000..7adcf0d --- /dev/null +++ b/app/common/controller/FinReceiptHisController.php @@ -0,0 +1,98 @@ +service = new FinReceiptHisService(); + } + + /** + * 列表页数据格式化 + * + * @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['arrived_at'])) + $data['arrived_at'] = date('Y-m-d H:i:s', $data['arrived_at']); + if (isset($data['received_at'])) + $data['received_at'] = date('Y-m-d H:i:s', $data['received_at']); + if (isset($data['account_id'])) { //收款账号 + $data['account_name'] = SysAccount::where('id', $data['account_id'])->value('account_name')??''; + $data['account_short_desc'] = SysAccount::where('id', $data['account_id'])->value('short_desc')??''; + } + if (isset($data['country_id'])) //付款来源国家 + $data['country_name'] = Country::where('id', $data['country_id'])->value('cn_addr')??''; + if (isset($data['currency_id'])) //付款币种 + $data['currency_name'] = SysCurrency::where('id', $data['currency_id'])->value('currency_name')??''; + if (!empty($data['fx_receipt_file'])){ + $data['fx_receipt_file_full'] = (new OssService)->generatePrivateUrl($data['fx_receipt_file']); + }elseif (isset($data['fx_receipt_file'])){ + $data['fx_receipt_file_full'] = ''; + } + + if (isset($data['created_by'])) + $data['created_by'] = User::where('id', $data['created_by'])->value('name')??''; + if (isset($data['updated_by'])) + $data['updated_by'] = User::where('id', $data['updated_by'])->value('name')??''; + return $data; + } + + /** + * 数据格式化 + * + * @param array $data + * @return array + */ + protected function showFormat($data) + { + if (isset($data['created_at'])) + $data['created_at'] = date('Y-m-d H:i:s', $data['created_at']); + if (isset($data['updated_at'])) + $data['updated_at'] = date('Y-m-d H:i:s', $data['updated_at']); + if (isset($data['arrived_at'])) + $data['arrived_at'] = date('Y-m-d H:i:s', $data['arrived_at']); + if (isset($data['received_at'])) + $data['received_at'] = date('Y-m-d H:i:s', $data['received_at']); + if (isset($data['account_id'])){ //收款账号 + $data['account_name'] = SysAccount::where('id', $data['account_id'])->value('account_name')??''; + $data['account_short_desc'] = SysAccount::where('id', $data['account_id'])->value('short_desc')??''; + } + if (isset($data['country_id'])) //付款来源国家 + $data['country_name'] = Country::where('id', $data['country_id'])->value('cn_addr')??''; + if (isset($data['currency_id'])) //付款币种 + $data['currency_name'] = SysCurrency::where('id', $data['currency_id'])->value('currency_name')??''; + if (!empty($data['fx_receipt_file'])){ + $data['fx_receipt_file_full'] = (new OssService)->generatePrivateUrl($data['fx_receipt_file']); + }elseif (isset($data['fx_receipt_file'])){ + $data['fx_receipt_file_full'] = ''; + } + + if (isset($data['created_by'])) + $data['created_by'] = User::where('id', $data['created_by'])->value('name')??''; + if (isset($data['updated_by'])) + $data['updated_by'] = User::where('id', $data['updated_by'])->value('name')??''; + return $data; + } +} \ No newline at end of file diff --git a/app/common/controller/UploadController.php b/app/common/controller/UploadController.php index d90c84a..a025666 100644 --- a/app/common/controller/UploadController.php +++ b/app/common/controller/UploadController.php @@ -87,12 +87,15 @@ class UploadController } $originalName = $file->getUploadName(); + $ossservice = new OssService(); + $mimes = $ossservice->getMimeTypes(['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'txt', 'zip', 'rar']); + $result = Storage::adapter('oss') ->path('x_server/' . $sceneUrl . date('Ymd')) ->size(1024*1024*5) + ->extYes($mimes) ->upload($file); - - $ossservice = new OssService(); + $data = [ 'original_name' => $originalName, diff --git a/app/common/logic/FinPaymentApplyLogic.php b/app/common/logic/FinPaymentApplyLogic.php new file mode 100644 index 0000000..09b93e1 --- /dev/null +++ b/app/common/logic/FinPaymentApplyLogic.php @@ -0,0 +1,17 @@ +update(['received_at' => time()]); + } +} \ No newline at end of file diff --git a/app/common/logic/SysConfigLogic.php b/app/common/logic/SysConfigLogic.php new file mode 100644 index 0000000..633d177 --- /dev/null +++ b/app/common/logic/SysConfigLogic.php @@ -0,0 +1,22 @@ +where('config_key', $key)->get(['id', 'config_key', 'config_value']); + } +} \ No newline at end of file diff --git a/app/common/model/FinPaymentApply.php b/app/common/model/FinPaymentApply.php new file mode 100644 index 0000000..0f4d157 --- /dev/null +++ b/app/common/model/FinPaymentApply.php @@ -0,0 +1,65 @@ +hasOne(FinReceipt::class, 'payment_apply_id', 'id'); + } + + /** + * 关联的备货单(通过中间表) + */ + public function stocks() + { + return $this->belongsToMany(StockOrders::class, 'fin_payment_stock', 'payment_apply_id', 'stock_id'); + } +} \ No newline at end of file diff --git a/app/common/model/FinPaymentStock.php b/app/common/model/FinPaymentStock.php new file mode 100644 index 0000000..d5b3d10 --- /dev/null +++ b/app/common/model/FinPaymentStock.php @@ -0,0 +1,67 @@ +belongsTo(FinPaymentApply::class, 'payment_apply_id', 'id'); + } + + // 收款记录 + public function receipt() + { + return $this->belongsTo(FinReceipt::class, 'receipt_id', 'id'); + } + + // 备货单 + public function stock() + { + return $this->belongsTo(StockOrders::class, 'stock_id', 'id'); + } +} \ No newline at end of file diff --git a/app/common/model/FinReceipt.php b/app/common/model/FinReceipt.php new file mode 100644 index 0000000..7347be0 --- /dev/null +++ b/app/common/model/FinReceipt.php @@ -0,0 +1,65 @@ +belongsTo(FinPaymentApply::class, 'payment_apply_id', 'id'); + } + + /** + * 关联的备货单(通过中间表) + */ + public function stocks() + { + return $this->belongsToMany(StockOrders::class, 'fin_payment_stock', 'receipt_id', 'stock_id'); + } +} \ No newline at end of file diff --git a/app/common/model/FinReceiptHis.php b/app/common/model/FinReceiptHis.php new file mode 100644 index 0000000..814b87f --- /dev/null +++ b/app/common/model/FinReceiptHis.php @@ -0,0 +1,49 @@ +hasOne(User::class, 'id', 'salesperson_id'); } + + /** + * 客户的标签 + */ + public function payment() + { + return $this->belongsToMany(FinPaymentApply::class, 'fin_payment_stock', 'stock_id', 'payment_apply_id'); + } + + /** + * 关联收款记录(多对多,经由中间表) + */ + public function receipts() + { + return $this->belongsToMany(FinReceipt::class, 'fin_payment_stock', 'stock_id', 'receipt_id'); + } } \ No newline at end of file diff --git a/app/common/model/SysConfig.php b/app/common/model/SysConfig.php new file mode 100644 index 0000000..29b4577 --- /dev/null +++ b/app/common/model/SysConfig.php @@ -0,0 +1,49 @@ +validator; + } + + /** + * 构建查询条件 + */ + protected function buildQueryConfig(Request $request, string $scene = 'index'): array + { + $p = $request->all(); + + $config = []; + $config['model'] = $this->modelClass; + switch ($scene) { + case 'index': + // ====== ====== 排序 ====== ====== + $config['order']['id'] = 'desc'; + + // ====== ====== 筛选 ====== ====== + // 汇款国家 + if (isset($p['country_id']) && !empty($p['country_id'])) { + $config['where']['country_id'] = $p['country_id']; + } + + // 收款账户筛选 + if (isset($p['account_id']) && !empty($p['account_id'])) { + $config['where']['account_id'] = $p['account_id']; + } + + // 关键字 + if (isset($p['keyword']) && $p['keyword']) { + $k = '%' . $p['keyword'] . '%'; + $config['orWhere'][] = ['remitter', 'like', $k]; + } + break; + case 'show': + $config['where']['id'] = $p['id']; + break; + case 'update': + case 'destroy': + $config['where']['id'] = $p['id']; + break; + case 'store': + break; + } + + return $config; + } + + public function beforeServiceStore($data, Request $request) + { + $data['apply_no'] = $this->generateUniqueApplyNo(); + $data['doc_user_id'] = $request->user->uid; + $data['submit_files'] = json_encode($data['submit_files']); + redis::set('stock_' . $data['doc_user_id'], json_encode($request->post('stock')), 'EX', 30); + unset($data['stock']); + return $data; + } + + + public function afterServiceStore($res) + { + $data = [ + 'payment_apply_id' => $res['id'], + ]; + $piStock = json_decode(redis::get('stock_' . $res['doc_user_id']), true); + redis::del('stock' . $res['doc_user_id']); + foreach ($piStock as $stock) { + foreach ($stock['stock_id'] as $v) { + $data['stock_id'] = $v; + $data['pi_no'] = $stock['pi_on']; + FinPaymentStock::create($data); + } + } + } + + public function beforeServiceUpdate($data, Request $request) + { + $data['submit_files'] = json_encode($data['submit_files']); + return $data; + } + + /** + * 生成唯一的付款申请编码 + * 格式:PA + 日期 + 6位字母数字组合 + * + * @return string 唯一编码 + */ + public function generateUniqueApplyNo() + { + $chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ'; + $maxAttempts = 10; // 防止无限循环 + $secondChar = 'PA' . date('Ymd'); + + for ($attempt = 0; $attempt < $maxAttempts; $attempt++) { + // 生成后6位随机字符 + $suffix = ''; + for ($i = 0; $i < 6; $i++) { + $suffix .= $chars[rand(0, strlen($chars) - 1)]; + } + + $applyNo = $secondChar . $suffix; + + // 检查是否已存在 + $existing = FinPaymentApply::where('apply_no', $applyNo)->first(); + if (!$existing) { + return $applyNo; + } + } + + // 如果1000次尝试后仍然重复,使用时间戳确保唯一性 + $timestamp = substr(time(), -6); + return $secondChar . $timestamp; + } + + /** + * 根据相似度排序付款记录 + * @param Request $request + * @return \Illuminate\Contracts\Pagination\LengthAwarePaginator|\Illuminate\Pagination\LengthAwarePaginator + */ + public function complexQueryIndex(Request $request) + { + $limit = (int)$request->get('size', 15); + $page = (int)$request->get('current', 1); + $param = $request->all(); + + $where = []; + + // 汇款国家 + if (isset($param['country_id']) && !empty($param['country_id'])) { + $where[] = ['country_id', $param['country_id']]; + } + // 收款账户筛选 + if (isset($param['account_id']) && !empty($param['account_id'])) { + $where[] = ['account_id', $param['account_id']]; + } + + // 关键字 + if (isset($param['keyword']) && $param['keyword']) { + $k = '%' . $param['keyword'] . '%'; + $where[] = ['remitter', 'like', $k]; + } + + // 构建查询 + $query = $this->modelClass::where($where); + + // 是否已关联收款记录的筛选 + $correlation = $param['correlation']??''; + if ($correlation === 'receipt') { //已关联 + $query->whereHas('receipt'); + } elseif ($correlation === 'unbind') { //未关联 + $query->whereDoesntHave('receipt'); + } + + if ((isset($param['scene']) && $param['scene'] == 'list') || empty($param['receipt_id'])) { + return $query->orderBy('id','desc')->paginate($limit, ['*'], 'page', $page); + } + + $receipt = FinReceipt::where('id', $param['receipt_id'])->first(); + $account_id = $receipt->account_id; + $currency_id = $receipt->currency_id; + $remitter = $receipt->remitter; + $amount = $receipt->amount; + + // 计算十天前的时间戳 + $sevenDaysAgo = time() - (10 * 24 * 60 * 60); + + // 构建相似度排序逻辑 + $similarityOrder = []; + + // 如果有传入参数,计算相似度 + if (!empty($account_id)) { + $similarityOrder[] = "CASE WHEN account_id = {$account_id} THEN 0 ELSE 1 END"; + } + + if (!empty($currency_id)) { + $similarityOrder[] = "CASE WHEN currency_id = {$currency_id} THEN 0 ELSE 1 END"; + } + + if (!empty($remitter)) { + $similarityOrder[] = "CASE WHEN remitter = {$remitter} THEN 0 ELSE 1 END"; + } + + if (!empty($amount)) { + $amount = (float)$amount; + $similarityOrder[] = "ABS(amount - {$amount}) ASC"; + $similarityOrder[] = "CASE WHEN (ABS(amount - {$amount}) / {$amount} * 100) <= 5 THEN 0 ELSE 1 END"; + } + + // 构建排序SQL + $orderByClause = ""; + if (!empty($similarityOrder)) { + $orderByClause = "CASE + WHEN created_at >= {$sevenDaysAgo} THEN + (" . implode(' + ', $similarityOrder) . ") + ELSE 0 + END ASC, "; + } + + $orderByClause .= "CASE + WHEN created_at > {$sevenDaysAgo} THEN id + ELSE 999999 + END DESC, "; + + $orderByClause .= "id DESC"; + + $query->orderByRaw($orderByClause); + + return $query->paginate($limit, ['*'], 'page', $page); + } + +} \ No newline at end of file diff --git a/app/common/service/FinReceiptHisService.php b/app/common/service/FinReceiptHisService.php new file mode 100644 index 0000000..f43f77b --- /dev/null +++ b/app/common/service/FinReceiptHisService.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': + if(empty($p['receipt_id'])){ + throw new BusinessException('receipt_id不能为空'); + } + // ====== ====== 排序 ====== ====== + $config['order']['id'] = 'desc'; + + // ====== ====== 筛选 ====== ====== + $config['where']['receipt_id'] = $p['receipt_id']; + + break; + case 'show': + $config['where']['id'] = $p['id']; + break; + } + + return $config; + } +} \ No newline at end of file diff --git a/app/common/service/FinReceiptService.php b/app/common/service/FinReceiptService.php new file mode 100644 index 0000000..1f1c731 --- /dev/null +++ b/app/common/service/FinReceiptService.php @@ -0,0 +1,319 @@ +logic = (new FinReceiptLogic()); + } + + /** + * 获取验证器 + */ + public function getValidator() + { + return new $this->validator; + } + + + /** + * 构建查询条件 + */ + protected function buildQueryConfig(Request $request, string $scene = 'index'): array + { + $p = $request->all(); + + $config = []; + $config['model'] = $this->modelClass; + switch ($scene) { + case 'index': + // ====== ====== 排序 ====== ====== + $config['order']['id'] = 'desc'; + + // ====== ====== 筛选 ====== ====== + // 汇款国家 + if (isset($p['country_id']) && !empty($p['country_id'])) { + $config['where']['country_id'] = $p['country_id']; + } + + // 没有付款记录的收款记录 + if (isset($p['payment_apply']) && $p['payment_apply'] == 'not') { //未认领 + $config['where']['payment_apply_id'] = ['null']; + } elseif (isset($p['payment_apply']) && $p['payment_apply'] == 'already') { //已认领 + $config['where']['payment_apply_id'] = ['not null']; + } elseif (isset($p['payment_apply']) && $p['payment_apply'] == 'timeout') { //超时未认领 + $config['where']['payment_apply_id'] = ['not null']; + $config['where']['created_at'] = ['<' ,time() - 24*60*60]; + } + + // 收款申请筛选 + if (isset($p['payment_apply_id']) && !empty($p['payment_apply_id'])) { + $config['where']['payment_apply_id'] = $p['payment_apply_id']; + } + + // 收款账户筛选 + if (isset($p['account_id']) && !empty($p['account_id'])) { + $config['where']['account_id'] = $p['account_id']; + } + + // 关键字 + if (isset($p['keyword']) && $p['keyword']) { + $k = '%' . $p['keyword'] . '%'; + $config['orWhere'][] = ['remitter', 'like', $k]; + } + break; + case 'show': + $config['where']['id'] = $p['id']; + break; + case 'update': + case 'payment': + case 'destroy': + $config['where']['id'] = $p['id']; + break; + case 'store': + break; + } + + return $config; + } + + public function beforeServiceStore($data, Request $request) + { + if(isset($data['payment_apply_id']) && empty($data['payment_apply_id'])){ + unset($data['payment_apply_id']); + } + $data['receipt_no'] = $this->generateUniqueReceipt(); + $data['arrived_at'] = strtotime( $data['arrived_at']); + $data['created_by'] = $request->user->uid; + $data['updated_by'] = $request->user->uid; + return $data; + } + + public function afterServiceStore($res) + { + $receipt = FinReceipt::where('id',$res->id)->first()->toArray(); + $receipt = array_merge($receipt,['receipt_id' => $res->id]); + unset($receipt['id']); + FinReceiptHis::create($receipt); + } + + public function beforeServiceUpdate($data, Request $request) + { + $data['updated_by'] = $request->user->uid; + if(isset($data['arrived_at'])) + $data['arrived_at'] = strtotime($data['arrived_at']); + if(isset($data['received_at'])) + $data['received_at'] = strtotime($data['received_at']); + + return $data; + } + public function afterServiceUpdate($res) + { + $receipt = FinReceipt::where('id',$res['id'])->first()->toArray(); + $receipt = array_merge($receipt,['receipt_id' => $res['id']]); + unset($receipt['id']); + FinReceiptHis::create($receipt); + } + + + /** + * 生成唯一的付款申请编码 + * 格式:PA + 日期 + 6位字母数字组合 + * + * @return string 唯一编码 + */ + public function generateUniqueReceipt() + { + $chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ'; + $maxAttempts = 10; // 防止无限循环 + $secondChar = 'RE' . date('Ymd'); + + for ($attempt = 0; $attempt < $maxAttempts; $attempt++) { + // 生成后6位随机字符 + $suffix = ''; + for ($i = 0; $i < 6; $i++) { + $suffix .= $chars[rand(0, strlen($chars) - 1)]; + } + + $ReceiptNo = $secondChar . $suffix; + + // 检查是否已存在 + $existing = FinReceipt::where('receipt_no', $ReceiptNo)->first(); + if (!$existing) { + return $ReceiptNo; + } + } + + // 如果1000次尝试后仍然重复,使用时间戳确保唯一性 + $timestamp = substr(time(), -6); + return $secondChar . $timestamp; + } + + /** + * 根据相似度排序付款记录 + * @param Request $request + * @return \Illuminate\Contracts\Pagination\LengthAwarePaginator|\Illuminate\Pagination\LengthAwarePaginator + */ + public function complexQueryIndex(Request $request) + { + $limit = (int)$request->get('size', 15); + $page = (int)$request->get('current', 1); + $param = $request->all(); + + $where = []; + + // 汇款国家 + if (isset($param['country_id']) && !empty($param['country_id'])) { + $where[] = ['country_id', $param['country_id']]; + } + // 收款账户筛选 + if (isset($param['account_id']) && !empty($param['account_id'])) { + $where[] = ['account_id', $param['account_id']]; + } + // 收款申请筛选 + if (isset($param['payment_apply_id']) && !empty($param['payment_apply_id'])) { + $where[]['payment_apply_id'] = $param['payment_apply_id']; + } + + // 关键字 + if (isset($param['keyword']) && $param['keyword']) { + $k = '%' . $param['keyword'] . '%'; + $where[] = ['remitter', 'like', $k]; + } + + // 构建查询 + $query = $this->modelClass::where($where); + + // 没有付款记录的收款记录 + if (isset($param['payment_apply']) && $param['payment_apply'] == 'not') { + $query->whereNull('payment_apply_id'); + } elseif (isset($param['payment_apply']) && $param['payment_apply'] == 'already') { + $query->whereNotNull('payment_apply_id'); + } + + if (isset($param['scene']) && $param['scene'] == 'list') { + return $query->paginate($limit, ['*'], 'page', $page); + } + + // 计算七天前的时间戳 + $sevenDaysAgo = time() - (10 * 24 * 60 * 60); + + // 构建相似度排序逻辑 + $similarityOrder = []; + + // 如果有传入参数,计算相似度 + if (isset($param['account_id']) && !empty($param['account_id'])) { + $similarityOrder[] = "CASE WHEN account_id = {$param['account_id']} THEN 0 ELSE 1 END"; + } + + if (isset($param['country_id']) && !empty($param['country_id'])) { + $similarityOrder[] = "CASE WHEN country_id = {$param['country_id']} THEN 0 ELSE 1 END"; + } + + if (isset($param['keyword']) && $param['keyword']) { + $keyword = addslashes($param['keyword']); + $similarityOrder[] = "CASE WHEN remitter LIKE '%{$keyword}%' THEN 0 ELSE 1 END"; + } + + if (isset($param['amount']) && !empty($param['amount'])) { + $amount = (float)$param['amount']; + $similarityOrder[] = "ABS(amount - {$amount})"; + } + + // 构建排序SQL + $orderByClause = ""; + if (!empty($similarityOrder)) { + $orderByClause = "CASE + WHEN created_at >= {$sevenDaysAgo} THEN + (" . implode(' + ', $similarityOrder) . ") + ELSE 999999 + END ASC, "; + } + + $orderByClause .= "CASE + WHEN created_at < {$sevenDaysAgo} THEN id + ELSE 0 + END DESC"; + + $query->orderByRaw($orderByClause); + + return $query->paginate($limit, ['*'], 'page', $page); + } + + public function credit($id) + { + $finReceipt = FinReceipt::find($id); + if(!empty($finReceipt->received_at)){ + throw new BusinessException('该记录已将确认入账'); + } + $res = $this->logic->credit($id); + $this->afterServiceUpdate(['id'=>$id]); + return $res; + } + + /** + * 更新:绑定收款记录,计算折让金额 + * @param Request $request + * @param array $data + * @param string $scene + * @return mixed + */ + public function serviceUpdatePayment(Request $request, array $data = [], string $scene = 'payment') + { + $payment_apply_id = $request->input('payment_apply_id'); + $id = $request->input('id'); + $data = $this->preProcess(ucfirst($scene), $data, $request); + $config = $this->buildQueryConfig($request, $scene); + FinPaymentApply::where('id', $payment_apply_id)->update(['receipt_id' => $id, 'claimed_at' => time(), 'claimed_by' => $request->user->uid]); + + //折让金额 + $payment = FinPaymentApply::where('id', $payment_apply_id)->first(); + $receipt = FinReceipt::find($id); + $data['discount_amount'] = $payment->amount - $receipt->amount ?? 0; + + $updated = $this->logicUpdateBy($config, $data); + return $this->postProcess(ucfirst($scene), $data); + } + + /** + * 收款记录统计 + * @return bool|int + */ + public function statistics() + { + $finReceipt = FinReceipt::find($id); + if(!empty($finReceipt->received_at)){ + throw new BusinessException('该记录已将确认入账'); + } + $res = $this->logic->credit($id); + $this->afterServiceUpdate(['id'=>$id]); + return $res; + } +} \ No newline at end of file diff --git a/app/common/service/OssService.php b/app/common/service/OssService.php index 72a956e..1aacc1c 100644 --- a/app/common/service/OssService.php +++ b/app/common/service/OssService.php @@ -76,4 +76,38 @@ class OssService throw new BusinessException('Delete Error: ' . $th->getMessage()); } } + + /** + * 获取对应的 MIME 类型 + */ + public function getMimeTypes($extensions) + { + $mimeMap = [ + 'jpg' => 'image/jpeg', + 'jpeg' => 'image/jpeg', + 'png' => 'image/png', + 'gif' => 'image/gif', + 'bmp' => 'image/bmp', + 'webp' => 'image/webp', + 'pdf' => 'application/pdf', + 'doc' => 'application/msword', + 'docx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + 'xls' => 'application/vnd.ms-excel', + 'xlsx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + 'ppt' => 'application/vnd.ms-powerpoint', + 'pptx' => 'application/vnd.openxmlformats-officedocument.presentationml.presentation', + 'txt' => 'text/plain', + 'zip' => 'application/zip', + 'rar' => 'application/x-rar-compressed' + ]; + + $mimes = []; + foreach ($extensions as $ext) { + if (isset($mimeMap[$ext])) { + $mimes[] = $mimeMap[$ext]; + } + } + + return array_unique($mimes); + } } \ No newline at end of file diff --git a/app/common/service/SysConfigService.php b/app/common/service/SysConfigService.php new file mode 100644 index 0000000..d52972f --- /dev/null +++ b/app/common/service/SysConfigService.php @@ -0,0 +1,37 @@ +logic = (new SysConfigLogic()); + } + + /** + * 获取系统list + */ + public static function getConfig(string $config_key) + { + $value = Redis::get($config_key); + if (empty($value)) { + $config = (new SysConfigLogic())->findByKey($config_key); + if (empty($config)) { + return null; + } + $value = $config->config_value; + Redis::set($config->config_key, $value, 'EX', 7200); + } + + return $value; + } +} \ No newline at end of file diff --git a/app/common/service/SysOpLogService.php b/app/common/service/SysOpLogService.php index 3291093..ee19817 100644 --- a/app/common/service/SysOpLogService.php +++ b/app/common/service/SysOpLogService.php @@ -61,7 +61,7 @@ class SysOpLogService // 关键字 if (isset($p['keyword']) && $p['keyword']) { $k = '%' . $p['keyword'] . '%'; - foreach (['user_name', 'route'] as $col) { + foreach (['user_name', 'route', 'params_brief'] as $col) { $config['orWhere'][] = [$col, 'like', $k]; } } diff --git a/app/common/utils/scaffold/templates/paymentFiles.stub b/app/common/utils/scaffold/templates/paymentFiles.stub new file mode 100644 index 0000000..6c9cded --- /dev/null +++ b/app/common/utils/scaffold/templates/paymentFiles.stub @@ -0,0 +1,103 @@ + + + + + + 邮箱提醒 + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/common/validate/FinPaymentApplyValidate.php b/app/common/validate/FinPaymentApplyValidate.php new file mode 100644 index 0000000..58145e3 --- /dev/null +++ b/app/common/validate/FinPaymentApplyValidate.php @@ -0,0 +1,29 @@ + [ + + ], + + 'update' => [ + + ] + ]; +} \ No newline at end of file diff --git a/app/common/validate/FinReceiptHisValidate.php b/app/common/validate/FinReceiptHisValidate.php new file mode 100644 index 0000000..7a1f7c4 --- /dev/null +++ b/app/common/validate/FinReceiptHisValidate.php @@ -0,0 +1,29 @@ + [ + + ], + + 'update' => [ + + ] + ]; +} \ No newline at end of file diff --git a/app/common/validate/FinReceiptValidate.php b/app/common/validate/FinReceiptValidate.php new file mode 100644 index 0000000..237b03e --- /dev/null +++ b/app/common/validate/FinReceiptValidate.php @@ -0,0 +1,56 @@ + 'integer|gt:0', +// 'country_id' => 'require|integer|gt:0', +// 'remitter' => 'require', +// 'account_id' => 'require|integer|gt:0', +// 'currency_id' => 'require|integer|gt:0', +// 'amount' => 'require|gt:0', +// 'exchange_rate' => 'require|gt:0', +// 'arrived_at' => 'require', +// 'is_domestic_return' => 'require|integer', + ]; + + // 提示 + protected $message = [ +// 'payment_apply_id.integer' => '付款申请ID必须是整数', +// 'payment_apply_id.gt' => '付款申请ID必须大于0', +// 'country_id.require' => '付款来源国家ID不能为空', +// 'country_id.integer' => '付款来源国家ID必须是整数', +// 'country_id.gt' => '付款来源国家ID必须大于0', +// 'remitter.require' => '汇款人不能为空', +// 'account_id.require' => '收款账号ID不能为空', +// 'account_id.integer' => '收款账号ID必须是整数', +// 'account_id.gt' => '收款账号ID必须大于0', +// 'currency_id.require' => '收款币种ID不能为空', +// 'currency_id.integer' => '收款币种ID必须是整数', +// 'currency_id.gt' => '收款币种ID必须大于0', +// 'amount.require' => '收款金额不能为空', +// 'amount.gt' => '收款金额必须大于0', +// 'exchange_rate.require' => '实际汇率不能为空', +// 'exchange_rate.gt' => '实际汇率必须大于0', +// 'arrived_at.require' => '到账时间不能为空', +// 'arrived_at.gt' => '到账时间必须大于0', +// 'is_domestic_return.require' => '是否回转内地不能为空', +// 'is_domestic_return.integer' => '是否回转内地必须是整数', + ]; + + // 场景 + protected $scene = [ + 'store' => [ +// 'payment_apply_id', 'country_id', 'remitter', 'account_id', 'currency_id', 'amount', 'exchange_rate', 'arrived_at' + ], + + 'update' => [ +// 'payment_apply_id' + ] + ]; +} \ No newline at end of file diff --git a/app/functions.php b/app/functions.php index 21e53c9..43c2f9a 100644 --- a/app/functions.php +++ b/app/functions.php @@ -180,7 +180,8 @@ if (!function_exists('tplUrl')) { { $urlArray = [ 'email' => 'common/utils/scaffold/templates/Email.stub', - 'work_order' => 'common/utils/scaffold/templates/workOrder.stub' + 'work_order' => 'common/utils/scaffold/templates/workOrder.stub', + 'payment_files' => 'common/utils/scaffold/templates/paymentFiles.stub', ]; return $urlArray[$tplKey]; } diff --git a/app/process/Task.php b/app/process/Task.php index ccaa228..8f9cea1 100644 --- a/app/process/Task.php +++ b/app/process/Task.php @@ -2,6 +2,8 @@ namespace app\process; +use app\common\model\FinPaymentApply; +use app\common\model\User; use app\crm\model\Customer; use support\Log; use Workerman\Crontab\Crontab; @@ -11,7 +13,7 @@ class Task public function onWorkerStart() { - // 每半小时执行一次 + // 每半小时执行一次,通知待跟进客户 new Crontab('0 */30 * * * *', function(){ echo date('Y-m-d H:i:s') . "\n"; Log::info('send follow start',[time()]); @@ -41,7 +43,45 @@ class Task $item->save(); } - Log::info('send follow start',[time()]); + Log::info('send follow end',[time()]); + }); + + // 检查入账资料是否提交 + new Crontab('* */70 * * * *', function () { + $G = date('G'); + if ($G > 22 || $G < 8) { + return; + } + Log::info('fin_payment_apply submit_files start', [time()]); + $orWhere = [ + ['submit_files', '=', '[]'], + ['submit_files', '=', ''], + ]; + $where = [ + ['warn', '<', 2], + ['created_at', '>', time() - 60 * 60 * 48], + ['created_at', '<', time() - 60 * 60 * 24], + ]; + $payment = FinPaymentApply::where($where)->orWhere($orWhere)->orderBy('id', 'desc')->get(); + foreach ($payment as $item) { + Log::info('fin_payment_apply submit_files ', [$item->toArray()]); + $notice = [ + 'code' => $item->apply_no, + 'payment_entity' => $item->payment_entity ?? $item->remitter, + 'content' => '认领收款单后请在24小时内提交入账资料', + 'reply' => '', + ]; + $email = []; + $email[] = User::where('id', $item->claimed_by)->value('email'); + $email[] = User::where('id', $item->doc_user_id)->value('email'); + $email = array_unique($email); + \app\common\utils\email\MailSend::send($email, '【XYH】付款记录', replaceTpl($notice, 'payment_files')); + + $item->warn = ++$item->warn; + $item->save(); + } + + Log::info('fin_payment_apply submit_files end', [time()]); }); } diff --git a/config/route.php b/config/route.php index a19797e..22b0862 100644 --- a/config/route.php +++ b/config/route.php @@ -389,7 +389,6 @@ Route::group('/admin', function () { }); }); - Route::group('/purchase/order', function () { Route::any('/predata', [app\common\controller\PurchaseOrdersController::class, 'preQueryData']); Route::any('/list', [app\common\controller\PurchaseOrdersController::class, 'index']); @@ -423,6 +422,44 @@ Route::group('/admin', function () { Route::any('/destroy', [app\common\controller\SysUpdateLogController::class, 'destroy']); }); + // 收款记录 + Route::group('/receipt', function () { + Route::any('/list', [app\common\controller\FinReceiptController::class, 'index']); + Route::any('/show', [app\common\controller\FinReceiptController::class, 'show']); + Route::any('/store', [app\common\controller\FinReceiptController::class, 'store']); + Route::any('/update', [app\common\controller\FinReceiptController::class, 'update']); + Route::any('/payment', [app\common\controller\FinReceiptController::class, 'payment']); + Route::any('/destroy', [app\common\controller\FinReceiptController::class, 'destroy']); + Route::any('/credit', [app\common\controller\FinReceiptController::class, 'credit']); + }); + // 收款记录历史 + Route::group('/receipt/his', function () { + Route::any('/list', [app\common\controller\FinReceiptHisController::class, 'index']); + Route::any('/show', [app\common\controller\FinReceiptHisController::class, 'show']); + }); + + Route::group('/fin', function () { + Route::any('/statistics', [app\admin\controller\dashboard\FinStatisticsController::class, 'statistics']); + }); + + // 付款申请 + Route::group('/payment', function () { + Route::any('/list', [app\common\controller\FinPaymentApplyController::class, 'index']); + Route::any('/show', [app\common\controller\FinPaymentApplyController::class, 'show']); + Route::any('/store', [app\common\controller\FinPaymentApplyController::class, 'store']); + Route::any('/update', [app\common\controller\FinPaymentApplyController::class, 'update']); + Route::any('/destroy', [app\common\controller\FinPaymentApplyController::class, 'destroy']); + }); + + // 系统配置 + Route::group('/config', function () { + Route::any('/list', [app\admin\controller\SysConfigController::class, 'index']); + Route::any('/show', [app\admin\controller\SysConfigController::class, 'show']); + Route::any('/store', [app\admin\controller\SysConfigController::class, 'store']); + Route::any('/update', [app\admin\controller\SysConfigController::class, 'update']); + Route::any('/destroy', [app\admin\controller\SysConfigController::class, 'destroy']); + }); + // 仪表盘 Route::group('/dashboard', function () { // 内勤 From ef0c23613015490356c887a3b7139eb8c5da2636 Mon Sep 17 00:00:00 2001 From: mzl Date: Thu, 30 Oct 2025 14:39:35 +0800 Subject: [PATCH 045/110] Merge branch 'develop' into feature/mzl_dev # Conflicts: # config/route.php From 077edc5fd6bf914e31c22f1909c2e1187a1be706 Mon Sep 17 00:00:00 2001 From: mzl Date: Thu, 30 Oct 2025 14:40:24 +0800 Subject: [PATCH 046/110] Merge branch 'feature/mzl_dev' into develop # Conflicts: # config/route.php --- .../dashboard/OfficeClerkController.php | 164 ++++++++++++++++++ config/route.php | 11 ++ 2 files changed, 175 insertions(+) create mode 100644 app/admin/controller/dashboard/OfficeClerkController.php diff --git a/app/admin/controller/dashboard/OfficeClerkController.php b/app/admin/controller/dashboard/OfficeClerkController.php new file mode 100644 index 0000000..2bd2bcc --- /dev/null +++ b/app/admin/controller/dashboard/OfficeClerkController.php @@ -0,0 +1,164 @@ +where('approver_id', $request->user->uid) + ->where('status', 'approved') + ->count('id'); + + $pending = Db::table('approval_records') + ->where('approver_id', $request->user->uid) + ->where('status', 'pending') + ->count('id'); + + $unclaimed = Db::table('fin_payment_apply') + ->where('doc_user_id', $request->user->uid) + ->where('receipt_id', 0) + ->count('id'); + + $claimed = Db::table('fin_payment_apply') + ->where('doc_user_id', $request->user->uid) + ->where('receipt_id', '>', 0) + ->count('id'); + + $twentyFourHoursAgo = time() - (24 * 3600); + $timeout = Db::table('fin_payment_apply') + ->where('doc_user_id', $request->user->uid) + ->whereNotNull('submit_files') + ->whereJsonLength('submit_files', '>', 0) + ->where('receipt_id', '>', 0) + ->where('claimed_at', '<', $twentyFourHoursAgo) + ->count('id'); + + return $this->success([ + 'approved' => $approved, + 'pending' => $pending, + 'unclaimed' => $unclaimed, + 'claimed' => $claimed, + 'timeout' => $timeout + ]); + } + + // 曲线 + public function curve() + { + $groupIds = Db::table('user_group') + ->where('scenario_code', 'stock_orders') + ->pluck('id'); + $userIds = Db::table('user_group_members') + ->whereIn('group_id', $groupIds) + ->pluck('user_id'); + $list = Db::table('stock_orders') + ->join('user', 'stock_orders.salesperson_id', '=', 'user.id') + ->whereIn('stock_orders.salesperson_id', $userIds) + ->whereIn('stock_orders.approval_status', [0, 1, 2]) + ->whereNull('stock_orders.deleted_at') + ->groupBy('stock_orders.salesperson_id', 'user.name') + ->selectRaw('user.name, count(*) as count') + ->pluck('count', 'name') + ->toArray(); + $optionType = []; + $counts = []; + foreach ($list as $key=>$val) { + $optionType[] = $key; + $counts[] = $val; + } + $arr = [ + "optionType" => $optionType, + "counts" => $counts + ]; + return $this->success($arr); + } + + // 审批 + public function approval(Request $request) + { + $list = Db::table('approval_records as ar') + ->join('approval_instances as ai', 'ar.instance_id', '=', 'ai.id') + ->join('stock_orders as so', 'ai.biz_id', '=', 'so.id') + ->where('ar.approver_id', $request->user->uid) + ->where('ar.status', 'pending') + ->where('ai.flow_type', 'stock_orders') + ->where('ai.biz_type', 'stock_orders') + ->where('ai.status', 'processing') + ->whereNull('so.deleted_at') + ->select( + 'so.order_no', + 'ai.created_at as approval_created_at', + 'so.salesperson_snapshot as salesperson_name' + ) + ->get() + ->map(function($item) { + return [ + 'order_no' => $item->order_no, + 'approval_created_at' => date('Y-m-d H:i:s', strtotime($item->approval_created_at)), // 格式化时间 + 'salesperson_name' => $item->salesperson_name + ]; + }) + ->toArray(); + return $this->success($list); + } + + // 认领 + public function claim(Request $request) + { + $type = $request->get('type'); + if ($type == 'unclaimed') { + $list = Db::table('fin_payment_apply') + ->where('doc_user_id', $request->user->uid) + ->where('receipt_id', 0) + ->select( + 'apply_no', + 'amount', + 'created_at' + ) + ->get() + ->map(function($item) { + return [ + 'apply_no' => $item->apply_no, + 'amount' => $item->amount, + 'created_at' => date('Y-m-d H:i:s', $item->created_at) + ]; + }) + ->toArray(); + } else { + $twentyFourHoursAgo = time() - (24 * 3600); + $list = Db::table('fin_payment_apply') + ->where('doc_user_id', $request->user->uid) + ->whereNotNull('submit_files') + ->whereJsonLength('submit_files', '>', 0) + ->where('receipt_id', '>', 0) + ->where('claimed_at', '<', $twentyFourHoursAgo) + ->select( + 'apply_no', + 'amount', + 'created_at' + ) + ->get() + ->map(function($item) { + return [ + 'apply_no' => $item->apply_no, + 'amount' => $item->amount, + 'created_at' => date('Y-m-d H:i:s', $item->created_at) + ]; + }) + ->toArray(); + } + return $this->success($list); + } +} \ No newline at end of file diff --git a/config/route.php b/config/route.php index d6536c4..22b0862 100644 --- a/config/route.php +++ b/config/route.php @@ -459,6 +459,17 @@ Route::group('/admin', function () { Route::any('/update', [app\admin\controller\SysConfigController::class, 'update']); Route::any('/destroy', [app\admin\controller\SysConfigController::class, 'destroy']); }); + + // 仪表盘 + Route::group('/dashboard', function () { + // 内勤 + Route::group('/officeclerk', function () { + Route::any('/card', [app\admin\controller\dashboard\OfficeClerkController::class, 'card']); + Route::any('/curve', [app\admin\controller\dashboard\OfficeClerkController::class, 'curve']); + Route::any('/approval', [app\admin\controller\dashboard\OfficeClerkController::class, 'approval']); + Route::any('/claim', [app\admin\controller\dashboard\OfficeClerkController::class, 'claim']); + }); + }); }); From 15df6ad9a3ec5de4a5a98c1c68ab346ec104692f Mon Sep 17 00:00:00 2001 From: "WIN-C27C5MSVTPO\\16961" <1696136552@qq.com> Date: Thu, 30 Oct 2025 14:58:29 +0800 Subject: [PATCH 047/110] =?UTF-8?q?=E8=B6=85=E6=97=B6=E6=9C=AA=E8=AE=A4?= =?UTF-8?q?=E9=A2=86=E7=AD=9B=E9=80=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/common/service/FinReceiptService.php | 18 +++--------------- 1 file changed, 3 insertions(+), 15 deletions(-) diff --git a/app/common/service/FinReceiptService.php b/app/common/service/FinReceiptService.php index 1f1c731..52e3681 100644 --- a/app/common/service/FinReceiptService.php +++ b/app/common/service/FinReceiptService.php @@ -216,6 +216,9 @@ class FinReceiptService $query->whereNull('payment_apply_id'); } elseif (isset($param['payment_apply']) && $param['payment_apply'] == 'already') { $query->whereNotNull('payment_apply_id'); + } elseif (isset($param['payment_apply']) && $param['payment_apply'] == 'timeout') { //超时未认领 + $query->whereNull('payment_apply_id'); + $query->where('created_at', '<' ,time() - 24*60*60); } if (isset($param['scene']) && $param['scene'] == 'list') { @@ -301,19 +304,4 @@ class FinReceiptService $updated = $this->logicUpdateBy($config, $data); return $this->postProcess(ucfirst($scene), $data); } - - /** - * 收款记录统计 - * @return bool|int - */ - public function statistics() - { - $finReceipt = FinReceipt::find($id); - if(!empty($finReceipt->received_at)){ - throw new BusinessException('该记录已将确认入账'); - } - $res = $this->logic->credit($id); - $this->afterServiceUpdate(['id'=>$id]); - return $res; - } } \ No newline at end of file From 499b0894b7fe374a0711416a80c1c6577c4fa06e Mon Sep 17 00:00:00 2001 From: "WIN-C27C5MSVTPO\\16961" <1696136552@qq.com> Date: Thu, 30 Oct 2025 15:24:05 +0800 Subject: [PATCH 048/110] =?UTF-8?q?=E5=B7=B2=E8=AE=A4=E9=A2=86=E7=AD=9B?= =?UTF-8?q?=E9=80=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/common/service/FinReceiptService.php | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/app/common/service/FinReceiptService.php b/app/common/service/FinReceiptService.php index 52e3681..3cbc6d2 100644 --- a/app/common/service/FinReceiptService.php +++ b/app/common/service/FinReceiptService.php @@ -65,11 +65,11 @@ class FinReceiptService // 没有付款记录的收款记录 if (isset($p['payment_apply']) && $p['payment_apply'] == 'not') { //未认领 - $config['where']['payment_apply_id'] = ['null']; + $config['where']['payment_apply_id'] = 0; } elseif (isset($p['payment_apply']) && $p['payment_apply'] == 'already') { //已认领 - $config['where']['payment_apply_id'] = ['not null']; + $config['where']['payment_apply_id'] = ['>', 0]; } elseif (isset($p['payment_apply']) && $p['payment_apply'] == 'timeout') { //超时未认领 - $config['where']['payment_apply_id'] = ['not null']; + $config['where']['payment_apply_id'] = 0; $config['where']['created_at'] = ['<' ,time() - 24*60*60]; } @@ -213,11 +213,11 @@ class FinReceiptService // 没有付款记录的收款记录 if (isset($param['payment_apply']) && $param['payment_apply'] == 'not') { - $query->whereNull('payment_apply_id'); + $query->where('payment_apply_id', 0); } elseif (isset($param['payment_apply']) && $param['payment_apply'] == 'already') { - $query->whereNotNull('payment_apply_id'); + $query->where('payment_apply_id', '>', 0); } elseif (isset($param['payment_apply']) && $param['payment_apply'] == 'timeout') { //超时未认领 - $query->whereNull('payment_apply_id'); + $query->where('payment_apply_id', 0); $query->where('created_at', '<' ,time() - 24*60*60); } From f64324fba536356ac882f5ce9f77954ec5d752a8 Mon Sep 17 00:00:00 2001 From: "WIN-C27C5MSVTPO\\16961" <1696136552@qq.com> Date: Thu, 30 Oct 2025 16:29:54 +0800 Subject: [PATCH 049/110] =?UTF-8?q?=E8=B6=85=E6=97=B6=E6=9C=AA=E4=B8=8A?= =?UTF-8?q?=E4=BC=A0=E5=85=A5=E8=B4=A6=E8=B5=84=E6=96=99=E7=AD=9B=E9=80=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/FinPaymentApplyController.php | 10 ++++++++++ app/common/service/FinPaymentApplyService.php | 20 +++++++++++++++++-- app/common/service/FinReceiptService.php | 2 ++ app/process/Task.php | 4 +++- 4 files changed, 33 insertions(+), 3 deletions(-) diff --git a/app/common/controller/FinPaymentApplyController.php b/app/common/controller/FinPaymentApplyController.php index 3bf4b15..6f29595 100644 --- a/app/common/controller/FinPaymentApplyController.php +++ b/app/common/controller/FinPaymentApplyController.php @@ -70,6 +70,16 @@ class FinPaymentApplyController extends BaseController $data['enter_file_last'] = date('Y-m-d H:i:s', ($data['claimed_at']+86400)); //入账资料最后提交时间 $data['claimed_at'] = date('Y-m-d H:i:s', $data['claimed_at']); //认领时间 } + if (isset($data['id'])){ + $finPaymentStock = FinPaymentStock::where('payment_apply_id', $data['id'])->get(); + $stock = []; + foreach ($finPaymentStock as $v) { + $stock[$v['pi_no']]['pi_no'] = $v['pi_no']; + $stock[$v['pi_no']]['stock_id'][] = [$v['stock_id']]; + } + $data['stock'] = array_values($stock); + } + return $data; } diff --git a/app/common/service/FinPaymentApplyService.php b/app/common/service/FinPaymentApplyService.php index e5091fd..ce2bbfd 100644 --- a/app/common/service/FinPaymentApplyService.php +++ b/app/common/service/FinPaymentApplyService.php @@ -58,6 +58,11 @@ class FinPaymentApplyService $config['where']['account_id'] = $p['account_id']; } + // 入账资料超时 + if (isset($p['scene']) && $p['scene'] == 'file_timeout') { + $config['where']['claimed_at'] = ['between', [1, time()-24*60*60]]; + } + // 关键字 if (isset($p['keyword']) && $p['keyword']) { $k = '%' . $p['keyword'] . '%'; @@ -156,7 +161,8 @@ class FinPaymentApplyService $param = $request->all(); $where = []; - + $orWhere = []; + // 汇款国家 if (isset($param['country_id']) && !empty($param['country_id'])) { $where[] = ['country_id', $param['country_id']]; @@ -166,6 +172,14 @@ class FinPaymentApplyService $where[] = ['account_id', $param['account_id']]; } + // 入账资料超时 + if (isset($param['scene']) && $param['scene'] == 'file_timeout') { + $where[] = ['claimed_at', '<', time()-24*60*60]; + $where[] = ['claimed_at', '>', 1]; + $orWhere[] = ['submit_files', '[]']; + $orWhere[] = ['submit_files', '']; + } + // 关键字 if (isset($param['keyword']) && $param['keyword']) { $k = '%' . $param['keyword'] . '%'; @@ -173,7 +187,9 @@ class FinPaymentApplyService } // 构建查询 - $query = $this->modelClass::where($where); + $query = $this->modelClass::where($where)->where(function($query) use ($orWhere) { + $query->orWhere($orWhere); + }); // 是否已关联收款记录的筛选 $correlation = $param['correlation']??''; diff --git a/app/common/service/FinReceiptService.php b/app/common/service/FinReceiptService.php index 52e3681..be72fc9 100644 --- a/app/common/service/FinReceiptService.php +++ b/app/common/service/FinReceiptService.php @@ -4,6 +4,7 @@ namespace app\common\service; use app\common\logic\FinReceiptLogic; use app\common\model\FinPaymentApply; +use app\common\model\FinPaymentStock; use app\common\model\FinReceiptHis; use app\common\validate\FinReceiptValidate; use app\common\traits\crud\CrudServiceTrait; @@ -295,6 +296,7 @@ class FinReceiptService $data = $this->preProcess(ucfirst($scene), $data, $request); $config = $this->buildQueryConfig($request, $scene); FinPaymentApply::where('id', $payment_apply_id)->update(['receipt_id' => $id, 'claimed_at' => time(), 'claimed_by' => $request->user->uid]); + FinPaymentStock::where('payment_apply_id', $payment_apply_id)->update(['receipt_id' => $id]); //折让金额 $payment = FinPaymentApply::where('id', $payment_apply_id)->first(); diff --git a/app/process/Task.php b/app/process/Task.php index 47dbb93..7175244 100644 --- a/app/process/Task.php +++ b/app/process/Task.php @@ -55,7 +55,9 @@ class Task ['created_at', '>', time() - 60 * 60 * 48], ['created_at', '<', time() - 60 * 60 * 24], ]; - $payment = FinPaymentApply::where($where)->orWhere($orWhere)->orderBy('id', 'desc')->get(); + $payment = FinPaymentApply::where($where)->where(function($query) use ($orWhere) { + $query->orWhere($orWhere); + })->orderBy('id', 'desc')->get(); foreach ($payment as $item) { Log::info('fin_payment_apply submit_files ', [$item->toArray()]); $notice = [ From 9fbe77a3a95a66dc98db499c90f7500c95a6ff7e Mon Sep 17 00:00:00 2001 From: mzl Date: Thu, 30 Oct 2025 16:42:42 +0800 Subject: [PATCH 050/110] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E8=AE=A4=E9=A2=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dashboard/OfficeClerkController.php | 85 +++++++++++-------- 1 file changed, 50 insertions(+), 35 deletions(-) diff --git a/app/admin/controller/dashboard/OfficeClerkController.php b/app/admin/controller/dashboard/OfficeClerkController.php index 2bd2bcc..6fa70ee 100644 --- a/app/admin/controller/dashboard/OfficeClerkController.php +++ b/app/admin/controller/dashboard/OfficeClerkController.php @@ -120,44 +120,59 @@ class OfficeClerkController extends BaseController $type = $request->get('type'); if ($type == 'unclaimed') { $list = Db::table('fin_payment_apply') - ->where('doc_user_id', $request->user->uid) - ->where('receipt_id', 0) - ->select( - 'apply_no', - 'amount', - 'created_at' - ) - ->get() - ->map(function($item) { - return [ - 'apply_no' => $item->apply_no, - 'amount' => $item->amount, - 'created_at' => date('Y-m-d H:i:s', $item->created_at) - ]; - }) - ->toArray(); + ->join('sys_currency', 'fin_payment_apply.currency_id', '=', 'sys_currency.id') + ->where('fin_payment_apply.doc_user_id', $request->user->uid) + ->where('fin_payment_apply.receipt_id', 0) + ->get([ + 'fin_payment_apply.id', + 'fin_payment_apply.created_at', + 'fin_payment_apply.doc_user_id', + 'fin_payment_apply.payment_entity', + 'fin_payment_apply.payment_account', + 'fin_payment_apply.currency_id', + 'sys_currency.currency_name', + 'fin_payment_apply.amount', + 'fin_payment_apply.apply_no' + ]); + foreach ($list as $key => &$val) { + $val->created_at = date('Y-m-d H:i:s', $val->created_at); + } + unset($val); } else { $twentyFourHoursAgo = time() - (24 * 3600); $list = Db::table('fin_payment_apply') - ->where('doc_user_id', $request->user->uid) - ->whereNotNull('submit_files') - ->whereJsonLength('submit_files', '>', 0) - ->where('receipt_id', '>', 0) - ->where('claimed_at', '<', $twentyFourHoursAgo) - ->select( - 'apply_no', - 'amount', - 'created_at' - ) - ->get() - ->map(function($item) { - return [ - 'apply_no' => $item->apply_no, - 'amount' => $item->amount, - 'created_at' => date('Y-m-d H:i:s', $item->created_at) - ]; - }) - ->toArray(); + ->join('user', 'fin_payment_apply.doc_user_id', '=', 'user.id') + ->join('sys_currency', 'fin_payment_apply.currency_id', '=', 'sys_currency.id') + ->where('fin_payment_apply.doc_user_id', $request->user->uid) + ->whereNotNull('fin_payment_apply.submit_files') + ->whereJsonLength('fin_payment_apply.submit_files', '>', 0) + ->where('fin_payment_apply.receipt_id', '>', 0) + ->where('fin_payment_apply.claimed_at', '<', $twentyFourHoursAgo) + ->get([ + 'fin_payment_apply.id', + 'fin_payment_apply.created_at', + 'fin_payment_apply.doc_user_id', + 'user.name', + 'fin_payment_apply.payment_entity', + 'fin_payment_apply.payment_account', + 'fin_payment_apply.currency_id', + 'sys_currency.currency_name', + 'fin_payment_apply.amount', + 'fin_payment_apply.apply_no' + ]); + + foreach ($list as $key => &$val) { + $stockIds = Db::table('fin_payment_stock') + ->where('payment_apply_id', $val->id) + ->pluck('stock_id') + ->toArray(); + $val->order_no = Db::table('stock_orders') + ->whereIn('id', $stockIds) + ->pluck('order_no') + ->toArray(); + $val->created_at = date('Y-m-d H:i:s', $val->created_at); + } + unset($val); } return $this->success($list); } From 94e9d6eb7e8b294e1390fb9e409b8f5a8a2d946c Mon Sep 17 00:00:00 2001 From: mzl Date: Thu, 30 Oct 2025 16:51:58 +0800 Subject: [PATCH 051/110] xg --- app/admin/controller/dashboard/OfficeClerkController.php | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/app/admin/controller/dashboard/OfficeClerkController.php b/app/admin/controller/dashboard/OfficeClerkController.php index 6fa70ee..f1762c4 100644 --- a/app/admin/controller/dashboard/OfficeClerkController.php +++ b/app/admin/controller/dashboard/OfficeClerkController.php @@ -39,8 +39,7 @@ class OfficeClerkController extends BaseController $twentyFourHoursAgo = time() - (24 * 3600); $timeout = Db::table('fin_payment_apply') ->where('doc_user_id', $request->user->uid) - ->whereNotNull('submit_files') - ->whereJsonLength('submit_files', '>', 0) + ->where('fin_payment_apply.submit_files', '[]') ->where('receipt_id', '>', 0) ->where('claimed_at', '<', $twentyFourHoursAgo) ->count('id'); @@ -144,8 +143,7 @@ class OfficeClerkController extends BaseController ->join('user', 'fin_payment_apply.doc_user_id', '=', 'user.id') ->join('sys_currency', 'fin_payment_apply.currency_id', '=', 'sys_currency.id') ->where('fin_payment_apply.doc_user_id', $request->user->uid) - ->whereNotNull('fin_payment_apply.submit_files') - ->whereJsonLength('fin_payment_apply.submit_files', '>', 0) + ->where('fin_payment_apply.submit_files', '[]') ->where('fin_payment_apply.receipt_id', '>', 0) ->where('fin_payment_apply.claimed_at', '<', $twentyFourHoursAgo) ->get([ From 659f6d486694ecdcbb1c5a94a64643a44392f42a Mon Sep 17 00:00:00 2001 From: mzl Date: Thu, 30 Oct 2025 17:09:41 +0800 Subject: [PATCH 052/110] ts --- app/admin/controller/dashboard/OfficeClerkController.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/admin/controller/dashboard/OfficeClerkController.php b/app/admin/controller/dashboard/OfficeClerkController.php index f1762c4..9bf30d8 100644 --- a/app/admin/controller/dashboard/OfficeClerkController.php +++ b/app/admin/controller/dashboard/OfficeClerkController.php @@ -39,7 +39,7 @@ class OfficeClerkController extends BaseController $twentyFourHoursAgo = time() - (24 * 3600); $timeout = Db::table('fin_payment_apply') ->where('doc_user_id', $request->user->uid) - ->where('fin_payment_apply.submit_files', '[]') + ->where('fin_payment_apply.submit_files', '') ->where('receipt_id', '>', 0) ->where('claimed_at', '<', $twentyFourHoursAgo) ->count('id'); @@ -143,7 +143,7 @@ class OfficeClerkController extends BaseController ->join('user', 'fin_payment_apply.doc_user_id', '=', 'user.id') ->join('sys_currency', 'fin_payment_apply.currency_id', '=', 'sys_currency.id') ->where('fin_payment_apply.doc_user_id', $request->user->uid) - ->where('fin_payment_apply.submit_files', '[]') + ->where('fin_payment_apply.submit_files', '') ->where('fin_payment_apply.receipt_id', '>', 0) ->where('fin_payment_apply.claimed_at', '<', $twentyFourHoursAgo) ->get([ From a3819bcba7b4ba3ec47d6d143c84c07f7fb46513 Mon Sep 17 00:00:00 2001 From: "WIN-C27C5MSVTPO\\16961" <1696136552@qq.com> Date: Thu, 30 Oct 2025 17:21:10 +0800 Subject: [PATCH 053/110] =?UTF-8?q?=E5=85=A5=E8=B4=A6=E8=B5=84=E6=96=99?= =?UTF-8?q?=E9=99=84=E4=BB=B6=E6=A0=BC=E5=BC=8F=E5=A4=84=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/common/controller/FinPaymentApplyController.php | 9 +++++++-- app/common/model/FinReceiptHis.php | 2 +- app/common/service/FinPaymentApplyService.php | 11 ++++++----- app/process/Task.php | 9 ++------- 4 files changed, 16 insertions(+), 15 deletions(-) diff --git a/app/common/controller/FinPaymentApplyController.php b/app/common/controller/FinPaymentApplyController.php index 6f29595..1db09d3 100644 --- a/app/common/controller/FinPaymentApplyController.php +++ b/app/common/controller/FinPaymentApplyController.php @@ -47,7 +47,7 @@ class FinPaymentApplyController extends BaseController if (isset($data['currency_id'])) //付款币种 $data['currency_name'] = SysCurrency::where('id', $data['currency_id'])->value('currency_name')??''; $OssService = new OssService(); - if (isset($data['submit_files'])){ + if (!empty($data['submit_files'])){ $data['submit_files'] = json_decode($data['submit_files'], true); foreach ($data['submit_files'] as $key => $value) { $data['submit_files'][$key] = [ @@ -56,6 +56,8 @@ class FinPaymentApplyController extends BaseController 'file_name' => $value['file_name'], ]; } + }else{ + $data['submit_files'] = []; } if (!empty($data['voucher_file'])) { $data['voucher_file_full'] = $OssService->generatePrivateUrl($data['voucher_file']); @@ -106,7 +108,7 @@ class FinPaymentApplyController extends BaseController if (isset($data['currency_id'])) //付款币种 $data['currency_name'] = SysCurrency::where('id', $data['currency_id'])->value('currency_name')??''; $OssService = new OssService(); - if (isset($data['submit_files'])){ + if (!empty($data['submit_files'])){ $data['submit_files'] = json_decode($data['submit_files'], true); foreach ($data['submit_files'] as $key => $value) { $data['submit_files'][$key] = [ @@ -115,7 +117,10 @@ class FinPaymentApplyController extends BaseController 'file_name' => $value['file_name'], ]; } + }else{ + $data['submit_files'] = []; } + if (!empty($data['voucher_file'])) { $data['voucher_file_full'] = $OssService->generatePrivateUrl($data['voucher_file']); } elseif (isset($data['voucher_file'])) { diff --git a/app/common/model/FinReceiptHis.php b/app/common/model/FinReceiptHis.php index 814b87f..8a036b3 100644 --- a/app/common/model/FinReceiptHis.php +++ b/app/common/model/FinReceiptHis.php @@ -40,7 +40,7 @@ class FinReceiptHis extends Model /** * 允许自动填充模型字段 */ - protected $fillable = ['id', 'receipt_id', 'receipt_no', 'payment_apply_id', 'country_id', 'remitter', 'account_id', 'currency_id', 'amount', 'discount_amount', 'total_amount_cny', 'exchange_rate', 'arrived_at', 'received_at', 'voucher_no', 'fx_receipt_file', 'is_domestic_return', 'created_by', 'updated_by', 'created_at', 'updated_at']; + protected $fillable = ['id', 'receipt_id', 'receipt_no', 'payment_apply_id', 'country_id', 'remitter', 'account_id', 'currency_id', 'amount', 'discount_amount', 'total_amount_cny', 'exchange_rate', 'arrived_at', 'received_at', 'voucher_no', 'fx_receipt_file', 'is_domestic_return', 'created_by', 'updated_by', 'claimed_by', 'created_at', 'updated_at']; /** * 不允许自动填充模型字段 diff --git a/app/common/service/FinPaymentApplyService.php b/app/common/service/FinPaymentApplyService.php index ce2bbfd..f5f9ed3 100644 --- a/app/common/service/FinPaymentApplyService.php +++ b/app/common/service/FinPaymentApplyService.php @@ -90,6 +90,8 @@ class FinPaymentApplyService $data['submit_files'] = json_encode($data['submit_files']); redis::set('stock_' . $data['doc_user_id'], json_encode($request->post('stock')), 'EX', 30); unset($data['stock']); + if($data['submit_files'] == '[]' || $data['submit_files'] == '{}' ) + $data['submit_files'] = ''; return $data; } @@ -113,6 +115,8 @@ class FinPaymentApplyService public function beforeServiceUpdate($data, Request $request) { $data['submit_files'] = json_encode($data['submit_files']); + if($data['submit_files'] == '[]' || $data['submit_files'] == '{}' ) + $data['submit_files'] = ''; return $data; } @@ -174,10 +178,9 @@ class FinPaymentApplyService // 入账资料超时 if (isset($param['scene']) && $param['scene'] == 'file_timeout') { + $where[] = ['submit_files', '']; $where[] = ['claimed_at', '<', time()-24*60*60]; $where[] = ['claimed_at', '>', 1]; - $orWhere[] = ['submit_files', '[]']; - $orWhere[] = ['submit_files', '']; } // 关键字 @@ -187,9 +190,7 @@ class FinPaymentApplyService } // 构建查询 - $query = $this->modelClass::where($where)->where(function($query) use ($orWhere) { - $query->orWhere($orWhere); - }); + $query = $this->modelClass::where($where); // 是否已关联收款记录的筛选 $correlation = $param['correlation']??''; diff --git a/app/process/Task.php b/app/process/Task.php index 7175244..cf526c6 100644 --- a/app/process/Task.php +++ b/app/process/Task.php @@ -46,18 +46,13 @@ class Task return; } Log::info('fin_payment_apply submit_files start', [time()]); - $orWhere = [ - ['submit_files', '=', '[]'], - ['submit_files', '=', ''], - ]; $where = [ + ['submit_files', '=', ''], ['warn', '<', 2], ['created_at', '>', time() - 60 * 60 * 48], ['created_at', '<', time() - 60 * 60 * 24], ]; - $payment = FinPaymentApply::where($where)->where(function($query) use ($orWhere) { - $query->orWhere($orWhere); - })->orderBy('id', 'desc')->get(); + $payment = FinPaymentApply::where($where)->orderBy('id', 'desc')->get(); foreach ($payment as $item) { Log::info('fin_payment_apply submit_files ', [$item->toArray()]); $notice = [ From 0110e393ce4a55468843c2bd8b0aeb9094f7ef0b Mon Sep 17 00:00:00 2001 From: "WIN-C27C5MSVTPO\\16961" <1696136552@qq.com> Date: Thu, 30 Oct 2025 17:38:40 +0800 Subject: [PATCH 054/110] =?UTF-8?q?=E4=BB=98=E6=AC=BE=E5=8D=95=E5=85=B3?= =?UTF-8?q?=E8=81=94=E5=A4=87=E8=B4=A7=E5=8D=95=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/common/service/FinPaymentApplyService.php | 1 + 1 file changed, 1 insertion(+) diff --git a/app/common/service/FinPaymentApplyService.php b/app/common/service/FinPaymentApplyService.php index f5f9ed3..69fb4ce 100644 --- a/app/common/service/FinPaymentApplyService.php +++ b/app/common/service/FinPaymentApplyService.php @@ -114,6 +114,7 @@ class FinPaymentApplyService public function beforeServiceUpdate($data, Request $request) { + unset($data['stock']); $data['submit_files'] = json_encode($data['submit_files']); if($data['submit_files'] == '[]' || $data['submit_files'] == '{}' ) $data['submit_files'] = ''; From d73728128366ecf4f9d747fd05a4615271874a03 Mon Sep 17 00:00:00 2001 From: "WIN-C27C5MSVTPO\\16961" <1696136552@qq.com> Date: Thu, 30 Oct 2025 17:54:38 +0800 Subject: [PATCH 055/110] =?UTF-8?q?=E6=B0=B4=E5=8D=95=E5=A4=9A=E6=96=87?= =?UTF-8?q?=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/FinPaymentApplyController.php | 33 ++++++++++++++----- .../controller/FinReceiptController.php | 33 +++++++++++++++---- app/common/service/FinPaymentApplyService.php | 6 ++++ app/common/service/FinReceiptService.php | 6 +++- 4 files changed, 62 insertions(+), 16 deletions(-) diff --git a/app/common/controller/FinPaymentApplyController.php b/app/common/controller/FinPaymentApplyController.php index 1db09d3..0939646 100644 --- a/app/common/controller/FinPaymentApplyController.php +++ b/app/common/controller/FinPaymentApplyController.php @@ -59,11 +59,20 @@ class FinPaymentApplyController extends BaseController }else{ $data['submit_files'] = []; } - if (!empty($data['voucher_file'])) { - $data['voucher_file_full'] = $OssService->generatePrivateUrl($data['voucher_file']); - } elseif (isset($data['voucher_file'])) { - $data['voucher_file_full'] = ''; + + if (!empty($data['voucher_file'])){ + $data['voucher_file'] = json_decode($data['voucher_file'], true); + foreach ($data['voucher_file'] as $key => $value) { + $data['voucher_file'][$key] = [ + 'file_src' => $value['file_src'], + 'file_full' => $OssService->generatePrivateUrl($value['file_src']), + 'file_name' => $value['file_name'], + ]; + } + }else{ + $data['voucher_file'] = []; } + if (isset($data['created_at'])) $data['created_at'] = date('Y-m-d H:i:s', $data['created_at']); if (isset($data['updated_at'])) @@ -121,11 +130,19 @@ class FinPaymentApplyController extends BaseController $data['submit_files'] = []; } - if (!empty($data['voucher_file'])) { - $data['voucher_file_full'] = $OssService->generatePrivateUrl($data['voucher_file']); - } elseif (isset($data['voucher_file'])) { - $data['voucher_file_full'] = ''; + if (!empty($data['voucher_file'])){ + $data['voucher_file'] = json_decode($data['voucher_file'], true); + foreach ($data['voucher_file'] as $key => $value) { + $data['voucher_file'][$key] = [ + 'file_src' => $value['file_src'], + 'file_full' => $OssService->generatePrivateUrl($value['file_src']), + 'file_name' => $value['file_name'], + ]; + } + }else{ + $data['voucher_file'] = []; } + if (isset($data['created_at'])) $data['created_at'] = date('Y-m-d H:i:s', $data['created_at']); if (isset($data['updated_at'])) diff --git a/app/common/controller/FinReceiptController.php b/app/common/controller/FinReceiptController.php index 4e9c574..cade259 100644 --- a/app/common/controller/FinReceiptController.php +++ b/app/common/controller/FinReceiptController.php @@ -97,10 +97,19 @@ class FinReceiptController extends BaseController $data['country_name'] = Country::where('id', $data['country_id'])->value('cn_addr')??''; if (isset($data['currency_id'])) //付款币种 $data['currency_name'] = SysCurrency::where('id', $data['currency_id'])->value('currency_name')??''; + + $OssService = new OssService(); if (!empty($data['fx_receipt_file'])){ - $data['fx_receipt_file_full'] = (new OssService)->generatePrivateUrl($data['fx_receipt_file']); - }elseif (isset($data['fx_receipt_file'])){ - $data['fx_receipt_file_full'] = ''; + $data['fx_receipt_file'] = json_decode($data['fx_receipt_file'], true); + foreach ($data['fx_receipt_file'] as $key => $value) { + $data['fx_receipt_file'][$key] = [ + 'file_src' => $value['file_src'], + 'file_full' => $OssService->generatePrivateUrl($value['file_src']), + 'file_name' => $value['file_name'], + ]; + } + }else{ + $data['fx_receipt_file'] = []; } return $data; } @@ -129,12 +138,22 @@ class FinReceiptController extends BaseController $data['country_name'] = Country::where('id', $data['country_id'])->value('cn_addr')??''; if (isset($data['currency_id'])) //付款币种 $data['currency_name'] = SysCurrency::where('id', $data['currency_id'])->value('currency_name')??''; - if (!empty($data['fx_receipt_file'])) { - $data['fx_receipt_file_full'] = (new OssService)->generatePrivateUrl($data['fx_receipt_file']); - } elseif (isset($data['fx_receipt_file'])) { - $data['fx_receipt_file_full'] = ''; + + $OssService = new OssService(); + if (!empty($data['fx_receipt_file'])){ + $data['fx_receipt_file'] = json_decode($data['fx_receipt_file'], true); + foreach ($data['fx_receipt_file'] as $key => $value) { + $data['fx_receipt_file'][$key] = [ + 'file_src' => $value['file_src'], + 'file_full' => $OssService->generatePrivateUrl($value['file_src']), + 'file_name' => $value['file_name'], + ]; + } + }else{ + $data['fx_receipt_file'] = []; } + //查询关联的付款记录 if (!empty($data['payment_apply_id'])) { $paymentApply = FinPaymentApply::where('id', $data['payment_apply_id'])->first(); if(!$paymentApply) diff --git a/app/common/service/FinPaymentApplyService.php b/app/common/service/FinPaymentApplyService.php index 69fb4ce..0f6bec7 100644 --- a/app/common/service/FinPaymentApplyService.php +++ b/app/common/service/FinPaymentApplyService.php @@ -87,9 +87,12 @@ class FinPaymentApplyService { $data['apply_no'] = $this->generateUniqueApplyNo(); $data['doc_user_id'] = $request->user->uid; + $data['voucher_file'] = json_encode($data['voucher_file']); $data['submit_files'] = json_encode($data['submit_files']); redis::set('stock_' . $data['doc_user_id'], json_encode($request->post('stock')), 'EX', 30); unset($data['stock']); + if($data['voucher_file'] == '[]' || $data['voucher_file'] == '{}' ) + $data['voucher_file'] = ''; if($data['submit_files'] == '[]' || $data['submit_files'] == '{}' ) $data['submit_files'] = ''; return $data; @@ -118,6 +121,9 @@ class FinPaymentApplyService $data['submit_files'] = json_encode($data['submit_files']); if($data['submit_files'] == '[]' || $data['submit_files'] == '{}' ) $data['submit_files'] = ''; + $data['voucher_file'] = json_encode($data['voucher_file']); + if($data['voucher_file'] == '[]' || $data['voucher_file'] == '{}' ) + $data['voucher_file'] = ''; return $data; } diff --git a/app/common/service/FinReceiptService.php b/app/common/service/FinReceiptService.php index be72fc9..f55ac9a 100644 --- a/app/common/service/FinReceiptService.php +++ b/app/common/service/FinReceiptService.php @@ -114,6 +114,8 @@ class FinReceiptService $data['arrived_at'] = strtotime( $data['arrived_at']); $data['created_by'] = $request->user->uid; $data['updated_by'] = $request->user->uid; + if($data['fx_receipt_file'] == '[]' || $data['fx_receipt_file'] == '{}' ) + $data['fx_receipt_file'] = ''; return $data; } @@ -132,7 +134,9 @@ class FinReceiptService $data['arrived_at'] = strtotime($data['arrived_at']); if(isset($data['received_at'])) $data['received_at'] = strtotime($data['received_at']); - + + if($data['fx_receipt_file'] == '[]' || $data['fx_receipt_file'] == '{}' ) + $data['fx_receipt_file'] = ''; return $data; } public function afterServiceUpdate($res) From e9636513ec8c24ed17fd96ae13adc26b1690a94e Mon Sep 17 00:00:00 2001 From: mzl Date: Thu, 30 Oct 2025 17:55:08 +0800 Subject: [PATCH 056/110] =?UTF-8?q?=E4=B8=B4=E6=97=B6=E5=A4=84=E7=90=86?= =?UTF-8?q?=E4=BB=AA=E8=A1=A8=E7=9B=98=E6=98=BE=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/common/logic/AuthLogic.php | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/app/common/logic/AuthLogic.php b/app/common/logic/AuthLogic.php index ee449b9..9abe5e1 100644 --- a/app/common/logic/AuthLogic.php +++ b/app/common/logic/AuthLogic.php @@ -172,7 +172,7 @@ class AuthLogic $payload = $this->buildJwtPayload($user, $system); $token = $this->generateAccessToken($payload); $refreshToken = $this->generateRefreshToken($user->id, $data['system_code']); - $homeUrl = $this->getHome($system->id); + $homeUrl = $this->getHome($system->id, $payload); // 记录 token 到 Redis 和 MySQL $this->recordUserToken($user->id, $token, $data['system_code']); @@ -537,8 +537,22 @@ class AuthLogic } - public function getHome($systemId) + public function getHome($systemId, $data) { + // 2025.10.30 临时处理 ====== start + if ($systemId == 1) { + if (in_array(1, $data['role_ids'])) { + return '/dashboard/console'; + } + if (count($data['role_ids']) == 1 && in_array(5, $data['role_ids'])) { + return '/dashboard/console/backoffice'; + } + if (count($data['role_ids']) == 1 && in_array(6, $data['role_ids'])) { + return '/dashboard/console/finance'; + } + } + // 2025.10.30 临时处理 ====== end + $info = Resource::where('system_id', $systemId) ->where('is_home', 1) ->first(); From ad67a88d0f1062760ceb8332702d5e96d4748c0a Mon Sep 17 00:00:00 2001 From: "WIN-C27C5MSVTPO\\16961" <1696136552@qq.com> Date: Thu, 30 Oct 2025 18:11:50 +0800 Subject: [PATCH 057/110] =?UTF-8?q?=E6=B0=B4=E5=8D=95=E5=A4=9A=E6=96=87?= =?UTF-8?q?=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/common/service/FinReceiptService.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/common/service/FinReceiptService.php b/app/common/service/FinReceiptService.php index f55ac9a..676ab0f 100644 --- a/app/common/service/FinReceiptService.php +++ b/app/common/service/FinReceiptService.php @@ -114,6 +114,7 @@ class FinReceiptService $data['arrived_at'] = strtotime( $data['arrived_at']); $data['created_by'] = $request->user->uid; $data['updated_by'] = $request->user->uid; + $data['fx_receipt_file'] = json_encode($data['fx_receipt_file']); if($data['fx_receipt_file'] == '[]' || $data['fx_receipt_file'] == '{}' ) $data['fx_receipt_file'] = ''; return $data; @@ -135,6 +136,7 @@ class FinReceiptService if(isset($data['received_at'])) $data['received_at'] = strtotime($data['received_at']); + $data['fx_receipt_file'] = json_encode($data['fx_receipt_file']); if($data['fx_receipt_file'] == '[]' || $data['fx_receipt_file'] == '{}' ) $data['fx_receipt_file'] = ''; return $data; From 72fa638cf56877a56bc484af1970b8ff28838df1 Mon Sep 17 00:00:00 2001 From: "WIN-C27C5MSVTPO\\16961" <1696136552@qq.com> Date: Thu, 30 Oct 2025 18:37:40 +0800 Subject: [PATCH 058/110] =?UTF-8?q?=E6=8E=92=E5=BA=8F=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/common/service/FinPaymentApplyService.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/app/common/service/FinPaymentApplyService.php b/app/common/service/FinPaymentApplyService.php index 0f6bec7..78d0d37 100644 --- a/app/common/service/FinPaymentApplyService.php +++ b/app/common/service/FinPaymentApplyService.php @@ -233,12 +233,12 @@ class FinPaymentApplyService } if (!empty($remitter)) { - $similarityOrder[] = "CASE WHEN remitter = {$remitter} THEN 0 ELSE 1 END"; + $similarityOrder[] = "CASE WHEN remitter = '{$remitter}' THEN 0 ELSE 1 END"; } if (!empty($amount)) { $amount = (float)$amount; - $similarityOrder[] = "ABS(amount - {$amount}) ASC"; + $similarityOrder[] = "ABS(amount - {$amount})"; $similarityOrder[] = "CASE WHEN (ABS(amount - {$amount}) / {$amount} * 100) <= 5 THEN 0 ELSE 1 END"; } @@ -252,9 +252,9 @@ class FinPaymentApplyService END ASC, "; } - $orderByClause .= "CASE - WHEN created_at > {$sevenDaysAgo} THEN id - ELSE 999999 + $orderByClause .= "CASE + WHEN created_at > {$sevenDaysAgo} THEN id + ELSE 999999 END DESC, "; $orderByClause .= "id DESC"; From 206eff87e2b2a73ac5474371eef0360c8df8e224 Mon Sep 17 00:00:00 2001 From: "WIN-C27C5MSVTPO\\16961" <1696136552@qq.com> Date: Thu, 30 Oct 2025 18:49:28 +0800 Subject: [PATCH 059/110] =?UTF-8?q?=E9=99=84=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/common/service/FinPaymentApplyService.php | 8 ++++---- app/common/service/FinReceiptService.php | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/app/common/service/FinPaymentApplyService.php b/app/common/service/FinPaymentApplyService.php index 78d0d37..e55b6a7 100644 --- a/app/common/service/FinPaymentApplyService.php +++ b/app/common/service/FinPaymentApplyService.php @@ -91,9 +91,9 @@ class FinPaymentApplyService $data['submit_files'] = json_encode($data['submit_files']); redis::set('stock_' . $data['doc_user_id'], json_encode($request->post('stock')), 'EX', 30); unset($data['stock']); - if($data['voucher_file'] == '[]' || $data['voucher_file'] == '{}' ) + if($data['voucher_file'] == '[]' || $data['voucher_file'] == '{}' || $data['voucher_file'] == '""') $data['voucher_file'] = ''; - if($data['submit_files'] == '[]' || $data['submit_files'] == '{}' ) + if($data['submit_files'] == '[]' || $data['submit_files'] == '{}' || $data['submit_files'] == '""') $data['submit_files'] = ''; return $data; } @@ -119,10 +119,10 @@ class FinPaymentApplyService { unset($data['stock']); $data['submit_files'] = json_encode($data['submit_files']); - if($data['submit_files'] == '[]' || $data['submit_files'] == '{}' ) + if($data['submit_files'] == '[]' || $data['submit_files'] == '{}' || $data['submit_files'] == '""') $data['submit_files'] = ''; $data['voucher_file'] = json_encode($data['voucher_file']); - if($data['voucher_file'] == '[]' || $data['voucher_file'] == '{}' ) + if($data['voucher_file'] == '[]' || $data['voucher_file'] == '{}' || $data['voucher_file'] == '""') $data['voucher_file'] = ''; return $data; } diff --git a/app/common/service/FinReceiptService.php b/app/common/service/FinReceiptService.php index 676ab0f..775d77d 100644 --- a/app/common/service/FinReceiptService.php +++ b/app/common/service/FinReceiptService.php @@ -115,7 +115,7 @@ class FinReceiptService $data['created_by'] = $request->user->uid; $data['updated_by'] = $request->user->uid; $data['fx_receipt_file'] = json_encode($data['fx_receipt_file']); - if($data['fx_receipt_file'] == '[]' || $data['fx_receipt_file'] == '{}' ) + if($data['fx_receipt_file'] == '[]' || $data['fx_receipt_file'] == '{}' || $data['fx_receipt_file'] == '""') $data['fx_receipt_file'] = ''; return $data; } @@ -137,7 +137,7 @@ class FinReceiptService $data['received_at'] = strtotime($data['received_at']); $data['fx_receipt_file'] = json_encode($data['fx_receipt_file']); - if($data['fx_receipt_file'] == '[]' || $data['fx_receipt_file'] == '{}' ) + if($data['fx_receipt_file'] == '[]' || $data['fx_receipt_file'] == '{}' || $data['fx_receipt_file'] == '""') $data['fx_receipt_file'] = ''; return $data; } From 24805257c717c98916deb032c05be21b73fbf421 Mon Sep 17 00:00:00 2001 From: mzl Date: Fri, 31 Oct 2025 09:46:57 +0800 Subject: [PATCH 060/110] =?UTF-8?q?=E8=B0=83=E6=95=B4=E5=86=85=E5=8B=A4?= =?UTF-8?q?=E4=BB=AA=E8=A1=A8=E7=9B=98=E6=9B=B2=E7=BA=BF=E6=9F=A5=E8=AF=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dashboard/OfficeClerkController.php | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/app/admin/controller/dashboard/OfficeClerkController.php b/app/admin/controller/dashboard/OfficeClerkController.php index 9bf30d8..463a874 100644 --- a/app/admin/controller/dashboard/OfficeClerkController.php +++ b/app/admin/controller/dashboard/OfficeClerkController.php @@ -54,13 +54,18 @@ class OfficeClerkController extends BaseController } // 曲线 - public function curve() + public function curve(Request $request) { - $groupIds = Db::table('user_group') - ->where('scenario_code', 'stock_orders') - ->pluck('id'); + $groupId = Db::table('user_group_members as ugm') + ->join('user_group as ug', 'ugm.group_id', '=', 'ug.id') + ->where('ugm.user_id', $request->user->uid) + ->where('ugm.is_leader', 1) + ->where('ugm.status', 1) + ->where('ug.scenario_code', 'stock_orders') + ->where('ug.status', 1) + ->value('ug.id'); $userIds = Db::table('user_group_members') - ->whereIn('group_id', $groupIds) + ->where('group_id', $groupId) ->pluck('user_id'); $list = Db::table('stock_orders') ->join('user', 'stock_orders.salesperson_id', '=', 'user.id') From 43fe263df1e441047237cded74ea391f7e1b0eb5 Mon Sep 17 00:00:00 2001 From: "WIN-C27C5MSVTPO\\16961" <1696136552@qq.com> Date: Fri, 31 Oct 2025 10:17:41 +0800 Subject: [PATCH 061/110] =?UTF-8?q?=E5=A4=87=E8=B4=A7=E5=8D=95=E7=BC=96?= =?UTF-8?q?=E5=8F=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/common/controller/FinPaymentApplyController.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/common/controller/FinPaymentApplyController.php b/app/common/controller/FinPaymentApplyController.php index 0939646..d58c1d6 100644 --- a/app/common/controller/FinPaymentApplyController.php +++ b/app/common/controller/FinPaymentApplyController.php @@ -5,6 +5,7 @@ namespace app\common\controller; use app\common\controller\BaseController; use app\common\model\Country; use app\common\model\FinPaymentStock; +use app\common\model\StockOrders; use app\common\model\SysAccount; use app\common\model\SysCurrency; use app\common\model\SysDictItem; @@ -84,11 +85,15 @@ class FinPaymentApplyController extends BaseController if (isset($data['id'])){ $finPaymentStock = FinPaymentStock::where('payment_apply_id', $data['id'])->get(); $stock = []; + $stock_order_no = []; foreach ($finPaymentStock as $v) { $stock[$v['pi_no']]['pi_no'] = $v['pi_no']; $stock[$v['pi_no']]['stock_id'][] = [$v['stock_id']]; + $stockOrders = StockOrders::where('id', $v['stock_id'])->first(); + $stock_order_no[] = $stockOrders['order_no']; } $data['stock'] = array_values($stock); + $data['stock_order_no'] = $stock_order_no; } return $data; From 29ba35f9b6309c04c9c2beac1420d71963e64d45 Mon Sep 17 00:00:00 2001 From: "WIN-C27C5MSVTPO\\16961" <1696136552@qq.com> Date: Fri, 31 Oct 2025 16:22:28 +0800 Subject: [PATCH 062/110] =?UTF-8?q?=E6=94=B6=E4=BB=98=E6=AC=BE=E7=BB=9F?= =?UTF-8?q?=E8=AE=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dashboard/FinStatisticsController.php | 83 +++++++++++++++++-- 1 file changed, 76 insertions(+), 7 deletions(-) diff --git a/app/admin/controller/dashboard/FinStatisticsController.php b/app/admin/controller/dashboard/FinStatisticsController.php index 95cf09d..59965da 100644 --- a/app/admin/controller/dashboard/FinStatisticsController.php +++ b/app/admin/controller/dashboard/FinStatisticsController.php @@ -3,7 +3,9 @@ namespace app\admin\controller\dashboard; use app\common\controller\BaseController; +use app\common\model\FinPaymentApply; use app\common\model\FinReceipt; +use app\common\model\SysCurrency; use support\Request; class FinStatisticsController extends BaseController @@ -16,33 +18,100 @@ class FinStatisticsController extends BaseController public function statistics(Request $request) { try { + $currency = SysCurrency::where('status',1)->get(); $query = FinReceipt::where('payment_apply_id','>',0); $result = []; //已认领数量 $result['claimed_num'] = $query->count(); //已认领金额 - $result['claimed_amount'] = $query->sum('amount'); +// $result['claimed_amount'] = $query->sum('amount'); + $result['claimed_amount'] = array_values($this->receiptStatisticsSum($query, $currency)); //各币种的已认领金额 + $result['claimed_amount_sum'] =$this->amountSum($result['claimed_amount']); //已认领的人民币总金额 //未入账数量 - $result['unreceived_num'] = FinReceipt::whereNull('received_at')->count(); + $query2 = FinReceipt::whereNull('received_at'); + $result['unreceived_num'] = $query2->count(); //未入账金额 - $result['unreceived_amount'] = FinReceipt::whereNull('received_at')->sum('amount'); +// $result['unreceived_amount'] = $query2->sum('amount'); + $result['unreceived_amount'] = array_values($this->receiptStatisticsSum($query2, $currency)); //各币种的未入账金额 + $result['unreceived_amount_sum'] = $this->amountSum($result['unreceived_amount']); //未入账的已认领的人民币总金额 - $query = FinReceipt::whereNull('payment_apply_id'); + $query3 = FinReceipt::whereNull('payment_apply_id'); //未认领数量 - $result['unclaimed_num'] = $query->count(); + $result['unclaimed_num'] = $query3->count(); //未认领金额 - $result['unclaimed_amount'] = $query->sum('amount'); +// $result['unclaimed_amount'] = $query3->sum('amount'); + $result['unclaimed_amount'] = $this->receiptStatisticsSum($query3, $currency); //各币种的未认领金额 + $result['unclaimed_amount_sum'] = $this->amountSum($result['unclaimed_amount']); //未认领的已认领的人民币总金额 //已超时未认领数量 - $result['timeout'] = $query->where('created_at','<',time()-24*3600)->count(); + $query4 = $query3->where('created_at','<',time()-24*3600); + $result['timeout'] = $query4->count(); + //在途资金 + //1、没有认领的付款资金 + $query5 = FinPaymentApply::where('receipt_id', '=', 0); + $unclaimed_payment_amount = $this->receiptStatisticsSum($query5, $currency); + foreach ($unclaimed_payment_amount as $key => &$value) { + $value['amount'] = $value['amount'] - $result['unclaimed_amount'][$key]['amount']; + } + $result['unclaimed_payment_amount'] = array_values($result['unclaimed_payment_amount']); + $result['unclaimed_payment_amount_sum'] = $this->amountSum($result['unclaimed_payment_amount']); + //在途资金 = 没有认领的付款资金-未认领的收款资金 + $result['intransit_amount'] = $result['unclaimed_payment_amount_sum'] - $result['unclaimed_amount_sum']; + + //在途数量 = 没有认领的付款数量-未认领的收款数量 + $result['intransit_count'] = count($result['unclaimed_payment_amount']) - count($result['unclaimed_amount']); + + //未认领金额重新组织索引 + $result['unclaimed_amount'] = array_values($result['unclaimed_amount']); return $this->success($result); } catch (\Throwable $e) { return $this->error($e->getMessage()); } } + /** + * 根据不同条件查询收款金额 + * @param $query + * @param $currency + * @return array + */ + public function receiptStatisticsSum($query, $currency) + { + $amount = []; + foreach ($currency as $k => $v) { + $amount[$v->currency_code] = [ + 'currency_code' => $v->currency_code, + 'exchange_rate' => $v->exchange_rate, + 'amount' => $query->where('currency_id',$v->id)->sum('amount'), + ]; + } + return $amount; + } + + /** + * 不同币种汇率换算之后求和 + * @param $query + * @param $currency + */ + public function amountSum($amount) + { + $amountSum = 0; + foreach ($amount as $v) { + // 转换为整数(乘以100避免浮点数问题) + $v['amount'] = (int) round($v['amount'] * 100); + $v['exchange_rate'] = (int) round($v['exchange_rate'] * 10000); // 汇率保留4位小数 + $amountSum += $v['amount'] * $v['exchange_rate']; + } + + // 转换回金额(除以1000000,因为汇率乘了10000) + $result = $amountSum / 1000000; + + // 四舍五入到2位小数 + return round($amountSum, 2); + } + } \ No newline at end of file From f7faf798091967fdbbc84faf48a970ae1a75c447 Mon Sep 17 00:00:00 2001 From: "WIN-C27C5MSVTPO\\16961" <1696136552@qq.com> Date: Sat, 1 Nov 2025 08:56:48 +0800 Subject: [PATCH 063/110] =?UTF-8?q?=E5=BE=85=E5=85=A5=E8=B4=A6=E5=88=97?= =?UTF-8?q?=E8=A1=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/common/model/FinPaymentApply.php | 9 +++++++++ app/common/model/FinReceipt.php | 18 +++++++++++++++++- app/common/service/FinPaymentApplyService.php | 7 +++++++ app/common/service/FinReceiptService.php | 17 ++++++++++++----- 4 files changed, 45 insertions(+), 6 deletions(-) diff --git a/app/common/model/FinPaymentApply.php b/app/common/model/FinPaymentApply.php index 0f4d157..8dbe4f9 100644 --- a/app/common/model/FinPaymentApply.php +++ b/app/common/model/FinPaymentApply.php @@ -62,4 +62,13 @@ class FinPaymentApply extends Model { return $this->belongsToMany(StockOrders::class, 'fin_payment_stock', 'payment_apply_id', 'stock_id'); } + + /** + * 查询 submit_files 不为空的记录 + */ + public function scopeWithNonEmptyFiles($query) + { + return $query->whereNotNull('submit_files') + ->whereRaw('JSON_LENGTH(submit_files) > 0'); + } } \ No newline at end of file diff --git a/app/common/model/FinReceipt.php b/app/common/model/FinReceipt.php index 7347be0..88d3090 100644 --- a/app/common/model/FinReceipt.php +++ b/app/common/model/FinReceipt.php @@ -40,7 +40,7 @@ class FinReceipt extends Model /** * 允许自动填充模型字段 */ - protected $fillable = ['id', 'receipt_no', 'payment_apply_id', 'country_id', 'remitter', 'account_id', 'currency_id', 'amount', 'discount_amount', 'total_amount_cny', 'exchange_rate', 'arrived_at', 'received_at', 'voucher_no', 'fx_receipt_file', 'fx_receipt_file_name', 'is_domestic_return', 'created_by', 'updated_by', 'created_at', 'updated_at']; + protected $fillable = ['id', 'receipt_no', 'payment_apply_id', 'country_id', 'remitter', 'account_id', 'currency_id', 'amount', 'discount_amount', 'total_amount_cny', 'exchange_rate', 'arrived_at', 'received_at', 'voucher_no', 'fx_receipt_file', 'fx_receipt_file_name', 'is_domestic_return', 'submit_files', 'created_by', 'updated_by', 'created_at', 'updated_at']; /** * 不允许自动填充模型字段 @@ -62,4 +62,20 @@ class FinReceipt extends Model { return $this->belongsToMany(StockOrders::class, 'fin_payment_stock', 'receipt_id', 'stock_id'); } + + /** + * 关联付款申请 + */ +// public function finPaymentApply() +// { +// return $this->hasOne(FinPaymentApply::class, 'receipt_id', 'id'); +// } + + /** + * 关联付款申请表 + */ + public function paymentApply() + { + return $this->belongsTo(FinPaymentApply::class, 'payment_apply_id', 'id'); + } } \ No newline at end of file diff --git a/app/common/service/FinPaymentApplyService.php b/app/common/service/FinPaymentApplyService.php index e55b6a7..e659a86 100644 --- a/app/common/service/FinPaymentApplyService.php +++ b/app/common/service/FinPaymentApplyService.php @@ -127,6 +127,13 @@ class FinPaymentApplyService return $data; } + + public function afterServiceUpdate($res, Request $request) + { + if($res->submit_files != '' && $res->receipt_id != 0) + FinReceipt::where('id', $res->receipt_id)->update(['submit_files' => $res->submit_files]); + } + /** * 生成唯一的付款申请编码 * 格式:PA + 日期 + 6位字母数字组合 diff --git a/app/common/service/FinReceiptService.php b/app/common/service/FinReceiptService.php index 775d77d..6dd69a7 100644 --- a/app/common/service/FinReceiptService.php +++ b/app/common/service/FinReceiptService.php @@ -218,17 +218,22 @@ class FinReceiptService // 构建查询 $query = $this->modelClass::where($where); - // 没有付款记录的收款记录 + // 收款记录绑定情况 if (isset($param['payment_apply']) && $param['payment_apply'] == 'not') { - $query->whereNull('payment_apply_id'); + $query->where('payment_apply_id', '0'); } elseif (isset($param['payment_apply']) && $param['payment_apply'] == 'already') { - $query->whereNotNull('payment_apply_id'); + $query->where('payment_apply_id', '>' ,'0'); } elseif (isset($param['payment_apply']) && $param['payment_apply'] == 'timeout') { //超时未认领 - $query->whereNull('payment_apply_id'); + $query->where('payment_apply_id', '0'); $query->where('created_at', '<' ,time() - 24*60*60); } - if (isset($param['scene']) && $param['scene'] == 'list') { + if($param['scene'] == 'pending'){ //待入账列表 + $query->where('payment_apply_id', '>' ,'0'); + $query->where('submit_files','!=',''); + } + + if (isset($param['scene']) && ($param['scene'] == 'list' ||$param['scene'] == 'pending')) { return $query->paginate($limit, ['*'], 'page', $page); } @@ -308,8 +313,10 @@ class FinReceiptService $payment = FinPaymentApply::where('id', $payment_apply_id)->first(); $receipt = FinReceipt::find($id); $data['discount_amount'] = $payment->amount - $receipt->amount ?? 0; + $data['submit_files'] = $payment->submit_files ?? ''; $updated = $this->logicUpdateBy($config, $data); return $this->postProcess(ucfirst($scene), $data); } + } \ No newline at end of file From 139d3509f44973f8468fe7bf28b4f4d4362c0047 Mon Sep 17 00:00:00 2001 From: "WIN-C27C5MSVTPO\\16961" <1696136552@qq.com> Date: Sat, 1 Nov 2025 09:45:41 +0800 Subject: [PATCH 064/110] =?UTF-8?q?=E4=BD=9C=E5=BA=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/common/controller/FinReceiptController.php | 16 ++++++++++++++++ app/common/model/FinReceipt.php | 2 ++ app/common/service/FinReceiptService.php | 13 +++++++++++++ config/route.php | 2 +- 4 files changed, 32 insertions(+), 1 deletion(-) diff --git a/app/common/controller/FinReceiptController.php b/app/common/controller/FinReceiptController.php index cade259..aeca0eb 100644 --- a/app/common/controller/FinReceiptController.php +++ b/app/common/controller/FinReceiptController.php @@ -41,6 +41,22 @@ class FinReceiptController extends BaseController } } + /** + * 确认入账 + * @param Request $request + * @return \support\Response + */ + public function delete(Request $request) + { + try { + $id = $request->input('id'); + $raw = $this->service->delete($id); + return $this->success($raw); + } catch (\Throwable $e) { + return $this->error($e->getMessage()); + } + } + public function show(Request $request) { try { diff --git a/app/common/model/FinReceipt.php b/app/common/model/FinReceipt.php index 88d3090..099697f 100644 --- a/app/common/model/FinReceipt.php +++ b/app/common/model/FinReceipt.php @@ -2,10 +2,12 @@ namespace app\common\model; +use Illuminate\Database\Eloquent\SoftDeletes; use support\Model; class FinReceipt extends Model { + use SoftDeletes; /** * The connection name for the model. * diff --git a/app/common/service/FinReceiptService.php b/app/common/service/FinReceiptService.php index 6dd69a7..5b04368 100644 --- a/app/common/service/FinReceiptService.php +++ b/app/common/service/FinReceiptService.php @@ -233,6 +233,12 @@ class FinReceiptService $query->where('submit_files','!=',''); } + if($param['scene'] == 'delete'){ //查询作废数据 + $query->onlyTrashed(); + }elseif ($param['scene'] == 'all'){ //查询包含作废数据的全量数据 + $query->withTrashed(); + } + if (isset($param['scene']) && ($param['scene'] == 'list' ||$param['scene'] == 'pending')) { return $query->paginate($limit, ['*'], 'page', $page); } @@ -293,6 +299,13 @@ class FinReceiptService return $res; } + + public function delete($id) + { + $res = FinReceipt::where('id', $id)->delete(); + return $res; + } + /** * 更新:绑定收款记录,计算折让金额 * @param Request $request diff --git a/config/route.php b/config/route.php index ec7d821..e313549 100644 --- a/config/route.php +++ b/config/route.php @@ -359,8 +359,8 @@ Route::group('/admin', function () { Route::any('/store', [app\common\controller\FinReceiptController::class, 'store']); Route::any('/update', [app\common\controller\FinReceiptController::class, 'update']); Route::any('/payment', [app\common\controller\FinReceiptController::class, 'payment']); - Route::any('/destroy', [app\common\controller\FinReceiptController::class, 'destroy']); Route::any('/credit', [app\common\controller\FinReceiptController::class, 'credit']); + Route::any('/delete', [app\common\controller\FinReceiptController::class, 'delete']); }); // 收款记录历史 Route::group('/receipt/his', function () { From 18eb3e7c5dd01ce2934395b9ee606d935279f854 Mon Sep 17 00:00:00 2001 From: "WIN-C27C5MSVTPO\\16961" <1696136552@qq.com> Date: Sat, 1 Nov 2025 11:32:10 +0800 Subject: [PATCH 065/110] =?UTF-8?q?=E7=BB=9F=E8=AE=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/admin/controller/dashboard/FinStatisticsController.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/admin/controller/dashboard/FinStatisticsController.php b/app/admin/controller/dashboard/FinStatisticsController.php index 59965da..3a66c8c 100644 --- a/app/admin/controller/dashboard/FinStatisticsController.php +++ b/app/admin/controller/dashboard/FinStatisticsController.php @@ -57,7 +57,7 @@ class FinStatisticsController extends BaseController foreach ($unclaimed_payment_amount as $key => &$value) { $value['amount'] = $value['amount'] - $result['unclaimed_amount'][$key]['amount']; } - $result['unclaimed_payment_amount'] = array_values($result['unclaimed_payment_amount']); + $result['unclaimed_payment_amount'] = array_values($unclaimed_payment_amount); $result['unclaimed_payment_amount_sum'] = $this->amountSum($result['unclaimed_payment_amount']); //在途资金 = 没有认领的付款资金-未认领的收款资金 $result['intransit_amount'] = $result['unclaimed_payment_amount_sum'] - $result['unclaimed_amount_sum']; From 6493bdebb115a3da14dd9f7c0a23267c44d782f9 Mon Sep 17 00:00:00 2001 From: "WIN-C27C5MSVTPO\\16961" <1696136552@qq.com> Date: Sat, 1 Nov 2025 11:34:49 +0800 Subject: [PATCH 066/110] =?UTF-8?q?=E5=BE=85=E5=85=A5=E8=B4=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/common/service/FinReceiptService.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/common/service/FinReceiptService.php b/app/common/service/FinReceiptService.php index 062d3ee..b93b074 100644 --- a/app/common/service/FinReceiptService.php +++ b/app/common/service/FinReceiptService.php @@ -228,14 +228,14 @@ class FinReceiptService $query->where('created_at', '<' ,time() - 24*60*60); } - if($param['scene'] == 'pending'){ //待入账列表 + if(isset($param['scene']) && $param['scene'] == 'pending'){ //待入账列表 $query->where('payment_apply_id', '>' ,'0'); $query->where('submit_files','!=',''); } - if($param['scene'] == 'delete'){ //查询作废数据 + if(isset($param['scene']) && $param['scene'] == 'delete'){ //查询作废数据 $query->onlyTrashed(); - }elseif ($param['scene'] == 'all'){ //查询包含作废数据的全量数据 + }elseif (isset($param['scene']) && $param['scene'] == 'all'){ //查询包含作废数据的全量数据 $query->withTrashed(); } From 74ac8941ea0717d01b321570693cd3e098b2e522 Mon Sep 17 00:00:00 2001 From: "WIN-C27C5MSVTPO\\16961" <1696136552@qq.com> Date: Sat, 1 Nov 2025 13:20:33 +0800 Subject: [PATCH 067/110] =?UTF-8?q?=E7=BB=9F=E8=AE=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dashboard/FinStatisticsController.php | 25 ++++++++++++++++--- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/app/admin/controller/dashboard/FinStatisticsController.php b/app/admin/controller/dashboard/FinStatisticsController.php index 3a66c8c..6901082 100644 --- a/app/admin/controller/dashboard/FinStatisticsController.php +++ b/app/admin/controller/dashboard/FinStatisticsController.php @@ -37,7 +37,17 @@ class FinStatisticsController extends BaseController $result['unreceived_amount'] = array_values($this->receiptStatisticsSum($query2, $currency)); //各币种的未入账金额 $result['unreceived_amount_sum'] = $this->amountSum($result['unreceived_amount']); //未入账的已认领的人民币总金额 - $query3 = FinReceipt::whereNull('payment_apply_id'); + //今日入账 + $query6 = FinReceipt::where('received_at','>',strtotime(date("Y-m-d"))); + //今日入账数量 + $result['received_num'] = $query6->count(); + //未入账金额 +// $result['unreceived_amount'] = $query2->sum('amount'); + $result['received_amount'] = array_values($this->receiptStatisticsSum($query6, $currency)); //各币种的未入账金额 + $result['received_amount_sum'] = $this->amountSum($result['unreceived_amount']); //未入账的已认领的人民币总金额 + + + $query3 = FinReceipt::where('payment_apply_id',0); //未认领数量 $result['unclaimed_num'] = $query3->count(); @@ -57,13 +67,20 @@ class FinStatisticsController extends BaseController foreach ($unclaimed_payment_amount as $key => &$value) { $value['amount'] = $value['amount'] - $result['unclaimed_amount'][$key]['amount']; } + $result['unclaimed_payment_num'] = $query5->count(); $result['unclaimed_payment_amount'] = array_values($unclaimed_payment_amount); $result['unclaimed_payment_amount_sum'] = $this->amountSum($result['unclaimed_payment_amount']); //在途资金 = 没有认领的付款资金-未认领的收款资金 - $result['intransit_amount'] = $result['unclaimed_payment_amount_sum'] - $result['unclaimed_amount_sum']; - + $result['intransit_amount_sum'] = $result['unclaimed_payment_amount_sum'] - $result['unclaimed_amount_sum']; + $intransit_amount_amount = []; + //在途资金各币种明细 + foreach ($unclaimed_payment_amount as $key => $value) { + $value['amount'] = $value['amount'] - $result['unclaimed_amount'][$key]['amount']; + $intransit_amount_amount[] = $value; + } + $result['intransit_amount_amount'] = $intransit_amount_amount; //在途数量 = 没有认领的付款数量-未认领的收款数量 - $result['intransit_count'] = count($result['unclaimed_payment_amount']) - count($result['unclaimed_amount']); + $result['intransit_count'] = $result['unclaimed_payment_num'] - $result['unclaimed_num']; //未认领金额重新组织索引 $result['unclaimed_amount'] = array_values($result['unclaimed_amount']); From 1094bc406f093040c3ab5a0e039fbf3390b7b0cc Mon Sep 17 00:00:00 2001 From: "WIN-C27C5MSVTPO\\16961" <1696136552@qq.com> Date: Sat, 1 Nov 2025 13:22:45 +0800 Subject: [PATCH 068/110] =?UTF-8?q?Revert=20"=E4=BD=9C=E5=BA=9F"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit 139d3509f44973f8468fe7bf28b4f4d4362c0047. --- app/common/controller/FinReceiptController.php | 16 ---------------- app/common/model/FinReceipt.php | 2 -- app/common/service/FinReceiptService.php | 13 ------------- config/route.php | 2 +- 4 files changed, 1 insertion(+), 32 deletions(-) diff --git a/app/common/controller/FinReceiptController.php b/app/common/controller/FinReceiptController.php index aeca0eb..cade259 100644 --- a/app/common/controller/FinReceiptController.php +++ b/app/common/controller/FinReceiptController.php @@ -41,22 +41,6 @@ class FinReceiptController extends BaseController } } - /** - * 确认入账 - * @param Request $request - * @return \support\Response - */ - public function delete(Request $request) - { - try { - $id = $request->input('id'); - $raw = $this->service->delete($id); - return $this->success($raw); - } catch (\Throwable $e) { - return $this->error($e->getMessage()); - } - } - public function show(Request $request) { try { diff --git a/app/common/model/FinReceipt.php b/app/common/model/FinReceipt.php index 099697f..88d3090 100644 --- a/app/common/model/FinReceipt.php +++ b/app/common/model/FinReceipt.php @@ -2,12 +2,10 @@ namespace app\common\model; -use Illuminate\Database\Eloquent\SoftDeletes; use support\Model; class FinReceipt extends Model { - use SoftDeletes; /** * The connection name for the model. * diff --git a/app/common/service/FinReceiptService.php b/app/common/service/FinReceiptService.php index 5b04368..6dd69a7 100644 --- a/app/common/service/FinReceiptService.php +++ b/app/common/service/FinReceiptService.php @@ -233,12 +233,6 @@ class FinReceiptService $query->where('submit_files','!=',''); } - if($param['scene'] == 'delete'){ //查询作废数据 - $query->onlyTrashed(); - }elseif ($param['scene'] == 'all'){ //查询包含作废数据的全量数据 - $query->withTrashed(); - } - if (isset($param['scene']) && ($param['scene'] == 'list' ||$param['scene'] == 'pending')) { return $query->paginate($limit, ['*'], 'page', $page); } @@ -299,13 +293,6 @@ class FinReceiptService return $res; } - - public function delete($id) - { - $res = FinReceipt::where('id', $id)->delete(); - return $res; - } - /** * 更新:绑定收款记录,计算折让金额 * @param Request $request diff --git a/config/route.php b/config/route.php index e313549..ec7d821 100644 --- a/config/route.php +++ b/config/route.php @@ -359,8 +359,8 @@ Route::group('/admin', function () { Route::any('/store', [app\common\controller\FinReceiptController::class, 'store']); Route::any('/update', [app\common\controller\FinReceiptController::class, 'update']); Route::any('/payment', [app\common\controller\FinReceiptController::class, 'payment']); + Route::any('/destroy', [app\common\controller\FinReceiptController::class, 'destroy']); Route::any('/credit', [app\common\controller\FinReceiptController::class, 'credit']); - Route::any('/delete', [app\common\controller\FinReceiptController::class, 'delete']); }); // 收款记录历史 Route::group('/receipt/his', function () { From 8674063e6e0a8254781ce1a26f8383310fedb291 Mon Sep 17 00:00:00 2001 From: "WIN-C27C5MSVTPO\\16961" <1696136552@qq.com> Date: Sat, 1 Nov 2025 13:36:18 +0800 Subject: [PATCH 069/110] =?UTF-8?q?=E4=BB=98=E6=AC=BE=E8=AE=B0=E5=BD=95?= =?UTF-8?q?=E4=BD=9C=E5=BA=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/FinPaymentApplyController.php | 16 ++++++++++++++++ app/common/model/FinPaymentApply.php | 4 +++- app/common/service/FinPaymentApplyService.php | 13 +++++++++++++ config/route.php | 2 +- 4 files changed, 33 insertions(+), 2 deletions(-) diff --git a/app/common/controller/FinPaymentApplyController.php b/app/common/controller/FinPaymentApplyController.php index d58c1d6..98c49c2 100644 --- a/app/common/controller/FinPaymentApplyController.php +++ b/app/common/controller/FinPaymentApplyController.php @@ -162,4 +162,20 @@ class FinPaymentApplyController extends BaseController $data['stock'] = array_values($stock); return $data; } + + /** + * 作废 + * @param Request $request + * @return \support\Response + */ + public function delete(Request $request) + { + try { + $id = $request->input('id'); + $raw = $this->service->delete($id); + return $this->success($raw); + } catch (\Throwable $e) { + return $this->error($e->getMessage()); + } + } } \ No newline at end of file diff --git a/app/common/model/FinPaymentApply.php b/app/common/model/FinPaymentApply.php index 8dbe4f9..c81ac0a 100644 --- a/app/common/model/FinPaymentApply.php +++ b/app/common/model/FinPaymentApply.php @@ -2,10 +2,12 @@ namespace app\common\model; +use Illuminate\Database\Eloquent\SoftDeletes; use support\Model; class FinPaymentApply extends Model { + use SoftDeletes; /** * The connection name for the model. * @@ -40,7 +42,7 @@ class FinPaymentApply extends Model /** * 允许自动填充模型字段 */ - protected $fillable = ['id', 'apply_no', 'doc_user_id', 'sales_user_id', 'corp_id', 'account_id', 'payment_account', 'payment_entity', 'entity_type', 'currency_id', 'amount', 'swift', 'remit_bank', 'country_id', 'remitter', 'exchange_rate', 'total_amount_cny', 'voucher_file', 'voucher_name', 'submit_files','receipt_id','claimed_at','warn', 'created_at', 'updated_at', 'claimed_by',]; + protected $fillable = ['id', 'apply_no', 'doc_user_id', 'sales_user_id', 'corp_id', 'account_id', 'payment_account', 'payment_entity', 'entity_type', 'currency_id', 'amount', 'swift', 'remit_bank', 'country_id', 'remitter', 'exchange_rate', 'total_amount_cny', 'voucher_file', 'voucher_name', 'submit_files','receipt_id','claimed_at','warn', 'created_at', 'updated_at', 'deleted_at', 'claimed_by', 'created_by',]; /** * 不允许自动填充模型字段 diff --git a/app/common/service/FinPaymentApplyService.php b/app/common/service/FinPaymentApplyService.php index e659a86..4567461 100644 --- a/app/common/service/FinPaymentApplyService.php +++ b/app/common/service/FinPaymentApplyService.php @@ -214,6 +214,12 @@ class FinPaymentApplyService $query->whereDoesntHave('receipt'); } + if($param['scene'] == 'delete'){ //查询作废数据 + $query->onlyTrashed(); + }elseif ($param['scene'] == 'all'){ //查询包含作废数据的全量数据 + $query->withTrashed(); + } + if ((isset($param['scene']) && $param['scene'] == 'list') || empty($param['receipt_id'])) { return $query->orderBy('id','desc')->paginate($limit, ['*'], 'page', $page); } @@ -271,4 +277,11 @@ class FinPaymentApplyService return $query->paginate($limit, ['*'], 'page', $page); } + public function delete($id) + { + $res = FinPaymentApply::where('id', $id)->delete(); + return $res; + } + + } \ No newline at end of file diff --git a/config/route.php b/config/route.php index ec7d821..c390d7a 100644 --- a/config/route.php +++ b/config/route.php @@ -378,7 +378,7 @@ Route::group('/admin', function () { Route::any('/show', [app\common\controller\FinPaymentApplyController::class, 'show']); Route::any('/store', [app\common\controller\FinPaymentApplyController::class, 'store']); Route::any('/update', [app\common\controller\FinPaymentApplyController::class, 'update']); - Route::any('/destroy', [app\common\controller\FinPaymentApplyController::class, 'destroy']); + Route::any('/delete', [app\common\controller\FinPaymentApplyController::class, 'delete']); }); // 系统配置 From 4dbd72acce839f711b70f8a8dad60cff0014dcb4 Mon Sep 17 00:00:00 2001 From: "WIN-C27C5MSVTPO\\16961" <1696136552@qq.com> Date: Sat, 1 Nov 2025 14:02:15 +0800 Subject: [PATCH 070/110] =?UTF-8?q?=E5=BE=85=E5=85=A5=E8=B4=A6=E4=B8=8E?= =?UTF-8?q?=E5=85=A5=E8=B4=A6=E5=AE=9E=E9=99=85=E6=B1=87=E7=8E=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/common/service/FinPaymentApplyService.php | 7 ++++++- app/common/service/FinReceiptService.php | 7 +++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/app/common/service/FinPaymentApplyService.php b/app/common/service/FinPaymentApplyService.php index 4567461..be467e5 100644 --- a/app/common/service/FinPaymentApplyService.php +++ b/app/common/service/FinPaymentApplyService.php @@ -214,13 +214,18 @@ class FinPaymentApplyService $query->whereDoesntHave('receipt'); } + if(isset($param['scene']) && $param['scene'] == 'pending'){ //待入账列表 + $query->where('receipt_id', '>' ,'0'); + $query->where('submit_files','!=',''); + } + if($param['scene'] == 'delete'){ //查询作废数据 $query->onlyTrashed(); }elseif ($param['scene'] == 'all'){ //查询包含作废数据的全量数据 $query->withTrashed(); } - if ((isset($param['scene']) && $param['scene'] == 'list') || empty($param['receipt_id'])) { + if ((isset($param['scene']) && ($param['scene'] == 'list' || $param['scene'] == 'pending'))|| empty($param['receipt_id']) ) { return $query->orderBy('id','desc')->paginate($limit, ['*'], 'page', $page); } diff --git a/app/common/service/FinReceiptService.php b/app/common/service/FinReceiptService.php index 6dd69a7..37b9c94 100644 --- a/app/common/service/FinReceiptService.php +++ b/app/common/service/FinReceiptService.php @@ -303,6 +303,11 @@ class FinReceiptService public function serviceUpdatePayment(Request $request, array $data = [], string $scene = 'payment') { $payment_apply_id = $request->input('payment_apply_id'); + $exchange_rate = $request->input('exchange_rate'); + if(empty($exchange_rate) || !is_numeric($exchange_rate)){ + throw new BusinessException('请输入汇率'); + } + $id = $request->input('id'); $data = $this->preProcess(ucfirst($scene), $data, $request); $config = $this->buildQueryConfig($request, $scene); @@ -314,6 +319,8 @@ class FinReceiptService $receipt = FinReceipt::find($id); $data['discount_amount'] = $payment->amount - $receipt->amount ?? 0; $data['submit_files'] = $payment->submit_files ?? ''; + $data['exchange_rate'] = $exchange_rate; + $data['total_amount_cny'] = round($data['amount']*$exchange_rate,2); $updated = $this->logicUpdateBy($config, $data); return $this->postProcess(ucfirst($scene), $data); From f1d5fae122bdeb66a7ed3db0439c91ac0eccab8b Mon Sep 17 00:00:00 2001 From: "WIN-C27C5MSVTPO\\16961" <1696136552@qq.com> Date: Sat, 1 Nov 2025 14:13:55 +0800 Subject: [PATCH 071/110] =?UTF-8?q?=E5=85=A5=E8=B4=A6=E8=AE=B0=E5=BD=95?= =?UTF-8?q?=E6=B1=87=E7=8E=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/common/controller/FinReceiptController.php | 6 +++++- app/common/logic/FinReceiptLogic.php | 4 ++-- app/common/service/FinReceiptService.php | 16 ++++++++-------- 3 files changed, 15 insertions(+), 11 deletions(-) diff --git a/app/common/controller/FinReceiptController.php b/app/common/controller/FinReceiptController.php index cade259..f0e0626 100644 --- a/app/common/controller/FinReceiptController.php +++ b/app/common/controller/FinReceiptController.php @@ -34,7 +34,11 @@ class FinReceiptController extends BaseController { try { $id = $request->input('id'); - $raw = $this->service->credit($id); + $exchange_rate = $request->input('exchange_rate'); + if(empty($exchange_rate) || !is_numeric($exchange_rate)){ + return $this->error('请输入汇率'); + } + $raw = $this->service->credit($id,$exchange_rate); return $this->success($raw); } catch (\Throwable $e) { return $this->error($e->getMessage()); diff --git a/app/common/logic/FinReceiptLogic.php b/app/common/logic/FinReceiptLogic.php index 62db50f..622663b 100644 --- a/app/common/logic/FinReceiptLogic.php +++ b/app/common/logic/FinReceiptLogic.php @@ -14,8 +14,8 @@ class FinReceiptLogic */ protected string $modelClass = FinReceipt::class; - public function credit($id) + public function credit($id, $data) { - return FinReceipt::where('id', $id)->update(['received_at' => time()]); + return FinReceipt::where('id', $id)->update($data); } } \ No newline at end of file diff --git a/app/common/service/FinReceiptService.php b/app/common/service/FinReceiptService.php index 37b9c94..0070585 100644 --- a/app/common/service/FinReceiptService.php +++ b/app/common/service/FinReceiptService.php @@ -282,13 +282,19 @@ class FinReceiptService return $query->paginate($limit, ['*'], 'page', $page); } - public function credit($id) + public function credit($id, $exchange_rate) { $finReceipt = FinReceipt::find($id); if(!empty($finReceipt->received_at)){ throw new BusinessException('该记录已将确认入账'); } - $res = $this->logic->credit($id); + + $data = [ + 'received_at' => time(), + 'exchange_rate' => $exchange_rate, + 'total_amount_cny' => round($finReceipt['amount']*$exchange_rate,2), + ]; + $res = $this->logic->credit($id,$data); $this->afterServiceUpdate(['id'=>$id]); return $res; } @@ -303,10 +309,6 @@ class FinReceiptService public function serviceUpdatePayment(Request $request, array $data = [], string $scene = 'payment') { $payment_apply_id = $request->input('payment_apply_id'); - $exchange_rate = $request->input('exchange_rate'); - if(empty($exchange_rate) || !is_numeric($exchange_rate)){ - throw new BusinessException('请输入汇率'); - } $id = $request->input('id'); $data = $this->preProcess(ucfirst($scene), $data, $request); @@ -319,8 +321,6 @@ class FinReceiptService $receipt = FinReceipt::find($id); $data['discount_amount'] = $payment->amount - $receipt->amount ?? 0; $data['submit_files'] = $payment->submit_files ?? ''; - $data['exchange_rate'] = $exchange_rate; - $data['total_amount_cny'] = round($data['amount']*$exchange_rate,2); $updated = $this->logicUpdateBy($config, $data); return $this->postProcess(ucfirst($scene), $data); From 56ad5517075843898a748a113d75cb64620e2cc7 Mon Sep 17 00:00:00 2001 From: "WIN-C27C5MSVTPO\\16961" <1696136552@qq.com> Date: Sat, 1 Nov 2025 14:44:47 +0800 Subject: [PATCH 072/110] =?UTF-8?q?=E6=94=B6=E6=B1=87=E5=87=AD=E8=AF=81?= =?UTF-8?q?=E5=80=92=E8=AE=A1=E6=97=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/common/controller/FinReceiptController.php | 9 ++++++++- app/common/model/FinPaymentApply.php | 2 +- app/common/service/FinPaymentApplyService.php | 1 + app/common/service/FinReceiptService.php | 6 ++++++ 4 files changed, 16 insertions(+), 2 deletions(-) diff --git a/app/common/controller/FinReceiptController.php b/app/common/controller/FinReceiptController.php index f0e0626..4c73b7a 100644 --- a/app/common/controller/FinReceiptController.php +++ b/app/common/controller/FinReceiptController.php @@ -12,6 +12,7 @@ use app\common\model\User; use app\common\service\OssService; use app\common\traits\crud\CrudControllerTrait; use app\common\service\FinReceiptService; +use support\Redis; use support\Request; class FinReceiptController extends BaseController @@ -85,14 +86,20 @@ class FinReceiptController extends BaseController { if (isset($data['created_at'])){ $data['claim_timeout'] = (($data['created_at'] < (time()-86400)) && empty($data['payment_apply_id'])) ?1:0; + $data['claim_timeout_time'] = date('Y-m-d H:i:s', $data['created_at']+86400); $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['arrived_at'])) $data['arrived_at'] = date('Y-m-d H:i:s', $data['arrived_at']); - if (isset($data['received_at'])) + if (isset($data['received_at'])){ $data['received_at'] = date('Y-m-d H:i:s', $data['received_at']); + $voucher_no_timeout_hour = redis::get('voucher_no_timeout_hour'); + if(!empty($voucher_no_timeout_hour)){ + $data['voucher_no_timeout_time'] = date('Y-m-d H:i:s', time()+(60*60*$voucher_no_timeout_hour)); + } + } if (isset($data['account_id'])) { //收款账号 $data['account_name'] = SysAccount::where('id', $data['account_id'])->value('account_name')??''; $data['account_short_desc'] = SysAccount::where('id', $data['account_id'])->value('short_desc')??''; diff --git a/app/common/model/FinPaymentApply.php b/app/common/model/FinPaymentApply.php index c81ac0a..af52c85 100644 --- a/app/common/model/FinPaymentApply.php +++ b/app/common/model/FinPaymentApply.php @@ -42,7 +42,7 @@ class FinPaymentApply extends Model /** * 允许自动填充模型字段 */ - protected $fillable = ['id', 'apply_no', 'doc_user_id', 'sales_user_id', 'corp_id', 'account_id', 'payment_account', 'payment_entity', 'entity_type', 'currency_id', 'amount', 'swift', 'remit_bank', 'country_id', 'remitter', 'exchange_rate', 'total_amount_cny', 'voucher_file', 'voucher_name', 'submit_files','receipt_id','claimed_at','warn', 'created_at', 'updated_at', 'deleted_at', 'claimed_by', 'created_by',]; + protected $fillable = ['id', 'apply_no', 'doc_user_id', 'sales_user_id', 'corp_id', 'account_id', 'payment_account', 'payment_entity', 'entity_type', 'currency_id', 'amount', 'swift', 'remit_bank', 'country_id', 'remitter', 'exchange_rate', 'total_amount_cny', 'voucher_file', 'voucher_name', 'submit_files','receipt_id','claimed_at','warn', 'received_at', 'created_at', 'updated_at', 'deleted_at', 'claimed_by', 'created_by',]; /** * 不允许自动填充模型字段 diff --git a/app/common/service/FinPaymentApplyService.php b/app/common/service/FinPaymentApplyService.php index be467e5..ed9267a 100644 --- a/app/common/service/FinPaymentApplyService.php +++ b/app/common/service/FinPaymentApplyService.php @@ -217,6 +217,7 @@ class FinPaymentApplyService if(isset($param['scene']) && $param['scene'] == 'pending'){ //待入账列表 $query->where('receipt_id', '>' ,'0'); $query->where('submit_files','!=',''); + $query->whereNotNull('received_at'); } if($param['scene'] == 'delete'){ //查询作废数据 diff --git a/app/common/service/FinReceiptService.php b/app/common/service/FinReceiptService.php index 0070585..5f6b480 100644 --- a/app/common/service/FinReceiptService.php +++ b/app/common/service/FinReceiptService.php @@ -10,6 +10,7 @@ use app\common\validate\FinReceiptValidate; use app\common\traits\crud\CrudServiceTrait; use app\common\model\FinReceipt; use support\exception\BusinessException; +use support\Redis; use support\Request; class FinReceiptService @@ -194,6 +195,10 @@ class FinReceiptService $page = (int)$request->get('current', 1); $param = $request->all(); + if(isset($param['voucher_no_timeout_hour'])){ + redis::set('voucher_no_timeout_hour', $param['voucher_no_timeout_hour'], 'EX', 30); + } + $where = []; // 汇款国家 @@ -295,6 +300,7 @@ class FinReceiptService 'total_amount_cny' => round($finReceipt['amount']*$exchange_rate,2), ]; $res = $this->logic->credit($id,$data); + FinPaymentApply::where('id', $finReceipt->payment_apply_id)->update(['received_at' => $data['received_at']]); $this->afterServiceUpdate(['id'=>$id]); return $res; } From 5b77b8ed834f2f98d192a79763fd11cb42cf1a0d Mon Sep 17 00:00:00 2001 From: "WIN-C27C5MSVTPO\\16961" <1696136552@qq.com> Date: Sat, 1 Nov 2025 14:59:41 +0800 Subject: [PATCH 073/110] =?UTF-8?q?=E5=BE=85=E5=85=A5=E8=B4=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/common/service/FinPaymentApplyService.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/common/service/FinPaymentApplyService.php b/app/common/service/FinPaymentApplyService.php index ed9267a..42cae9a 100644 --- a/app/common/service/FinPaymentApplyService.php +++ b/app/common/service/FinPaymentApplyService.php @@ -217,7 +217,7 @@ class FinPaymentApplyService if(isset($param['scene']) && $param['scene'] == 'pending'){ //待入账列表 $query->where('receipt_id', '>' ,'0'); $query->where('submit_files','!=',''); - $query->whereNotNull('received_at'); + $query->whereNull('received_at'); } if($param['scene'] == 'delete'){ //查询作废数据 From 2f8f7dccf9cbc82c5664532317a492b251515d2f Mon Sep 17 00:00:00 2001 From: mzl Date: Mon, 3 Nov 2025 18:07:56 +0800 Subject: [PATCH 074/110] =?UTF-8?q?=E8=B0=83=E6=95=B4=E5=A4=87=E8=B4=A7?= =?UTF-8?q?=E5=8D=95=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/StockOrdersController.php | 11 ++-- app/admin/controller/UserController.php | 5 ++ app/admin/service/UserService.php | 2 + app/common/model/StockOrdersFiles.php | 2 +- app/common/service/StockOrdersService.php | 56 +++++++++++++++++-- .../approval/service/ApprovalFlowService.php | 10 +++- app/crm/controller/StockOrdersController.php | 42 ++++++++------ 7 files changed, 98 insertions(+), 30 deletions(-) diff --git a/app/admin/controller/StockOrdersController.php b/app/admin/controller/StockOrdersController.php index 39806a5..47e694a 100644 --- a/app/admin/controller/StockOrdersController.php +++ b/app/admin/controller/StockOrdersController.php @@ -81,19 +81,22 @@ class StockOrdersController extends BaseController if (!isset($groupedFiles[$file['file_code']])) { $groupedFiles[$file['file_code']] = []; } - $groupedFiles[$file['file_code']][] = $file['file_url']; + $groupedFiles[$file['file_code']][] = [ + 'url' => $file['file_url'], + 'original_name' => $file['original_name'] + ]; } // 映射到对应字段 foreach ($fileTypes as $field => $fileCode) { if (isset($groupedFiles[$fileCode])) { - // 原始URL数组 + // 文件信息数组,包含url和original_name $data[$field] = $groupedFiles[$fileCode]; // 完整URL数组 $fullUrls = []; - foreach ($groupedFiles[$fileCode] as $url) { - $fullUrls[] = (new \app\common\service\OssService)->generatePrivateUrl($url); + foreach ($groupedFiles[$fileCode] as $fileInfo) { + $fullUrls[] = (new \app\common\service\OssService)->generatePrivateUrl($fileInfo['url']); } $data[$field . '_url'] = $fullUrls; } diff --git a/app/admin/controller/UserController.php b/app/admin/controller/UserController.php index 8c24c6c..e9ef8d4 100644 --- a/app/admin/controller/UserController.php +++ b/app/admin/controller/UserController.php @@ -118,6 +118,11 @@ class UserController extends BaseController $data['system_ids'][] = $system['id']; } } + if ($data['reportToInfo']) { + $data['report_to_name'] = $data['reportToInfo']['id'] == $data['id'] ? '/' : $data['reportToInfo']['name']; + } else { + $data['report_to_name'] = '-'; + } return $data; } diff --git a/app/admin/service/UserService.php b/app/admin/service/UserService.php index 7f2fadd..9d850a5 100644 --- a/app/admin/service/UserService.php +++ b/app/admin/service/UserService.php @@ -100,6 +100,8 @@ class UserService $config['with']['roles'][] = 'name'; $config['with']['systems'][] = 'id'; $config['with']['systems'][] = 'name'; + $config['with']['reportToInfo'][] = 'id'; + $config['with']['reportToInfo'][] = 'name'; break; case 'show': $config['where']['id'] = $p['id']; diff --git a/app/common/model/StockOrdersFiles.php b/app/common/model/StockOrdersFiles.php index 98daa3f..3b78e5c 100644 --- a/app/common/model/StockOrdersFiles.php +++ b/app/common/model/StockOrdersFiles.php @@ -40,7 +40,7 @@ class StockOrdersFiles extends Model /** * 允许自动填充模型字段 */ - protected $fillable = ['id', 'order_id', 'file_name', 'file_code', 'file_url', 'created_at', 'updated_at']; + protected $fillable = ['id', 'order_id', 'file_name', 'file_code', 'file_url', 'original_name', 'created_at', 'updated_at']; /** * 不允许自动填充模型字段 diff --git a/app/common/service/StockOrdersService.php b/app/common/service/StockOrdersService.php index 76d2ab5..d4187a1 100644 --- a/app/common/service/StockOrdersService.php +++ b/app/common/service/StockOrdersService.php @@ -161,7 +161,8 @@ class StockOrdersService { // 启动审批 $appro = new \app\common\utils\approval\service\ApprovalFlowService(); - $appro->start('stock_orders', 'stock_orders', $item->id, [], $item->salesperson_id); + $formData = $this->checkNewOrUsedVehicles($item->id); + $appro->start('stock_orders', 'stock_orders', $item->id, $formData, $item->salesperson_id); // 异步生成pdf文件 \Webman\RedisQueue\Redis::send('stock-order-pdf', ['id'=>$item->id]); } @@ -295,7 +296,8 @@ class StockOrdersService public function approvalNullify($item) { $appro = new \app\common\utils\approval\service\ApprovalFlowService(); - $appro->startNullify('stock_orders_nullify', 'stock_orders', $item->id, [], $item->salesperson_id); + $formData = $this->checkNewOrUsedVehicles($item->id); + $appro->startNullify('stock_orders_nullify', 'stock_orders', $item->id, $formData, $item->salesperson_id); } /** @@ -479,14 +481,19 @@ class StockOrdersService // 检查数据中是否存在该字段且为数组 if (!empty($data[$field]) && is_array($data[$field])) { // 处理该字段下的每个文件 - foreach ($data[$field] as $fileUrl) { + foreach ($data[$field] as $fileInfo) { $fileData = [ 'order_id' => $order->id, 'file_code' => $fileCode, - 'file_url' => $fileUrl, + 'file_url' => $fileInfo['url'] ?? '', 'file_name' => StockOrdersFiles::FILE_CODE[$fileCode], + 'original_name' => $fileInfo['original_name'] ?? StockOrdersFiles::FILE_CODE[$fileCode], ]; - StockOrdersFiles::create($fileData); + + // 只有当file_url不为空时才创建记录 + if (!empty($fileData['file_url'])) { + StockOrdersFiles::create($fileData); + } } } } @@ -905,6 +912,7 @@ class StockOrdersService 'id' => $file->id, 'file_url' => $file->file_url, 'file_full_url' => $this->getFileUrl($file->file_url), + 'original_name' => $file->original_name, ]; } @@ -978,4 +986,42 @@ class StockOrdersService } return (new \app\common\service\OssService)->generatePrivateUrl($fileUrl); } + + /** + * 检查备货单中的车辆是新车还是二手车 + * @param int $stockOrderId 备货单ID + * @return array 返回包含has_new和has_used的数组 + */ + public function checkNewOrUsedVehicles(int $stockOrderId): array + { + // 获取备货单关联的所有车辆项 + $vehicles = StockOrderItems::where('order_id', $stockOrderId)->get(); + + $hasNew = false; + $hasUsed = false; + $submitNote = ''; + + foreach ($vehicles as $vehicle) { + // 判断是否为二手车(mileage > 0) + if ($vehicle->mileage > 0) { + $hasUsed = true; + $submitNote = '本单只有二手车'; + } else { + $hasNew = true; + $submitNote = '本单只有新车'; + } + + // 如果两种类型都已经存在,可以提前退出循环 + if ($hasNew && $hasUsed) { + $submitNote = '本单包含新车和二手车'; + break; + } + } + + return [ + 'has_new' => $hasNew ? 1 : 0, + 'has_used' => $hasUsed ? 1 : 0, + 'submit_note' => $submitNote + ]; + } } \ No newline at end of file diff --git a/app/common/utils/approval/service/ApprovalFlowService.php b/app/common/utils/approval/service/ApprovalFlowService.php index 6eae8d8..a26b79a 100644 --- a/app/common/utils/approval/service/ApprovalFlowService.php +++ b/app/common/utils/approval/service/ApprovalFlowService.php @@ -68,7 +68,7 @@ class ApprovalFlowService } $this->sendNotification($approverIds, $instance, 'pending', false, $extra); - $this->notifyCCs($firstStep, $instance, 'pending'); + // $this->notifyCCs($firstStep, $instance, 'pending'); $this->pushTodoChanged($approverIds); @@ -193,6 +193,12 @@ class ApprovalFlowService 'updated_at' => $now, ]); $this->onFlowFinalized($instance, $finalStatus); + + // 新增:在流程最终完成时才发本步骤的抄送 + if (!empty($step['cc'])) { + $this->notifyCCs($step, $instance, $notifyAction); + } + $this->sendNotification([$instance->initiator_id], $instance, $notifyAction, false); $this->pushTodoChanged([$approverId]); return ['status' => 'completed', 'msg' => ($finalStatus === 'voided' ? '已作废' : '同意')]; @@ -215,7 +221,7 @@ class ApprovalFlowService $this->insertApprovalRecord($instance, $nextIndex, $uid); } $this->sendNotification($nextApprovers, $instance, 'pending', false); - $this->notifyCCs($nextStep, $instance, 'pending'); + // $this->notifyCCs($nextStep, $instance, 'pending'); $this->pushTodoChanged(array_merge([$approverId], $nextApprovers)); return ['status' => 'next_step', 'msg' => '进行下一步:'.$nextStep['name'] ?? ('Step '.$nextIndex)]; }); diff --git a/app/crm/controller/StockOrdersController.php b/app/crm/controller/StockOrdersController.php index 3716c69..3f6c6e4 100644 --- a/app/crm/controller/StockOrdersController.php +++ b/app/crm/controller/StockOrdersController.php @@ -61,43 +61,49 @@ class StockOrdersController extends BaseController 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']); - // 处理文件附件 + // 处理文件附件:只允许 _url 字段调用 OSS 生成私有链接 $fileTypes = [ 'contract' => \app\common\model\StockOrdersFiles::CONTRACT, 'transaction_slip' => \app\common\model\StockOrdersFiles::TRANSACTION_SLIP, 'pi_doc' => \app\common\model\StockOrdersFiles::PI_DOC ]; - + // 初始化文件字段 foreach (array_keys($fileTypes) as $field) { - $data[$field] = []; - $data[$field . '_url'] = []; + $data[$field] = []; // 存储原始文件信息(不包含签名链接) + $data[$field . '_url'] = []; // 存储通过OSS生成的私有链接 } - - // 按file_code分组文件 + + // 按 file_code 分组文件 if (!empty($data['files'])) { $groupedFiles = []; foreach ($data['files'] as $file) { - if (!isset($groupedFiles[$file['file_code']])) { - $groupedFiles[$file['file_code']] = []; + $code = $file['file_code']; + if (!isset($groupedFiles[$code])) { + $groupedFiles[$code] = []; } - $groupedFiles[$file['file_code']][] = $file['file_url']; + $groupedFiles[$code][] = [ + 'url' => $file['file_url'], // 原始路径 + 'original_name' => $file['original_name'] // 原始文件名 + ]; } - - // 映射到对应字段 + + // 映射到对应字段,并仅对 _url 调用 OSS 服务 foreach ($fileTypes as $field => $fileCode) { if (isset($groupedFiles[$fileCode])) { - // 原始URL数组 + // 主字段存储基本信息(无签名链接) $data[$field] = $groupedFiles[$fileCode]; - - // 完整URL数组 - $fullUrls = []; - foreach ($groupedFiles[$fileCode] as $url) { - $fullUrls[] = (new \app\common\service\OssService)->generatePrivateUrl($url); + + // 只有 *_url 字段才生成带签名的完整 URL + $signedUrls = []; + foreach ($groupedFiles[$fileCode] as $fileInfo) { + $signedUrls[] = (new \app\common\service\OssService)->generatePrivateUrl($fileInfo['url']); } - $data[$field . '_url'] = $fullUrls; + $data[$field . '_url'] = $signedUrls; } } + + // 清理临时数据 unset($data['files']); } From 42bf32a9a58a520c05f4e34559eaf4502e40b367 Mon Sep 17 00:00:00 2001 From: mzl Date: Tue, 4 Nov 2025 10:31:45 +0800 Subject: [PATCH 075/110] =?UTF-8?q?=E4=BF=AE=E6=AD=A3=E7=BC=96=E8=BE=91?= =?UTF-8?q?=E5=90=8E=E7=9A=84=E5=A4=87=E8=B4=A7=E5=8D=95=E7=BB=95=E8=BF=87?= =?UTF-8?q?=E6=96=87=E4=BB=B6=E4=B8=8A=E4=BC=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/common/service/StockOrdersService.php | 131 ++++++++++++++++++---- 1 file changed, 111 insertions(+), 20 deletions(-) diff --git a/app/common/service/StockOrdersService.php b/app/common/service/StockOrdersService.php index d4187a1..9d5e4df 100644 --- a/app/common/service/StockOrdersService.php +++ b/app/common/service/StockOrdersService.php @@ -73,6 +73,7 @@ class StockOrdersService case 'print': case 'approval': $config['where']['id'] = $p['id']; + $config['with'][] = 'files'; // 添加文件附件关联 break; case 'detail': case 'show': @@ -102,22 +103,14 @@ class StockOrdersService 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; - // } - + // 检查绑定 + (new UserService())->checkBind($userId); + // 保存不检查文件上传 + if ($data['approval_status'] != 0) { + $this->validateRequiredFiles($data); + } + // 终版备货单时,至少一个车辆填写了vin if ($data['is_final'] == 1) { - // 检查是否至少有一个车辆填写了vin $hasVin = false; foreach ($data['veh_list'] as $k => $v) { if ($v['vin'] != '') { @@ -125,17 +118,105 @@ class StockOrdersService break; } } - // 如果没有任何车辆填写vin,则抛出异常 if (!$hasVin) { throw new BusinessException('至少需要为一辆车辆填写vin,才能提交审核'); } } - - // 检查绑定 - (new UserService())->checkBind($userId); + $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; + $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 array $data 提交的数据 + * @throws BusinessException 当必需的文件缺失或为空时抛出异常 + */ + private function validateRequiredFiles(array $data): void + { + $requiredFiles = [ + 'contract' => '合同', + 'transaction_slip' => '水单', + 'pi_doc' => 'PI' + ]; + + foreach ($requiredFiles as $fileType => $fileLabel) { + if (!isset($data[$fileType]) || !is_array($data[$fileType]) || empty($data[$fileType])) { + throw new BusinessException("创建备货单时{$fileLabel}文件不能为空"); + } + + // 检查文件数组中的每个条目是否都有有效的URL + $hasValidFile = false; + foreach ($data[$fileType] as $fileInfo) { + if (isset($fileInfo['url']) && !empty($fileInfo['url'])) { + $hasValidFile = true; + break; + } + } + + if (!$hasValidFile) { + throw new BusinessException("创建备货单时{$fileLabel}文件不能为空"); + } + } + } + + /** + * 验证备货单审批时必需的文件是否存在 + * + * @param StockOrders $item 备货单对象 + * @throws BusinessException 当必需的文件缺失或为空时抛出异常 + */ + private function validateRequiredFilesForApproval($item): void + { + // 确保已加载文件关联 + if (!isset($item->files)) { + $item->load('files'); + } + + $requiredFiles = [ + \app\common\model\StockOrdersFiles::CONTRACT => '合同', + \app\common\model\StockOrdersFiles::TRANSACTION_SLIP => '水单', + \app\common\model\StockOrdersFiles::PI_DOC => 'PI' + ]; + + // 按文件类型分组现有文件 + $existingFiles = []; + foreach ($item->files as $file) { + if (!isset($existingFiles[$file->file_code])) { + $existingFiles[$file->file_code] = []; + } + $existingFiles[$file->file_code][] = $file; + } + + // 检查每种必需文件是否存在 + foreach ($requiredFiles as $fileCode => $fileLabel) { + if (!isset($existingFiles[$fileCode]) || empty($existingFiles[$fileCode])) { + throw new BusinessException("提交审批前必须上传{$fileLabel}文件"); + } + + // 检查文件中是否有有效的URL + $hasValidFile = false; + foreach ($existingFiles[$fileCode] as $file) { + if (!empty($file->file_url)) { + $hasValidFile = true; + break; + } + } + + if (!$hasValidFile) { + throw new BusinessException("提交审批前必须上传有效的{$fileLabel}文件"); + } + } + } + protected function afterServiceStore($res) { // 处理原单 @@ -271,10 +352,14 @@ class StockOrdersService public function serviceApproval(Request $request) { $config = $this->buildQueryConfig($request, 'approval'); + $item = $this->logicShow($config); + + // 检查备货单是否包含必需的文件 + $this->validateRequiredFilesForApproval($item); + $this->logicUpdateBy($config, [ 'approval_status' => 1 ]); - $item = $this->logicShow($config); $this->startApproval($item); return $this->postProcess('Approval', $item); } @@ -366,6 +451,9 @@ class StockOrdersService */ public function beforeServiceUpdate($data, Request $request) { + // 检查必填文件是否已上传 + // $this->validateRequiredFiles($data); + $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']); @@ -1024,4 +1112,7 @@ class StockOrdersService 'submit_note' => $submitNote ]; } + + + } \ No newline at end of file From be4dcb4dd4b5bd04439253a552466f59175b5c9a Mon Sep 17 00:00:00 2001 From: mzl Date: Tue, 4 Nov 2025 13:10:46 +0800 Subject: [PATCH 076/110] =?UTF-8?q?=E5=88=A0=E9=99=A4=E5=A2=9E=E5=8A=A0?= =?UTF-8?q?=E8=BF=87=E6=BB=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/common/service/StockOrdersService.php | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/app/common/service/StockOrdersService.php b/app/common/service/StockOrdersService.php index 9d5e4df..6da7a24 100644 --- a/app/common/service/StockOrdersService.php +++ b/app/common/service/StockOrdersService.php @@ -1114,5 +1114,17 @@ class StockOrdersService } - + public function serviceDestroy(Request $request, string $scene = 'destroy') + { + $config = $this->buildQueryConfig($request, $scene); + $item = $this->logicShow($config); + if (!$item) { + throw new BusinessException('数据不存在'); + } + if ($item->approval_status == 1 || $item->approval_status == 2) { + throw new BusinessException('审核中或者已审核的订单不能删除'); + } + $deleted = $this->logicDeleteBy($config); + return $this->postProcess(ucfirst($scene), $deleted); + } } \ No newline at end of file From 32452dc29a0e5bb7fa7bed22a5b080d61e7c8169 Mon Sep 17 00:00:00 2001 From: mzl Date: Tue, 4 Nov 2025 15:13:19 +0800 Subject: [PATCH 077/110] =?UTF-8?q?=E8=B0=83=E6=95=B4=E6=B1=BD=E8=BD=A6?= =?UTF-8?q?=E6=95=B0=E6=8D=AE=E9=87=87=E9=9B=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/admin/service/BrandSeriesService.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/admin/service/BrandSeriesService.php b/app/admin/service/BrandSeriesService.php index 323e117..594605b 100644 --- a/app/admin/service/BrandSeriesService.php +++ b/app/admin/service/BrandSeriesService.php @@ -254,10 +254,13 @@ class BrandSeriesService // 验证系列是否存在 $series = BrandSeries::where('id', $series_id)->first(); if (!$series) { - throw new \Exception("系列ID {$series_id} 不存在"); + throw new \Exception("车辆系列未收录,联系管理员处理"); } // 获取品牌信息 $brand = Brand::where('id', $series->brand_id)->first(); + if (!$brand) { + throw new \Exception("车辆品牌未收录,联系管理员处理"); + } // 保存车型数据 $createdCount = 0; From 6ab4505e17f8a8017ef5c3a37e29597bfd92381b Mon Sep 17 00:00:00 2001 From: mzl Date: Fri, 7 Nov 2025 15:27:16 +0800 Subject: [PATCH 078/110] =?UTF-8?q?=E5=AF=B9=E8=B4=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../FinanceReconDubaiFilesController.php | 33 ++ app/command/FinanceRecoCommand.php | 32 ++ app/common/controller/UploadController.php | 4 +- .../logic/FinanceReconDubaiFilesLogic.php | 17 + app/common/model/FinanceReconDubaiFiles.php | 49 +++ .../service/FinanceReconDubaiFilesService.php | 290 +++++++++++++++++ app/common/service/UploadService.php | 2 +- app/common/utils/excel/ExcelImportTrait.php | 133 ++++++++ app/common/utils/excel/ExcelReader.php | 292 ++++++++++++++++++ .../FinanceReconDubaiFilesValidate.php | 29 ++ app/controller/IndexController.php | 26 +- config/oss.php | 10 + config/route.php | 10 + 13 files changed, 917 insertions(+), 10 deletions(-) create mode 100644 app/admin/controller/FinanceReconDubaiFilesController.php create mode 100644 app/command/FinanceRecoCommand.php create mode 100644 app/common/logic/FinanceReconDubaiFilesLogic.php create mode 100644 app/common/model/FinanceReconDubaiFiles.php create mode 100644 app/common/service/FinanceReconDubaiFilesService.php create mode 100644 app/common/utils/excel/ExcelImportTrait.php create mode 100644 app/common/utils/excel/ExcelReader.php create mode 100644 app/common/validate/FinanceReconDubaiFilesValidate.php create mode 100644 config/oss.php diff --git a/app/admin/controller/FinanceReconDubaiFilesController.php b/app/admin/controller/FinanceReconDubaiFilesController.php new file mode 100644 index 0000000..cd85dce --- /dev/null +++ b/app/admin/controller/FinanceReconDubaiFilesController.php @@ -0,0 +1,33 @@ +service = new FinanceReconDubaiFilesService(); + } + + /** + * 列表页数据格式化 + * + * @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/command/FinanceRecoCommand.php b/app/command/FinanceRecoCommand.php new file mode 100644 index 0000000..1406ab9 --- /dev/null +++ b/app/command/FinanceRecoCommand.php @@ -0,0 +1,32 @@ +generatePrivateUrl('fdesk_test/20251104/20251104_财务.xlsx');; + + $service = new \app\common\service\FinanceReconDubaiFilesService(); + $result = $service->dImport('fdesk_test/20251104/20251104_迪拜.xlsx'); + + return self::SUCCESS; + } +} \ No newline at end of file diff --git a/app/common/controller/UploadController.php b/app/common/controller/UploadController.php index a025666..800090a 100644 --- a/app/common/controller/UploadController.php +++ b/app/common/controller/UploadController.php @@ -91,7 +91,7 @@ class UploadController $mimes = $ossservice->getMimeTypes(['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'txt', 'zip', 'rar']); $result = Storage::adapter('oss') - ->path('x_server/' . $sceneUrl . date('Ymd')) + ->path(config('oss.oss_main_dir') . $sceneUrl . date('Ymd')) ->size(1024*1024*5) ->extYes($mimes) ->upload($file); @@ -132,7 +132,7 @@ class UploadController } $result = Storage::adapter('oss') - ->path('x_server/' . $sceneUrl . date('Ymd')) + ->path(config('oss.oss_main_dir') . $sceneUrl . date('Ymd')) ->size(1024*1024*5) ->extYes(['image/jpeg','image/png']) ->extNo(['image/gif']) diff --git a/app/common/logic/FinanceReconDubaiFilesLogic.php b/app/common/logic/FinanceReconDubaiFilesLogic.php new file mode 100644 index 0000000..360f23f --- /dev/null +++ b/app/common/logic/FinanceReconDubaiFilesLogic.php @@ -0,0 +1,17 @@ +generatePrivateUrl() 完成 + */ +class FinanceReconDubaiFilesService +{ + use CrudServiceTrait; + use ExcelImportTrait; // 复用通用导入方法 + + /** 指定当前要操作的模型(用于 CrudServiceTrait) */ + protected string $modelClass = FinanceReconDubaiFiles::class; + + /** 验证器 */ + protected $validator = FinanceReconDubaiFilesValidate::class; + + /** 获取验证器实例 */ + public function getValidator() + { + return new $this->validator; + } + + /** + * 写入前置:将上传组件的结构转为 *_url / *_name,并记录创建人 + */ + public function beforeServiceStore($data, Request $request) + { + $data['created_by'] = $request->user->uid ?? 0; + $data['caiwu_file_url'] = $data['caiwu_file']['url'] ?? ''; + $data['caiwu_file_name'] = $data['caiwu_file']['name'] ?? ''; + $data['dubai_file_url'] = $data['dubai_file']['url'] ?? ''; + $data['dubai_file_name'] = $data['dubai_file']['name'] ?? ''; + return $data; + } + + /* ============================================================ + * 一、M项目对账明细 导入配置(sheet: M项目对账明细) + * ============================================================ */ + + /** 中文表头 => 目标字段名 */ + protected array $headerMapM = [ + '车架号' => 'vin', + '开票价' => 'invoice_price', + '实际采购价' => 'actual_purchase_price', + '开票价和实际采购价差额' => 'invoice_vs_purchase_diff', + '车源中介费' => 'source_broker_fee', + '配件' => 'parts_fee', + '停车费' => 'parking_fee', + '保险费' => 'insurance_fee', + '车辆手续费' => 'vehicle_service_fee', + '检测费' => 'inspection_fee', + '购置税' => 'purchase_tax', + '汽车其他费用' => 'other_vehicle_fee', + '转移待出口' => 'transfer_to_export', + '国际运费' => 'freight_international', + '国内运费1' => 'freight_domestic_1', + '国内运费2' => 'freight_domestic_2', + '报关费及港杂费' => 'customs_port_fee', + '注销费' => 'cancellation_fee', + '车船税' => 'vehicle_vessel_tax', + '其他财务费用' => 'other_finance_fee', + '汽车小额杂费' => 'minor_vehicle_misc', + '上牌费' => 'registration_fee', + '其他保险费' => 'other_insurance_fee', + '油补' => 'fuel_subsidy', + '快递费' => 'express_fee', + '国内成本合计' => 'domestic_cost_total', + '退税额' => 'tax_refund_amount', + 'cif杰贝阿里成本价' => 'cif_jebel_ali_cost', + '范经理发车看板CIF' => 'fan_manager_cif', + '财务-范经理 差额' => 'finance_vs_fan_diff', + ]; + + /** 需要转为 decimal 的目标字段 */ + protected array $decimalFieldsM = [ + 'invoice_price','actual_purchase_price','invoice_vs_purchase_diff', + 'source_broker_fee','parts_fee','parking_fee','insurance_fee','vehicle_service_fee','inspection_fee', + 'purchase_tax','other_vehicle_fee','transfer_to_export', + 'freight_international','freight_domestic_1','freight_domestic_2','customs_port_fee', + 'cancellation_fee','vehicle_vessel_tax','other_finance_fee','minor_vehicle_misc', + 'registration_fee','other_insurance_fee','fuel_subsidy','express_fee', + 'domestic_cost_total','tax_refund_amount','cif_jebel_ali_cost','fan_manager_cif','finance_vs_fan_diff', + ]; + + /** + * M项目对账明细导入 + * + * @param string $file 可为“签名 URL”或“OSS 对象键” + * @param bool $isUrl 若传入的是已签名 URL,请设 true;若是对象键,设 false + * @param bool $insertToDb 是否立即入库 + * + * 返回:['sheet','total_rows','skipped','errors','to_import','inserted','preview_3',('error'?)] + */ + public function mImport(string $file, bool $isUrl = true, bool $insertToDb = true): array + { + $sheet = 'M项目对账明细'; + + // 允许传对象键:当 isUrl=false 且非 http(s) 时,自动用 OssService 签名 + if ($isUrl === false && !preg_match('#^https?://#i', $file)) { + $file = $this->ossSignedUrl($file); + $isUrl = true; + } + + $rows = $this->readSheetRows($file, $sheet, $isUrl, headerRow: 1); + $mapped = []; + $skipped = 0; + $errors = 0; + + foreach ($rows as $row) { + try { + if ($this->shouldSkipByVinAliases($row, ['车架号'])) { $skipped++; continue; } + $rec = $this->mapAndNormalizeRow($row, $this->headerMapM, $this->decimalFieldsM); + $this->stamp($rec); + $mapped[] = $rec; + } catch (\Throwable $e) { + // 单行异常不阻断全局导入 + $errors++; continue; + } + } + + $result = [ + 'sheet' => $sheet, + 'total_rows' => count($rows), + 'skipped' => $skipped, + 'errors' => $errors, + 'to_import' => count($mapped), + ]; + + if ($insertToDb) { + // 注意表名:按你的业务这里写入 finance_recon_m_detail + $ret = $this->batchInsert('finance_recon_m_detail', $mapped); + $result['inserted'] = $ret['inserted']; + if ($ret['error']) $result['error'] = 'Database insert error: ' . $ret['error']; + } else { + $result['inserted'] = 0; + } + + $result['preview_3'] = array_slice($mapped, 0, 3); + return $result; + } + + /** M项目对账明细:从 OSS 对象键导入(语义化便捷入口) */ + public function mImportFromOssKey(string $objectKey, bool $insertToDb = true): array + { + $signedUrl = $this->ossSignedUrl($objectKey); + return $this->mImport($signedUrl, true, $insertToDb); + } + + /** M项目对账明细:从已签名 URL 导入(语义化便捷入口) */ + public function mImportFromSignedUrl(string $signedUrl, bool $insertToDb = true): array + { + return $this->mImport($signedUrl, true, $insertToDb); + } + + /* ============================================================ + * 二、迪拜两表合并 导入配置(sheets: Stock in Jebel Ali / Under shipping) + * ============================================================ */ + + /** 表头映射(已兼容不一致列名:VIN NO/车架号、国际运费和杂费/SHIPPING COST) */ + protected array $headerMapDubai = [ + 'VIN NO' => 'vin', '车架号' => 'vin', + '开票价' => 'invoice_price', + '中介费' => 'broker_fee', + '购置税' => 'purchase_tax', + '退税' => 'tax_refund', + '不含税车价' => 'price_ex_vat', + '费用' => 'fee_total', + '国内运费' => 'domestic_freight', + 'FCA成本价' => 'fca_cost', + 'FCA成本价汇率' => 'fca_fx', + '国际运费和杂费' => 'shipping_cost', + 'SHIPPING COST' => 'shipping_cost', + '运费汇率' => 'shipping_fx', + 'CIF 杰贝阿里成本价' => 'cif_jebel_ali_cost', + 'CIF RMB' => 'cif_rmb', // Under shipping 没有该列,将在逻辑中推导 + ]; + + /** 需要转为 decimal 的目标字段(汇率字段使用 DECIMAL(18,6) 建表) */ + protected array $decimalFieldsDubai = [ + 'invoice_price','broker_fee','purchase_tax','tax_refund','price_ex_vat', + 'fee_total','domestic_freight','fca_cost','fca_fx', + 'shipping_cost','shipping_fx','cif_jebel_ali_cost','cif_rmb', + ]; + + /** + * 迪拜两表合并导入(Stock in Jebel Ali / Under shipping) + * + * 缺失值规则: + * - Under shipping 无 “CIF RMB”,尝试:cif_rmb = cif_jebel_ali_cost × shipping_fx + * 若运费汇率缺失则用 fca_fx 兜底,都没有则置 NULL。 + * + * 返回:['sheets','total_rows','skipped','to_import','inserted','preview_5',('error'?)] + */ + public function dImport(string $file, bool $isUrl = false, bool $insertToDb = true): array + { + // 允许传对象键:当 isUrl=false 且非 http(s) 时,自动用 OssService 签名 + if ($isUrl === false && !preg_match('#^https?://#i', $file)) { + $file = $this->ossSignedUrl($file); + $isUrl = true; + } + + $sheets = ['Stock in Jebel Ali', 'Under shipping']; + $parsed = []; + + // 逐 Sheet 读取 + foreach ($sheets as $sheet) { + $parsed[$sheet] = $this->readSheetRows($file, $sheet, $isUrl, headerRow: 1); + } + + // 行处理与合并 + $rowsAll = []; + $skipped = 0; + + foreach ($sheets as $sheet) { + foreach ($parsed[$sheet] as $row) { + if ($this->shouldSkipByVinAliases($row, ['VIN NO','车架号'])) { $skipped++; continue; } + + // 映射 + 数值规范化 + $rec = $this->mapAndNormalizeRow($row, $this->headerMapDubai, $this->decimalFieldsDubai); + + // Under shipping 无 CIF RMB:尝试用汇率推导 + if (empty($rec['cif_rmb'])) { + $cif = $rec['cif_jebel_ali_cost'] ?? null; + $fx = $rec['shipping_fx'] ?? null; + if ($cif !== null && $fx !== null) { + $rec['cif_rmb'] = round($cif * $fx, 2); + } elseif ($cif !== null && !empty($rec['fca_fx'])) { + $rec['cif_rmb'] = round($cif * (float)$rec['fca_fx'], 2); + } else { + $rec['cif_rmb'] = null; + } + } + + // 统一 VIN 与来源 + $rec['vin'] = $this->pickFirstNonEmpty($row, ['VIN NO','车架号']); + $rec['source_sheet'] = $sheet; + + $this->stamp($rec); + $rowsAll[] = $rec; + } + } + + // 结果与入库 + $result = [ + 'sheets' => $sheets, + 'total_rows' => array_sum(array_map('count', $parsed)), + 'skipped' => $skipped, + 'to_import' => count($rowsAll), + ]; + + if ($insertToDb) { + // 注意表名:按你的业务这里写入 finance_recon_d_detail + $ret = $this->batchInsert('finance_recon_d_detail', $rowsAll); + $result['inserted'] = $ret['inserted']; + if ($ret['error']) $result['error'] = 'Database insert error: ' . $ret['error']; + } else { + $result['inserted'] = 0; + } + + $result['preview_5'] = array_slice($rowsAll, 0, 5); + return $result; + } + + /** 迪拜两表:从 OSS 对象键导入(语义化便捷入口) */ + public function dImportFromOssKey(string $objectKey, bool $insertToDb = true): array + { + $signedUrl = $this->ossSignedUrl($objectKey); + return $this->dImport($signedUrl, true, $insertToDb); + } + + /** 迪拜两表:从已签名 URL 导入(语义化便捷入口) */ + public function dImportFromSignedUrl(string $signedUrl, bool $insertToDb = true): array + { + return $this->dImport($signedUrl, true, $insertToDb); + } +} \ No newline at end of file diff --git a/app/common/service/UploadService.php b/app/common/service/UploadService.php index 43684cf..62b838c 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 = 'x_server/' . $sceneUrl . date('Ymd') . '/'; + $dir = config('oss.oss_main_dir') . $sceneUrl . date('Ymd') . '/'; // 使用传入的 original_name 命名(缺少扩展名时补上推断的扩展名),并做简单安全处理 $safeBaseName = preg_replace('/[\\\\\/]+/', '_', $originalName); if ($extension && strtolower(pathinfo($safeBaseName, PATHINFO_EXTENSION)) !== strtolower($extension)) { diff --git a/app/common/utils/excel/ExcelImportTrait.php b/app/common/utils/excel/ExcelImportTrait.php new file mode 100644 index 0000000..79fffa4 --- /dev/null +++ b/app/common/utils/excel/ExcelImportTrait.php @@ -0,0 +1,133 @@ +ossSignedUrl($objectKey); + $ret = ExcelReader::readSheetFromUrl($signedUrl, $sheet, $headerRow, $startRow); + return $ret['rows'] ?? []; + } + + /** + * 使用你的 OssService 生成私有签名 URL + * - 兼容你的现有写法:(new \app\common\service\OssService)->generatePrivateUrl($objectKey) + */ + protected function ossSignedUrl(string $objectKey): string + { + return (new \app\common\service\OssService())->generatePrivateUrl($objectKey); + } + + /** + * 将一行做字段映射 + 基础类型规范化(decimal/bool/date) + * - $headerMap:表头映射(源列名 => 目标字段名) + * - $decimalFields/$boolFields/$dateFields:需要做类型规范化的目标字段名列表 + */ + protected function mapAndNormalizeRow( + array $row, + array $headerMap, + array $decimalFields = [], + array $boolFields = [], + array $dateFields = [] + ): array { + $rec = ExcelReader::remap($row, $headerMap); + + foreach ($decimalFields as $f) { + if (array_key_exists($f, $rec)) { + $rec[$f] = ExcelReader::toDecimal($rec[$f]); // 建议已升级为“去货币符号/千分位/括号负数”的版本 + } + } + foreach ($boolFields as $f) { + if (array_key_exists($f, $rec)) { + $rec[$f] = ExcelReader::toBool($rec[$f]); + } + } + foreach ($dateFields as $f) { + if (array_key_exists($f, $rec)) { + $rec[$f] = ExcelReader::toDate($rec[$f]); + } + } + return $rec; + } + + /** + * 基于 VIN 的跳过规则:VIN 为空、或是“合计/总计”行 + * - $vinAliases:VIN 列的候选表头(如 ['车架号','VIN NO']) + */ + protected function shouldSkipByVinAliases(array $row, array $vinAliases = ['车架号', 'VIN NO']): bool + { + $vin = $this->pickFirstNonEmpty($row, $vinAliases); + if ($vin === null) return true; + return (bool)preg_match('/^(合计|总计)/u', $vin); + } + + /** + * 在多个候选键里取第一个非空字符串 + */ + protected function pickFirstNonEmpty(array $row, array $keys): ?string + { + foreach ($keys as $k) { + if (isset($row[$k])) { + $v = trim((string)$row[$k]); + if ($v !== '') return $v; + } + } + return null; + } + + /** + * 统一补齐 created_at / updated_at 时间戳(int(11)) + */ + protected function stamp(array &$rec): void + { + $now = time(); + $rec['created_at'] = $now; + $rec['updated_at'] = $now; + } + + /** + * 批量入库 + * - 返回 ['inserted'=>N, 'error'=>null|string] + * - 发生异常时不抛出,返回 error 字符串,便于上层统一处理 + */ + protected function batchInsert(string $table, array $rows): array + { + if (!$rows) return ['inserted' => 0, 'error' => null]; + try { + Db::table($table)->insert($rows); + return ['inserted' => count($rows), 'error' => null]; + } catch (\Throwable $e) { + return ['inserted' => 0, 'error' => $e->getMessage()]; + } + } +} diff --git a/app/common/utils/excel/ExcelReader.php b/app/common/utils/excel/ExcelReader.php new file mode 100644 index 0000000..e5f002a --- /dev/null +++ b/app/common/utils/excel/ExcelReader.php @@ -0,0 +1,292 @@ +[], 'rows'=>[]] + * - $headerRow:从 1 开始计数(Excel 行号) + * - $startRow:数据起始行(默认 headerRow+1) + * - 注意:$filePath 必须是“本地文件路径”;若为 URL,请改用 readSheetFromUrl() + */ + public static function readSheet(string $filePath, string $sheetName, int $headerRow = 1, ?int $startRow = null): array + { + self::assertRequirements(); + + if (!is_file($filePath)) { + throw new \RuntimeException("File not found: {$filePath}"); + } + + $reader = IOFactory::createReaderForFile($filePath); + $reader->setReadDataOnly(true); + $spreadsheet = $reader->load($filePath); + + $sheet = $spreadsheet->getSheetByName($sheetName); + if (!$sheet) { + $names = implode(' / ', $spreadsheet->getSheetNames()); + $spreadsheet->disconnectWorksheets(); + throw new \RuntimeException("Sheet '{$sheetName}' not found. Available: {$names}"); + } + + $highestRow = $sheet->getHighestDataRow(); + $highestCol = $sheet->getHighestDataColumn(); + $highestColIndex = Coordinate::columnIndexFromString($highestCol); + + // 读取表头(去除可能的 UTF-8 BOM) + $headers = []; + for ($col = 1; $col <= $highestColIndex; $col++) { + $coord = Coordinate::stringFromColumnIndex($col) . $headerRow; + $cell = $sheet->getCell($coord); + $raw = $cell ? $cell->getCalculatedValue() : null; + $text = is_null($raw) ? '' : trim((string)$raw); + // 去掉 BOM + if ($col === 1) { + $text = preg_replace('/^\xEF\xBB\xBF/u', '', $text); + } + $headers[$col] = $text === '' ? "COL_{$col}" : $text; + } + + // 读取数据 + $rows = []; + $firstDataRow = $startRow ?? ($headerRow + 1); + for ($row = $firstDataRow; $row <= $highestRow; $row++) { + $record = []; + $isEmpty = true; + + for ($col = 1; $col <= $highestColIndex; $col++) { + $coord = Coordinate::stringFromColumnIndex($col) . $row; + $cell = $sheet->getCell($coord); + $value = $cell ? $cell->getCalculatedValue() : null; + + // 日期单元格转换(使用 PhpSpreadsheet 的日期检测) + if ($cell && \PhpOffice\PhpSpreadsheet\Shared\Date::isDateTime($cell) && is_numeric($value)) { + $dt = \PhpOffice\PhpSpreadsheet\Shared\Date::excelToDateTimeObject($value); + $value = $dt ? $dt->format('Y-m-d') : $value; + } + + if (is_string($value)) { + $value = trim($value); + } + if ($value !== null && $value !== '') { + $isEmpty = false; + } + $record[$headers[$col]] = $value; + } + + if (!$isEmpty) { + $rows[] = $record; + } + } + + $spreadsheet->disconnectWorksheets(); + return ['headers' => array_values($headers), 'rows' => $rows]; + } + + /** + * 从 URL 读取指定 sheet(内部先下载到临时文件) + */ + public static function readSheetFromUrl(string $url, string $sheetName, int $headerRow = 1, ?int $startRow = null): array + { + $tmp = self::downloadToTemp($url); + try { + return self::readSheet($tmp, $sheetName, $headerRow, $startRow); + } finally { + @unlink($tmp); + } + } + + /** + * 按映射表将“源表头”=>“目标字段名”(snake_case) + * $map 形如:['车架号' => 'vin', '实际采购价' => 'actual_purchase_price', ...] + */ + public static function remap(array $row, array $map): array + { + $out = []; + foreach ($map as $from => $to) { + $out[$to] = $row[$from] ?? null; + } + return $out; + } + + /** 将一批行做 remap */ + public static function remapRows(array $rows, array $map): array + { + $out = []; + foreach ($rows as $row) { + $out[] = self::remap($row, $map); + } + return $out; + } + + /** + * 将“金额字符串”解析成 float(适配多币种/多地区写法) + * - 支持:¥ ¥ $ € £ …、AED/Dirham:dh / dhs / aed、阿语 د.إ + * - 支持:中文/全角标点、阿拉伯小数/千分位、细空格、撇号千分位 + * - 支持:括号负数 (xxx) 与显式负号 + * - 返回 null 表示无法解析 + */ + public static function toDecimal($v): ?float + { + if ($v === null || $v === '') return null; + if (is_numeric($v)) return (float)$v; + + $s = trim((string)$v); + + // 1) 统一常见 unicode 标点(全角/阿拉伯)与空格 + $s = strtr($s, [ + ',' => ',', '.' => '.', + '٫' => '.', '٬' => ',', + "\xC2\xA0" => ' ', // NBSP + "\xE2\x80\xAF" => ' ', // NNBSP(窄不换行空格) + ]); + + // 2) 括号负数,如 (dh1,234.56) + $negative = false; + if (preg_match('/^\s*\((.*)\)\s*$/u', $s, $m)) { + $negative = true; + $s = $m[1]; + } + + // 3) 去币种/符号:AED/Dirham(dh/dhs/aed/د.إ)& 其它常见符号/字母 + $s = preg_replace('/\b(dhs?|aed)\b/i', '', $s); // dh / dhs / aed + $s = str_replace(['د.إ', 'د', 'إ'], '', $s); // 常见阿语形式简化处理 + $s = preg_replace('/[A-Za-z¥¥$€£₽₺₩₹﷼]+/u', '', $s); // 其它字母/货币符号 + + // 4) 只保留 数字/点/逗号/减号/空格/撇号,然后去掉空格与撇号(千分位) + $s = preg_replace('/[^\d\.,\-\s\']+/u', '', $s); + $s = str_replace([' ', "'"], '', $s); + + // 5) 统一小数分隔符(判断最后出现的分隔符) + $lastComma = strrpos($s, ','); + $lastDot = strrpos($s, '.'); + + if ($lastComma !== false && $lastDot !== false) { + // 同时存在,最后出现者视为小数点 + if ($lastComma > $lastDot) { + // 欧式 1.234,56 + $s = str_replace('.', '', $s); // 去千分点 + $s = substr_replace($s, '.', $lastComma, 1); // 最后一个逗号改为小数点 + $s = str_replace(',', '', $s); // 其余逗号清空 + } else { + // 美式 1,234.56 + $s = str_replace(',', '', $s); + } + } elseif ($lastComma !== false) { + // 只有逗号 + $digitsAfter = strlen($s) - $lastComma - 1; + if (substr_count($s, ',') > 1) { + // 多个逗号基本都是千分位 + $s = str_replace(',', '', $s); + } else { + // 一个逗号:若逗号后 1~2 位,视为小数点,否则千分位 + $s = ($digitsAfter >= 1 && $digitsAfter <= 2) + ? str_replace(',', '.', $s) + : str_replace(',', '', $s); + } + } elseif ($lastDot !== false && substr_count($s, '.') > 1) { + // 多个点:保留最后一个为小数点,其他去掉(当作千分位) + $digitsAfter = strlen($s) - $lastDot - 1; + if ($digitsAfter >= 1 && $digitsAfter <= 2) { + $s = preg_replace('/\.(?=.*\.)/', '', $s); // 移除除最后一个外的点 + } else { + $s = str_replace('.', '', $s); + } + } + + // 6) 处理显式负号 + if (strpos($s, '-') !== false) { + $negative = true; + $s = str_replace('-', '', $s); + } + + // 7) 最终校验并转 float + if ($s === '' || !preg_match('/^\d+(\.\d+)?$/', $s)) { + return null; // 无法解析 + } + + $num = (float)$s; + return $negative ? -$num : $num; + } + + /** 规范化布尔:'是/否'、'Y/N'、'1/0'、'true/false' */ + public static function toBool($v): ?int + { + if ($v === null || $v === '') return null; + $s = mb_strtolower(trim((string)$v)); + if (in_array($s, ['是','y','yes','true','1','√','✅'], true)) return 1; + if (in_array($s, ['否','n','no','false','0','×','❌'], true)) return 0; + return null; + } + + /** 规范化日期(readSheet 已对 Excel 日期做过处理,这里再兜底) */ + public static function toDate($v): ?string + { + if ($v === null || $v === '') return null; + if (is_numeric($v)) { + // Excel 序列号兜底 + $ts = ExcelDate::excelToTimestamp($v); + return $ts ? date('Y-m-d', $ts) : null; + } + // 简单清洗 + $s = trim((string)$v); + // 常见格式直接返回(如 2025-11-04 / 2025/11/04) + if (preg_match('/^\d{4}[-\/]\d{1,2}[-\/]\d{1,2}$/', $s)) { + return str_replace('/', '-', $s); + } + return $s; // 留给上层决定是否再严格校验 + } + + /** + * 把远程 URL 流式下载到临时文件;返回临时路径 + */ + protected static function downloadToTemp(string $url, ?string $ext = null): string + { + $ext = $ext ? ('.' . ltrim($ext, '.')) : ''; + $tmp = sys_get_temp_dir() . '/excel_' . uniqid() . $ext; + $fp = fopen($tmp, 'w'); + if (!$fp) { + throw new \RuntimeException("Cannot open temp file: {$tmp}"); + } + $ch = curl_init($url); + curl_setopt_array($ch, [ + CURLOPT_FILE => $fp, + CURLOPT_FOLLOWLOCATION => true, + CURLOPT_TIMEOUT => 300, + CURLOPT_CONNECTTIMEOUT => 15, + CURLOPT_SSL_VERIFYPEER => true, + CURLOPT_SSL_VERIFYHOST => 2, + CURLOPT_USERAGENT => 'Webman-ExcelReader/1.0', + ]); + $ok = curl_exec($ch); + $http = curl_getinfo($ch, CURLINFO_HTTP_CODE); + $err = curl_error($ch); + curl_close($ch); + fclose($fp); + if ($ok === false || $http >= 400) { + @unlink($tmp); + throw new \RuntimeException("Download failed: HTTP {$http} {$err}"); + } + return $tmp; + } +} diff --git a/app/common/validate/FinanceReconDubaiFilesValidate.php b/app/common/validate/FinanceReconDubaiFilesValidate.php new file mode 100644 index 0000000..193b230 --- /dev/null +++ b/app/common/validate/FinanceReconDubaiFilesValidate.php @@ -0,0 +1,29 @@ + [ + + ], + + 'update' => [ + + ] + ]; +} \ No newline at end of file diff --git a/app/controller/IndexController.php b/app/controller/IndexController.php index 72532c7..dbc493a 100644 --- a/app/controller/IndexController.php +++ b/app/controller/IndexController.php @@ -74,25 +74,25 @@ class IndexController $options = [ 'controller' => [ 'module' => 'common', - 'name' => 'stock_orders_files' + 'name' => 'finance_recon_dubai_files' ], 'model' => [ 'module' => 'common', - 'name' => 'stock_orders_files' + 'name' => 'finance_recon_dubai_files' ], 'service' => [ 'module' => 'common', - 'name' => 'stock_orders_files' + 'name' => 'finance_recon_dubai_files' ], 'logic' => [ 'module' => 'common', - 'name' => 'stock_orders_files' + 'name' => 'finance_recon_dubai_files' ], 'validate' => [ 'module' => 'common', - 'name' => 'stock_orders_files' + 'name' => 'finance_recon_dubai_files' ], - 'table' => 'stock_orders_files' + 'table' => 'finance_recon_dubai_files' ]; $result = (new Generate($options))->doGenerate(); @@ -114,7 +114,7 @@ class IndexController } - public function test(Request $request) + public function pdf(Request $request) { $id = $request->get('id'); $service = new \app\common\service\StockOrdersService(); @@ -154,4 +154,16 @@ class IndexController $data = array('header' => $header,'body' => $response); return $data; } + + + + public function test(Request $request) + { + $tmp = (new \app\common\service\OssService)->generatePrivateUrl('fdesk_test/20251104/20251104_财务.xlsx');; + + $service = new \app\common\service\FinanceRecoDubaiFilesService(); + $result = $service->import($tmp, true); + + return json(['code' => 0, 'msg' => 'ok', 'data' => $result]); + } } \ No newline at end of file diff --git a/config/oss.php b/config/oss.php new file mode 100644 index 0000000..bb03fc7 --- /dev/null +++ b/config/oss.php @@ -0,0 +1,10 @@ + env('OSS_ACCESSID'), + 'oss_accesssecret' => env('OSS_ACCESSSECRET'), + 'oss_bucket' => env('OSS_BUCKET'), + 'oss_endpoint' => env('OSS_ENDPOINT'), + 'oss_static_url' => env('OSS_STATIC_URL'), + 'oss_main_dir' => env('OSS_MAIN_DIR') +]; \ No newline at end of file diff --git a/config/route.php b/config/route.php index 36492d9..c49f9dc 100644 --- a/config/route.php +++ b/config/route.php @@ -472,6 +472,16 @@ Route::group('/admin', function () { Route::any('/claim', [app\admin\controller\dashboard\OfficeClerkController::class, 'claim']); }); }); + + Route::group('/finance', function () { + Route::group('/dubai', function () { + Route::any('/list', [app\admin\controller\FinanceReconDubaiFilesController::class, 'index']); + Route::any('/show', [app\admin\controller\FinanceReconDubaiFilesController::class, 'show']); + Route::any('/store', [app\admin\controller\FinanceReconDubaiFilesController::class, 'store']); + Route::any('/update', [app\admin\controller\FinanceReconDubaiFilesController::class, 'update']); + Route::any('/destroy', [app\admin\controller\FinanceReconDubaiFilesController::class, 'destroy']); + }); + }); }); From 39820844f7605f604ff2b307a5a25b59971daf19 Mon Sep 17 00:00:00 2001 From: mzl Date: Fri, 7 Nov 2025 16:34:57 +0800 Subject: [PATCH 079/110] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E5=BF=BD=E7=95=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 5 +- webman | 142 ++++++++++++++++++++++++++--------------------------- 2 files changed, 75 insertions(+), 72 deletions(-) mode change 100644 => 100755 webman diff --git a/.gitignore b/.gitignore index fa572a1..c08e119 100644 --- a/.gitignore +++ b/.gitignore @@ -7,4 +7,7 @@ /tests/tmp /tests/.phpunit.result.cache /public/uploads -/webman \ No newline at end of file +/webman +/start.php +/windows.php +/bootstrap.php \ No newline at end of file diff --git a/webman b/webman old mode 100644 new mode 100755 index c853676..3f55062 --- a/webman +++ b/webman @@ -1,71 +1,71 @@ -#!/usr/bin/env php -load(); - } else { - Dotenv::createMutable(run_path())->load(); - } -} - -$appConfig = require $appConfigFile; -if ($timezone = $appConfig['default_timezone'] ?? '') { - date_default_timezone_set($timezone); -} - -if ($errorReporting = $appConfig['error_reporting'] ?? '') { - error_reporting($errorReporting); -} - -if (!in_array($argv[1] ?? '', ['start', 'restart', 'stop', 'status', 'reload', 'connections'])) { - require_once __DIR__ . '/support/bootstrap.php'; -} else { - if (class_exists('Support\App')) { - Support\App::loadAllConfig(['route']); - } else { - Config::reload(config_path(), ['route', 'container']); - } -} - -$cli = new Command(); -$cli->setName('webman cli'); -$cli->installInternalCommands(); -if (is_dir($command_path = Util::guessPath(app_path(), '/command', true))) { - $cli->installCommands($command_path); -} - -foreach (config('plugin', []) as $firm => $projects) { - if (isset($projects['app'])) { - foreach (['', '/app'] as $app) { - if ($command_str = Util::guessPath(base_path() . "/plugin/$firm{$app}", 'command')) { - $command_path = base_path() . "/plugin/$firm{$app}/$command_str"; - $cli->installCommands($command_path, "plugin\\$firm" . str_replace('/', '\\', $app) . "\\$command_str"); - } - } - } - foreach ($projects as $name => $project) { - if (!is_array($project)) { - continue; - } - $project['command'] ??= []; - array_walk($project['command'], [$cli, 'createCommandInstance']); - } -} - -$cli->run(); +#!/usr/bin/env php +load(); + } else { + Dotenv::createMutable(run_path())->load(); + } +} + +$appConfig = require $appConfigFile; +if ($timezone = $appConfig['default_timezone'] ?? '') { + date_default_timezone_set($timezone); +} + +if ($errorReporting = $appConfig['error_reporting'] ?? '') { + error_reporting($errorReporting); +} + +if (!in_array($argv[1] ?? '', ['start', 'restart', 'stop', 'status', 'reload', 'connections'])) { + require_once __DIR__ . '/support/bootstrap.php'; +} else { + if (class_exists('Support\App')) { + Support\App::loadAllConfig(['route']); + } else { + Config::reload(config_path(), ['route', 'container']); + } +} + +$cli = new Command(); +$cli->setName('webman cli'); +$cli->installInternalCommands(); +if (is_dir($command_path = Util::guessPath(app_path(), '/command', true))) { + $cli->installCommands($command_path); +} + +foreach (config('plugin', []) as $firm => $projects) { + if (isset($projects['app'])) { + foreach (['', '/app'] as $app) { + if ($command_str = Util::guessPath(base_path() . "/plugin/$firm{$app}", 'command')) { + $command_path = base_path() . "/plugin/$firm{$app}/$command_str"; + $cli->installCommands($command_path, "plugin\\$firm" . str_replace('/', '\\', $app) . "\\$command_str"); + } + } + } + foreach ($projects as $name => $project) { + if (!is_array($project)) { + continue; + } + $project['command'] ??= []; + array_walk($project['command'], [$cli, 'createCommandInstance']); + } +} + +$cli->run(); From d515bc07fede8a201f4328117d7fa20d74253614 Mon Sep 17 00:00:00 2001 From: mzl Date: Tue, 11 Nov 2025 08:38:32 +0000 Subject: [PATCH 080/110] =?UTF-8?q?=E5=A4=87=E8=B4=A7=E5=8D=95=E8=B0=83?= =?UTF-8?q?=E6=95=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/common/service/StockOrdersService.php | 40 ++++++++++--------- app/common/utils/pdf/template/stockOrder.stub | 22 ++++++---- app/controller/IndexController.php | 4 +- 3 files changed, 38 insertions(+), 28 deletions(-) diff --git a/app/common/service/StockOrdersService.php b/app/common/service/StockOrdersService.php index 6da7a24..7ee30a4 100644 --- a/app/common/service/StockOrdersService.php +++ b/app/common/service/StockOrdersService.php @@ -655,7 +655,7 @@ class StockOrdersService */ private function generateVehicleUniqueId(array $veh): string { - $keysToRemove = ['vin', 'remark', 'modify_list', 'order_id']; + $keysToRemove = ['remark', 'modify_list', 'order_id']; $array = array_diff_key($veh, array_flip($keysToRemove)); $uniqueIdStr = json_encode($array); return md5($uniqueIdStr); @@ -884,6 +884,7 @@ class StockOrdersService 'num' => 1, 'mileage' => $v->mileage, 'remark' => $v->remark, + 'vin' => $v->vin, ] + $moneyValues; } return ['list' => $vehList]; @@ -925,25 +926,26 @@ class StockOrdersService { $vehicleData['num'] += 1; - $fieldList = [ - 'fob_transport_fee', - 'car_price', - 'ocean_freight', - 'commission', - 'retention_price', - 'port_misc_fee', - 'total_price', - 'clearance_price', - 'profit' - ]; + // ** 11.11 remove 价格*数量 + // $fieldList = [ + // 'fob_transport_fee', + // 'car_price', + // 'ocean_freight', + // 'commission', + // 'retention_price', + // 'port_misc_fee', + // 'total_price', + // 'clearance_price', + // 'profit' + // ]; - foreach ($vehicleData as $field => $value) { - if (in_array($field, $fieldList)) { - $amount = $vehicleData[$field]['amount'] * $vehicleData['num']; - $currencySymbol = SysCurrency::where('currency_code', $vehicleData[$field]['currency'])->value('currency_symbol'); - $vehicleData[$field]['currency_info'] = $currencySymbol . number_format($amount, 2, '.', ','); - } - } + // foreach ($vehicleData as $field => $value) { + // if (in_array($field, $fieldList)) { + // $amount = $vehicleData[$field]['amount'] * $vehicleData['num']; + // $currencySymbol = SysCurrency::where('currency_code', $vehicleData[$field]['currency'])->value('currency_symbol'); + // $vehicleData[$field]['currency_info'] = $currencySymbol . number_format($amount, 2, '.', ','); + // } + // } } /** diff --git a/app/common/utils/pdf/template/stockOrder.stub b/app/common/utils/pdf/template/stockOrder.stub index 697b8eb..807c3e3 100644 --- a/app/common/utils/pdf/template/stockOrder.stub +++ b/app/common/utils/pdf/template/stockOrder.stub @@ -89,7 +89,7 @@
-

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

+

合塑实业-汽车事业部-备货单

备货单号: 业务员: @@ -126,7 +126,7 @@ 车型 数量 颜色 - 费用明细 + 费用明细 @@ -142,9 +142,12 @@ - +

0): ?> - (km) + 里程:km +
+ + VIN: @@ -160,6 +163,7 @@ + 手续费 改装费 配件费 收款价 @@ -171,6 +175,7 @@ + @@ -191,6 +196,7 @@ + 加价 报关价 FOB运费 车价 @@ -202,6 +208,7 @@ + @@ -214,7 +221,7 @@ 备注 - + @@ -231,7 +238,7 @@ 合同汇率: - + 合计到岸价: @@ -261,7 +268,8 @@ 出运方式: 国内收货港: - 贸易条款/清关目的港: / + 贸易条款: + 清关目的港: 提车日期: diff --git a/app/controller/IndexController.php b/app/controller/IndexController.php index dbc493a..c3c75ae 100644 --- a/app/controller/IndexController.php +++ b/app/controller/IndexController.php @@ -15,9 +15,9 @@ use GuzzleHttp\Client; class IndexController { - protected $noNeedLogin = ['index', 'view', 'perm', 'createResource', 'test']; + protected $noNeedLogin = ['index', 'view', 'perm', 'createResource', 'pdf', 'test']; - protected $noNeedHeaderData = ['index', 'view', 'perm', 'createResource', 'test']; + protected $noNeedHeaderData = ['index', 'view', 'perm', 'createResource', 'pdf', 'test']; public function index(Request $request) { From c157b3ee12d383cc7e0eeed8909fccb1bf3d16c9 Mon Sep 17 00:00:00 2001 From: mzl Date: Wed, 12 Nov 2025 11:35:22 +0800 Subject: [PATCH 081/110] =?UTF-8?q?=E6=9B=B4=E6=96=B0=E7=B1=BB=E5=BA=93?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/controller/IndexController.php | 8 +- composer.lock | 985 +++++++++++++++++------------ 2 files changed, 601 insertions(+), 392 deletions(-) diff --git a/app/controller/IndexController.php b/app/controller/IndexController.php index c3c75ae..1e83e3d 100644 --- a/app/controller/IndexController.php +++ b/app/controller/IndexController.php @@ -159,11 +159,9 @@ class IndexController public function test(Request $request) { - $tmp = (new \app\common\service\OssService)->generatePrivateUrl('fdesk_test/20251104/20251104_财务.xlsx');; - - $service = new \app\common\service\FinanceRecoDubaiFilesService(); - $result = $service->import($tmp, true); - + $result = [ + 'desc' => 'demo' + ]; return json(['code' => 0, 'msg' => 'ok', 'data' => $result]); } } \ No newline at end of file diff --git a/composer.lock b/composer.lock index c3a23c9..1dbd689 100644 --- a/composer.lock +++ b/composer.lock @@ -18,13 +18,7 @@ "type": "zip", "url": "https://api.github.com/repos/aliyun/aliyun-oss-php-sdk/zipball/483dd0b8bff5d47f0e4ffc99f6077a295c5ccbb5", "reference": "483dd0b8bff5d47f0e4ffc99f6077a295c5ccbb5", - "shasum": "", - "mirrors": [ - { - "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", - "preferred": true - } - ] + "shasum": "" }, "require": { "php": ">=5.3" @@ -59,25 +53,25 @@ }, { "name": "brick/math", - "version": "0.13.1", + "version": "0.14.0", "source": { "type": "git", "url": "https://github.com/brick/math.git", - "reference": "fc7ed316430118cc7836bf45faff18d5dfc8de04" + "reference": "113a8ee2656b882d4c3164fa31aa6e12cbb7aaa2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/brick/math/zipball/fc7ed316430118cc7836bf45faff18d5dfc8de04", - "reference": "fc7ed316430118cc7836bf45faff18d5dfc8de04", + "url": "https://api.github.com/repos/brick/math/zipball/113a8ee2656b882d4c3164fa31aa6e12cbb7aaa2", + "reference": "113a8ee2656b882d4c3164fa31aa6e12cbb7aaa2", "shasum": "" }, "require": { - "php": "^8.1" + "php": "^8.2" }, "require-dev": { "php-coveralls/php-coveralls": "^2.2", - "phpunit/phpunit": "^10.1", - "vimeo/psalm": "6.8.8" + "phpstan/phpstan": "2.1.22", + "phpunit/phpunit": "^11.5" }, "type": "library", "autoload": { @@ -107,7 +101,7 @@ ], "support": { "issues": "https://github.com/brick/math/issues", - "source": "https://github.com/brick/math/tree/0.13.1" + "source": "https://github.com/brick/math/tree/0.14.0" }, "funding": [ { @@ -115,7 +109,7 @@ "type": "github" } ], - "time": "2025-03-29T13:50:30+00:00" + "time": "2025-08-29T12:40:03+00:00" }, { "name": "carbonphp/carbon-doctrine-types", @@ -267,33 +261,32 @@ }, { "name": "doctrine/inflector", - "version": "2.0.10", + "version": "2.1.0", "source": { "type": "git", "url": "https://github.com/doctrine/inflector.git", - "reference": "5817d0659c5b50c9b950feb9af7b9668e2c436bc" + "reference": "6d6c96277ea252fc1304627204c3d5e6e15faa3b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/inflector/zipball/5817d0659c5b50c9b950feb9af7b9668e2c436bc", - "reference": "5817d0659c5b50c9b950feb9af7b9668e2c436bc", + "url": "https://api.github.com/repos/doctrine/inflector/zipball/6d6c96277ea252fc1304627204c3d5e6e15faa3b", + "reference": "6d6c96277ea252fc1304627204c3d5e6e15faa3b", "shasum": "" }, "require": { "php": "^7.2 || ^8.0" }, "require-dev": { - "doctrine/coding-standard": "^11.0", - "phpstan/phpstan": "^1.8", - "phpstan/phpstan-phpunit": "^1.1", - "phpstan/phpstan-strict-rules": "^1.3", - "phpunit/phpunit": "^8.5 || ^9.5", - "vimeo/psalm": "^4.25 || ^5.4" + "doctrine/coding-standard": "^12.0 || ^13.0", + "phpstan/phpstan": "^1.12 || ^2.0", + "phpstan/phpstan-phpunit": "^1.4 || ^2.0", + "phpstan/phpstan-strict-rules": "^1.6 || ^2.0", + "phpunit/phpunit": "^8.5 || ^12.2" }, "type": "library", "autoload": { "psr-4": { - "Doctrine\\Inflector\\": "lib/Doctrine/Inflector" + "Doctrine\\Inflector\\": "src" } }, "notification-url": "https://packagist.org/downloads/", @@ -338,7 +331,7 @@ ], "support": { "issues": "https://github.com/doctrine/inflector/issues", - "source": "https://github.com/doctrine/inflector/tree/2.0.10" + "source": "https://github.com/doctrine/inflector/tree/2.1.0" }, "funding": [ { @@ -354,7 +347,7 @@ "type": "tidelift" } ], - "time": "2024-02-18T20:23:39+00:00" + "time": "2025-08-10T19:31:58+00:00" }, { "name": "firebase/php-jwt", @@ -554,22 +547,22 @@ }, { "name": "guzzlehttp/guzzle", - "version": "7.9.3", + "version": "7.10.0", "source": { "type": "git", "url": "https://github.com/guzzle/guzzle.git", - "reference": "7b2f29fe81dc4da0ca0ea7d42107a0845946ea77" + "reference": "b51ac707cfa420b7bfd4e4d5e510ba8008e822b4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/guzzle/zipball/7b2f29fe81dc4da0ca0ea7d42107a0845946ea77", - "reference": "7b2f29fe81dc4da0ca0ea7d42107a0845946ea77", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/b51ac707cfa420b7bfd4e4d5e510ba8008e822b4", + "reference": "b51ac707cfa420b7bfd4e4d5e510ba8008e822b4", "shasum": "" }, "require": { "ext-json": "*", - "guzzlehttp/promises": "^1.5.3 || ^2.0.3", - "guzzlehttp/psr7": "^2.7.0", + "guzzlehttp/promises": "^2.3", + "guzzlehttp/psr7": "^2.8", "php": "^7.2.5 || ^8.0", "psr/http-client": "^1.0", "symfony/deprecation-contracts": "^2.2 || ^3.0" @@ -660,7 +653,7 @@ ], "support": { "issues": "https://github.com/guzzle/guzzle/issues", - "source": "https://github.com/guzzle/guzzle/tree/7.9.3" + "source": "https://github.com/guzzle/guzzle/tree/7.10.0" }, "funding": [ { @@ -676,20 +669,20 @@ "type": "tidelift" } ], - "time": "2025-03-27T13:37:11+00:00" + "time": "2025-08-23T22:36:01+00:00" }, { "name": "guzzlehttp/promises", - "version": "2.2.0", + "version": "2.3.0", "source": { "type": "git", "url": "https://github.com/guzzle/promises.git", - "reference": "7c69f28996b0a6920945dd20b3857e499d9ca96c" + "reference": "481557b130ef3790cf82b713667b43030dc9c957" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/promises/zipball/7c69f28996b0a6920945dd20b3857e499d9ca96c", - "reference": "7c69f28996b0a6920945dd20b3857e499d9ca96c", + "url": "https://api.github.com/repos/guzzle/promises/zipball/481557b130ef3790cf82b713667b43030dc9c957", + "reference": "481557b130ef3790cf82b713667b43030dc9c957", "shasum": "" }, "require": { @@ -697,7 +690,7 @@ }, "require-dev": { "bamarni/composer-bin-plugin": "^1.8.2", - "phpunit/phpunit": "^8.5.39 || ^9.6.20" + "phpunit/phpunit": "^8.5.44 || ^9.6.25" }, "type": "library", "extra": { @@ -743,7 +736,7 @@ ], "support": { "issues": "https://github.com/guzzle/promises/issues", - "source": "https://github.com/guzzle/promises/tree/2.2.0" + "source": "https://github.com/guzzle/promises/tree/2.3.0" }, "funding": [ { @@ -759,20 +752,20 @@ "type": "tidelift" } ], - "time": "2025-03-27T13:27:01+00:00" + "time": "2025-08-22T14:34:08+00:00" }, { "name": "guzzlehttp/psr7", - "version": "2.7.1", + "version": "2.8.0", "source": { "type": "git", "url": "https://github.com/guzzle/psr7.git", - "reference": "c2270caaabe631b3b44c85f99e5a04bbb8060d16" + "reference": "21dc724a0583619cd1652f673303492272778051" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/psr7/zipball/c2270caaabe631b3b44c85f99e5a04bbb8060d16", - "reference": "c2270caaabe631b3b44c85f99e5a04bbb8060d16", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/21dc724a0583619cd1652f673303492272778051", + "reference": "21dc724a0583619cd1652f673303492272778051", "shasum": "" }, "require": { @@ -788,7 +781,7 @@ "require-dev": { "bamarni/composer-bin-plugin": "^1.8.2", "http-interop/http-factory-tests": "0.9.0", - "phpunit/phpunit": "^8.5.39 || ^9.6.20" + "phpunit/phpunit": "^8.5.44 || ^9.6.25" }, "suggest": { "laminas/laminas-httphandlerrunner": "Emit PSR-7 responses" @@ -859,7 +852,7 @@ ], "support": { "issues": "https://github.com/guzzle/psr7/issues", - "source": "https://github.com/guzzle/psr7/tree/2.7.1" + "source": "https://github.com/guzzle/psr7/tree/2.8.0" }, "funding": [ { @@ -875,20 +868,20 @@ "type": "tidelift" } ], - "time": "2025-03-27T12:30:47+00:00" + "time": "2025-08-23T21:21:41+00:00" }, { "name": "guzzlehttp/uri-template", - "version": "v1.0.4", + "version": "v1.0.5", "source": { "type": "git", "url": "https://github.com/guzzle/uri-template.git", - "reference": "30e286560c137526eccd4ce21b2de477ab0676d2" + "reference": "4f4bbd4e7172148801e76e3decc1e559bdee34e1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/uri-template/zipball/30e286560c137526eccd4ce21b2de477ab0676d2", - "reference": "30e286560c137526eccd4ce21b2de477ab0676d2", + "url": "https://api.github.com/repos/guzzle/uri-template/zipball/4f4bbd4e7172148801e76e3decc1e559bdee34e1", + "reference": "4f4bbd4e7172148801e76e3decc1e559bdee34e1", "shasum": "" }, "require": { @@ -897,7 +890,7 @@ }, "require-dev": { "bamarni/composer-bin-plugin": "^1.8.2", - "phpunit/phpunit": "^8.5.36 || ^9.6.15", + "phpunit/phpunit": "^8.5.44 || ^9.6.25", "uri-template/tests": "1.0.0" }, "type": "library", @@ -945,7 +938,7 @@ ], "support": { "issues": "https://github.com/guzzle/uri-template/issues", - "source": "https://github.com/guzzle/uri-template/tree/v1.0.4" + "source": "https://github.com/guzzle/uri-template/tree/v1.0.5" }, "funding": [ { @@ -961,20 +954,20 @@ "type": "tidelift" } ], - "time": "2025-02-03T10:55:03+00:00" + "time": "2025-08-22T14:27:06+00:00" }, { "name": "illuminate/bus", - "version": "v12.18.0", + "version": "v12.37.0", "source": { "type": "git", "url": "https://github.com/illuminate/bus.git", - "reference": "60da78ea881c539ce56c5b66321be73755c5918c" + "reference": "797f768b7a52fb98443b8127cda71840cc2dbc5b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/illuminate/bus/zipball/60da78ea881c539ce56c5b66321be73755c5918c", - "reference": "60da78ea881c539ce56c5b66321be73755c5918c", + "url": "https://api.github.com/repos/illuminate/bus/zipball/797f768b7a52fb98443b8127cda71840cc2dbc5b", + "reference": "797f768b7a52fb98443b8127cda71840cc2dbc5b", "shasum": "" }, "require": { @@ -1014,27 +1007,29 @@ "issues": "https://github.com/laravel/framework/issues", "source": "https://github.com/laravel/framework" }, - "time": "2025-05-13T15:08:45+00:00" + "time": "2025-10-30T12:22:29+00:00" }, { "name": "illuminate/collections", - "version": "v12.18.0", + "version": "v12.37.0", "source": { "type": "git", "url": "https://github.com/illuminate/collections.git", - "reference": "e5711846b7c68128bc8de72c13e017606043c996" + "reference": "deb291b109b6f7fd776a3550a120771137b3c5d1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/illuminate/collections/zipball/e5711846b7c68128bc8de72c13e017606043c996", - "reference": "e5711846b7c68128bc8de72c13e017606043c996", + "url": "https://api.github.com/repos/illuminate/collections/zipball/deb291b109b6f7fd776a3550a120771137b3c5d1", + "reference": "deb291b109b6f7fd776a3550a120771137b3c5d1", "shasum": "" }, "require": { "illuminate/conditionable": "^12.0", "illuminate/contracts": "^12.0", "illuminate/macroable": "^12.0", - "php": "^8.2" + "php": "^8.2", + "symfony/polyfill-php84": "^1.33", + "symfony/polyfill-php85": "^1.33" }, "suggest": { "illuminate/http": "Required to convert collections to API resources (^12.0).", @@ -1071,11 +1066,11 @@ "issues": "https://github.com/laravel/framework/issues", "source": "https://github.com/laravel/framework" }, - "time": "2025-05-28T13:08:33+00:00" + "time": "2025-10-30T12:22:05+00:00" }, { "name": "illuminate/conditionable", - "version": "v12.18.0", + "version": "v12.37.0", "source": { "type": "git", "url": "https://github.com/illuminate/conditionable.git", @@ -1121,26 +1116,36 @@ }, { "name": "illuminate/container", - "version": "v12.18.0", + "version": "v12.37.0", "source": { "type": "git", "url": "https://github.com/illuminate/container.git", - "reference": "ff9dde2c8dce16ea9ecf0418095749311240aff9" + "reference": "d6eaa8afd48dbe16b6b3c412a87479cad67eeb12" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/illuminate/container/zipball/ff9dde2c8dce16ea9ecf0418095749311240aff9", - "reference": "ff9dde2c8dce16ea9ecf0418095749311240aff9", + "url": "https://api.github.com/repos/illuminate/container/zipball/d6eaa8afd48dbe16b6b3c412a87479cad67eeb12", + "reference": "d6eaa8afd48dbe16b6b3c412a87479cad67eeb12", "shasum": "" }, "require": { "illuminate/contracts": "^12.0", "php": "^8.2", - "psr/container": "^1.1.1|^2.0.1" + "psr/container": "^1.1.1|^2.0.1", + "symfony/polyfill-php84": "^1.33", + "symfony/polyfill-php85": "^1.33" }, "provide": { "psr/container-implementation": "1.1|2.0" }, + "suggest": { + "illuminate/auth": "Required to use the Auth attribute", + "illuminate/cache": "Required to use the Cache attribute", + "illuminate/config": "Required to use the Config attribute", + "illuminate/database": "Required to use the DB attribute", + "illuminate/filesystem": "Required to use the Storage attribute", + "illuminate/log": "Required to use the Log or Context attributes" + }, "type": "library", "extra": { "branch-alias": { @@ -1168,20 +1173,20 @@ "issues": "https://github.com/laravel/framework/issues", "source": "https://github.com/laravel/framework" }, - "time": "2025-06-09T14:04:48+00:00" + "time": "2025-09-12T14:35:11+00:00" }, { "name": "illuminate/contracts", - "version": "v12.18.0", + "version": "v12.37.0", "source": { "type": "git", "url": "https://github.com/illuminate/contracts.git", - "reference": "152313571553ef1be907a3c18b8ef2d635cb4339" + "reference": "5ab717c8f0dd4e84be703796bbb415ccff8de57a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/illuminate/contracts/zipball/152313571553ef1be907a3c18b8ef2d635cb4339", - "reference": "152313571553ef1be907a3c18b8ef2d635cb4339", + "url": "https://api.github.com/repos/illuminate/contracts/zipball/5ab717c8f0dd4e84be703796bbb415ccff8de57a", + "reference": "5ab717c8f0dd4e84be703796bbb415ccff8de57a", "shasum": "" }, "require": { @@ -1216,24 +1221,24 @@ "issues": "https://github.com/laravel/framework/issues", "source": "https://github.com/laravel/framework" }, - "time": "2025-06-09T18:23:03+00:00" + "time": "2025-10-07T19:59:08+00:00" }, { "name": "illuminate/database", - "version": "v12.18.0", + "version": "v12.37.0", "source": { "type": "git", "url": "https://github.com/illuminate/database.git", - "reference": "da274285e4c09f6794945354b5b11157f4d9cab1" + "reference": "e42bec6ad999bcc78ce9f3789f5ef937ee2e5982" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/illuminate/database/zipball/da274285e4c09f6794945354b5b11157f4d9cab1", - "reference": "da274285e4c09f6794945354b5b11157f4d9cab1", + "url": "https://api.github.com/repos/illuminate/database/zipball/e42bec6ad999bcc78ce9f3789f5ef937ee2e5982", + "reference": "e42bec6ad999bcc78ce9f3789f5ef937ee2e5982", "shasum": "" }, "require": { - "brick/math": "^0.11|^0.12|^0.13", + "brick/math": "^0.11|^0.12|^0.13|^0.14", "ext-pdo": "*", "illuminate/collections": "^12.0", "illuminate/container": "^12.0", @@ -1241,7 +1246,8 @@ "illuminate/macroable": "^12.0", "illuminate/support": "^12.0", "laravel/serializable-closure": "^1.3|^2.0", - "php": "^8.2" + "php": "^8.2", + "symfony/polyfill-php85": "^1.33" }, "suggest": { "ext-filter": "Required to use the Postgres database driver.", @@ -1286,20 +1292,20 @@ "issues": "https://github.com/laravel/framework/issues", "source": "https://github.com/laravel/framework" }, - "time": "2025-06-09T14:07:55+00:00" + "time": "2025-11-03T14:22:34+00:00" }, { "name": "illuminate/events", - "version": "v12.18.0", + "version": "v12.37.0", "source": { "type": "git", "url": "https://github.com/illuminate/events.git", - "reference": "bf1f121ea51e077e893d32e2848e102513d4b1b5" + "reference": "e0de667c68040d59a6ffc09e914536a1186870f0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/illuminate/events/zipball/bf1f121ea51e077e893d32e2848e102513d4b1b5", - "reference": "bf1f121ea51e077e893d32e2848e102513d4b1b5", + "url": "https://api.github.com/repos/illuminate/events/zipball/e0de667c68040d59a6ffc09e914536a1186870f0", + "reference": "e0de667c68040d59a6ffc09e914536a1186870f0", "shasum": "" }, "require": { @@ -1341,20 +1347,20 @@ "issues": "https://github.com/laravel/framework/issues", "source": "https://github.com/laravel/framework" }, - "time": "2025-05-13T15:08:45+00:00" + "time": "2025-10-21T15:10:34+00:00" }, { "name": "illuminate/filesystem", - "version": "v12.18.0", + "version": "v12.37.0", "source": { "type": "git", "url": "https://github.com/illuminate/filesystem.git", - "reference": "80fe8605cfa360fdbc85f67c19801a9657615aab" + "reference": "b1fbb20010e868f838feac86aeac8ba439fca10d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/illuminate/filesystem/zipball/80fe8605cfa360fdbc85f67c19801a9657615aab", - "reference": "80fe8605cfa360fdbc85f67c19801a9657615aab", + "url": "https://api.github.com/repos/illuminate/filesystem/zipball/b1fbb20010e868f838feac86aeac8ba439fca10d", + "reference": "b1fbb20010e868f838feac86aeac8ba439fca10d", "shasum": "" }, "require": { @@ -1408,20 +1414,20 @@ "issues": "https://github.com/laravel/framework/issues", "source": "https://github.com/laravel/framework" }, - "time": "2025-05-13T15:08:45+00:00" + "time": "2025-10-29T15:59:33+00:00" }, { "name": "illuminate/http", - "version": "v12.18.0", + "version": "v12.37.0", "source": { "type": "git", "url": "https://github.com/illuminate/http.git", - "reference": "bbafb2b2e629530e516d52a1fa46dfeb09b5e184" + "reference": "5c864f562b2fded08622ee19cd53260aef3db9a0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/illuminate/http/zipball/bbafb2b2e629530e516d52a1fa46dfeb09b5e184", - "reference": "bbafb2b2e629530e516d52a1fa46dfeb09b5e184", + "url": "https://api.github.com/repos/illuminate/http/zipball/5c864f562b2fded08622ee19cd53260aef3db9a0", + "reference": "5c864f562b2fded08622ee19cd53260aef3db9a0", "shasum": "" }, "require": { @@ -1437,7 +1443,8 @@ "symfony/http-foundation": "^7.2.0", "symfony/http-kernel": "^7.2.0", "symfony/mime": "^7.2.0", - "symfony/polyfill-php83": "^1.31" + "symfony/polyfill-php83": "^1.33", + "symfony/polyfill-php85": "^1.33" }, "suggest": { "ext-gd": "Required to use Illuminate\\Http\\Testing\\FileFactory::image()." @@ -1469,11 +1476,11 @@ "issues": "https://github.com/laravel/framework/issues", "source": "https://github.com/laravel/framework" }, - "time": "2025-06-06T18:14:49+00:00" + "time": "2025-10-28T14:38:15+00:00" }, { "name": "illuminate/macroable", - "version": "v12.18.0", + "version": "v12.37.0", "source": { "type": "git", "url": "https://github.com/illuminate/macroable.git", @@ -1519,16 +1526,16 @@ }, { "name": "illuminate/pagination", - "version": "v12.18.0", + "version": "v12.37.0", "source": { "type": "git", "url": "https://github.com/illuminate/pagination.git", - "reference": "7f68669ad7d8a706883b8409f6b7254fdc437f67" + "reference": "1d95e70671177108b202e6ceb61bc7bc9924bdbf" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/illuminate/pagination/zipball/7f68669ad7d8a706883b8409f6b7254fdc437f67", - "reference": "7f68669ad7d8a706883b8409f6b7254fdc437f67", + "url": "https://api.github.com/repos/illuminate/pagination/zipball/1d95e70671177108b202e6ceb61bc7bc9924bdbf", + "reference": "1d95e70671177108b202e6ceb61bc7bc9924bdbf", "shasum": "" }, "require": { @@ -1565,27 +1572,31 @@ "issues": "https://github.com/laravel/framework/issues", "source": "https://github.com/laravel/framework" }, - "time": "2025-05-28T21:37:18+00:00" + "time": "2025-09-02T15:31:06+00:00" }, { "name": "illuminate/pipeline", - "version": "v12.18.0", + "version": "v12.37.0", "source": { "type": "git", "url": "https://github.com/illuminate/pipeline.git", - "reference": "a1039dfe54854470cdda37782bab0901aa588dd4" + "reference": "b6a14c20d69a44bf0a6fba664a00d23ca71770ee" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/illuminate/pipeline/zipball/a1039dfe54854470cdda37782bab0901aa588dd4", - "reference": "a1039dfe54854470cdda37782bab0901aa588dd4", + "url": "https://api.github.com/repos/illuminate/pipeline/zipball/b6a14c20d69a44bf0a6fba664a00d23ca71770ee", + "reference": "b6a14c20d69a44bf0a6fba664a00d23ca71770ee", "shasum": "" }, "require": { "illuminate/contracts": "^12.0", + "illuminate/macroable": "^12.0", "illuminate/support": "^12.0", "php": "^8.2" }, + "suggest": { + "illuminate/database": "Required to use database transactions (^12.0)." + }, "type": "library", "extra": { "branch-alias": { @@ -1613,20 +1624,20 @@ "issues": "https://github.com/laravel/framework/issues", "source": "https://github.com/laravel/framework" }, - "time": "2025-05-13T15:08:45+00:00" + "time": "2025-08-20T13:36:50+00:00" }, { "name": "illuminate/redis", - "version": "v12.18.0", + "version": "v12.37.0", "source": { "type": "git", "url": "https://github.com/illuminate/redis.git", - "reference": "b225dc28092b00ed0555df66ce3a3a448b4dd8ae" + "reference": "8a397f00036974404d5d4d94eacfe8050af8e7f2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/illuminate/redis/zipball/b225dc28092b00ed0555df66ce3a3a448b4dd8ae", - "reference": "b225dc28092b00ed0555df66ce3a3a448b4dd8ae", + "url": "https://api.github.com/repos/illuminate/redis/zipball/8a397f00036974404d5d4d94eacfe8050af8e7f2", + "reference": "8a397f00036974404d5d4d94eacfe8050af8e7f2", "shasum": "" }, "require": { @@ -1667,20 +1678,20 @@ "issues": "https://github.com/laravel/framework/issues", "source": "https://github.com/laravel/framework" }, - "time": "2025-05-13T15:08:45+00:00" + "time": "2025-10-26T17:11:19+00:00" }, { "name": "illuminate/session", - "version": "v12.18.0", + "version": "v12.37.0", "source": { "type": "git", "url": "https://github.com/illuminate/session.git", - "reference": "9d27155b34bca502fe3e1adc16035b46d5ef3ed8" + "reference": "c376e4a0dda79a88462e8a7d16cc924aa3eb6e15" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/illuminate/session/zipball/9d27155b34bca502fe3e1adc16035b46d5ef3ed8", - "reference": "9d27155b34bca502fe3e1adc16035b46d5ef3ed8", + "url": "https://api.github.com/repos/illuminate/session/zipball/c376e4a0dda79a88462e8a7d16cc924aa3eb6e15", + "reference": "c376e4a0dda79a88462e8a7d16cc924aa3eb6e15", "shasum": "" }, "require": { @@ -1724,20 +1735,20 @@ "issues": "https://github.com/laravel/framework/issues", "source": "https://github.com/laravel/framework" }, - "time": "2025-05-13T15:08:45+00:00" + "time": "2025-10-26T17:11:19+00:00" }, { "name": "illuminate/support", - "version": "v12.18.0", + "version": "v12.37.0", "source": { "type": "git", "url": "https://github.com/illuminate/support.git", - "reference": "3c3894ba09a86663b747efaa6822639f1c32ea01" + "reference": "480df979c8ffe700d6adb6e6e64abe04b32e02e3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/illuminate/support/zipball/3c3894ba09a86663b747efaa6822639f1c32ea01", - "reference": "3c3894ba09a86663b747efaa6822639f1c32ea01", + "url": "https://api.github.com/repos/illuminate/support/zipball/480df979c8ffe700d6adb6e6e64abe04b32e02e3", + "reference": "480df979c8ffe700d6adb6e6e64abe04b32e02e3", "shasum": "" }, "require": { @@ -1751,6 +1762,8 @@ "illuminate/macroable": "^12.0", "nesbot/carbon": "^3.8.4", "php": "^8.2", + "symfony/polyfill-php83": "^1.33", + "symfony/polyfill-php85": "^1.33", "voku/portable-ascii": "^2.0.2" }, "conflict": { @@ -1801,7 +1814,7 @@ "issues": "https://github.com/laravel/framework/issues", "source": "https://github.com/laravel/framework" }, - "time": "2025-06-06T18:15:26+00:00" + "time": "2025-10-31T16:20:10+00:00" }, { "name": "jaeger/g-http", @@ -1960,16 +1973,16 @@ }, { "name": "laravel/serializable-closure", - "version": "v2.0.4", + "version": "v2.0.6", "source": { "type": "git", "url": "https://github.com/laravel/serializable-closure.git", - "reference": "b352cf0534aa1ae6b4d825d1e762e35d43f8a841" + "reference": "038ce42edee619599a1debb7e81d7b3759492819" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/serializable-closure/zipball/b352cf0534aa1ae6b4d825d1e762e35d43f8a841", - "reference": "b352cf0534aa1ae6b4d825d1e762e35d43f8a841", + "url": "https://api.github.com/repos/laravel/serializable-closure/zipball/038ce42edee619599a1debb7e81d7b3759492819", + "reference": "038ce42edee619599a1debb7e81d7b3759492819", "shasum": "" }, "require": { @@ -2017,27 +2030,21 @@ "issues": "https://github.com/laravel/serializable-closure/issues", "source": "https://github.com/laravel/serializable-closure" }, - "time": "2025-03-19T13:51:03+00:00" + "time": "2025-10-09T13:42:30+00:00" }, { "name": "league/flysystem", - "version": "3.29.0", + "version": "3.30.2", "source": { "type": "git", "url": "https://github.com/thephpleague/flysystem.git", - "reference": "0adc0d9a51852e170e0028a60bd271726626d3f0" + "reference": "5966a8ba23e62bdb518dd9e0e665c2dbd4b5b277" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/flysystem/zipball/0adc0d9a51852e170e0028a60bd271726626d3f0", - "reference": "0adc0d9a51852e170e0028a60bd271726626d3f0", - "shasum": "", - "mirrors": [ - { - "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", - "preferred": true - } - ] + "url": "https://api.github.com/repos/thephpleague/flysystem/zipball/5966a8ba23e62bdb518dd9e0e665c2dbd4b5b277", + "reference": "5966a8ba23e62bdb518dd9e0e665c2dbd4b5b277", + "shasum": "" }, "require": { "league/flysystem-local": "^3.0.0", @@ -2060,13 +2067,13 @@ "composer/semver": "^3.0", "ext-fileinfo": "*", "ext-ftp": "*", - "ext-mongodb": "^1.3", + "ext-mongodb": "^1.3|^2", "ext-zip": "*", "friendsofphp/php-cs-fixer": "^3.5", "google/cloud-storage": "^1.23", "guzzlehttp/psr7": "^2.6", "microsoft/azure-storage-blob": "^1.1", - "mongodb/mongodb": "^1.2", + "mongodb/mongodb": "^1.2|^2", "phpseclib/phpseclib": "^3.0.36", "phpstan/phpstan": "^1.10", "phpunit/phpunit": "^9.5.11|^10.0", @@ -2104,29 +2111,23 @@ ], "support": { "issues": "https://github.com/thephpleague/flysystem/issues", - "source": "https://github.com/thephpleague/flysystem/tree/3.29.0" + "source": "https://github.com/thephpleague/flysystem/tree/3.30.2" }, - "time": "2024-09-29T11:59:11+00:00" + "time": "2025-11-10T17:13:11+00:00" }, { "name": "league/flysystem-local", - "version": "3.29.0", + "version": "3.30.2", "source": { "type": "git", "url": "https://github.com/thephpleague/flysystem-local.git", - "reference": "e0e8d52ce4b2ed154148453d321e97c8e931bd27" + "reference": "ab4f9d0d672f601b102936aa728801dd1a11968d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/flysystem-local/zipball/e0e8d52ce4b2ed154148453d321e97c8e931bd27", - "reference": "e0e8d52ce4b2ed154148453d321e97c8e931bd27", - "shasum": "", - "mirrors": [ - { - "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", - "preferred": true - } - ] + "url": "https://api.github.com/repos/thephpleague/flysystem-local/zipball/ab4f9d0d672f601b102936aa728801dd1a11968d", + "reference": "ab4f9d0d672f601b102936aa728801dd1a11968d", + "shasum": "" }, "require": { "ext-fileinfo": "*", @@ -2159,29 +2160,23 @@ "local" ], "support": { - "source": "https://github.com/thephpleague/flysystem-local/tree/3.29.0" + "source": "https://github.com/thephpleague/flysystem-local/tree/3.30.2" }, - "time": "2024-08-09T21:24:39+00:00" + "time": "2025-11-10T11:23:37+00:00" }, { "name": "league/mime-type-detection", - "version": "1.15.0", + "version": "1.16.0", "source": { "type": "git", "url": "https://github.com/thephpleague/mime-type-detection.git", - "reference": "ce0f4d1e8a6f4eb0ddff33f57c69c50fd09f4301" + "reference": "2d6702ff215bf922936ccc1ad31007edc76451b9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/mime-type-detection/zipball/ce0f4d1e8a6f4eb0ddff33f57c69c50fd09f4301", - "reference": "ce0f4d1e8a6f4eb0ddff33f57c69c50fd09f4301", - "shasum": "", - "mirrors": [ - { - "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", - "preferred": true - } - ] + "url": "https://api.github.com/repos/thephpleague/mime-type-detection/zipball/2d6702ff215bf922936ccc1ad31007edc76451b9", + "reference": "2d6702ff215bf922936ccc1ad31007edc76451b9", + "shasum": "" }, "require": { "ext-fileinfo": "*", @@ -2211,7 +2206,7 @@ "description": "Mime-type detection for Flysystem", "support": { "issues": "https://github.com/thephpleague/mime-type-detection/issues", - "source": "https://github.com/thephpleague/mime-type-detection/tree/1.15.0" + "source": "https://github.com/thephpleague/mime-type-detection/tree/1.16.0" }, "funding": [ { @@ -2223,26 +2218,26 @@ "type": "tidelift" } ], - "time": "2024-01-28T23:22:08+00:00" + "time": "2024-09-21T08:32:55+00:00" }, { "name": "maennchen/zipstream-php", - "version": "3.1.2", + "version": "3.2.0", "source": { "type": "git", "url": "https://github.com/maennchen/ZipStream-PHP.git", - "reference": "aeadcf5c412332eb426c0f9b4485f6accba2a99f" + "reference": "9712d8fa4cdf9240380b01eb4be55ad8dcf71416" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/maennchen/ZipStream-PHP/zipball/aeadcf5c412332eb426c0f9b4485f6accba2a99f", - "reference": "aeadcf5c412332eb426c0f9b4485f6accba2a99f", + "url": "https://api.github.com/repos/maennchen/ZipStream-PHP/zipball/9712d8fa4cdf9240380b01eb4be55ad8dcf71416", + "reference": "9712d8fa4cdf9240380b01eb4be55ad8dcf71416", "shasum": "" }, "require": { "ext-mbstring": "*", "ext-zlib": "*", - "php-64bit": "^8.2" + "php-64bit": "^8.3" }, "require-dev": { "brianium/paratest": "^7.7", @@ -2251,7 +2246,7 @@ "guzzlehttp/guzzle": "^7.5", "mikey179/vfsstream": "^1.6", "php-coveralls/php-coveralls": "^2.5", - "phpunit/phpunit": "^11.0", + "phpunit/phpunit": "^12.0", "vimeo/psalm": "^6.0" }, "suggest": { @@ -2293,7 +2288,7 @@ ], "support": { "issues": "https://github.com/maennchen/ZipStream-PHP/issues", - "source": "https://github.com/maennchen/ZipStream-PHP/tree/3.1.2" + "source": "https://github.com/maennchen/ZipStream-PHP/tree/3.2.0" }, "funding": [ { @@ -2301,7 +2296,7 @@ "type": "github" } ], - "time": "2025-01-27T12:07:53+00:00" + "time": "2025-07-17T11:15:13+00:00" }, { "name": "markbaker/complex", @@ -2747,16 +2742,16 @@ }, { "name": "nesbot/carbon", - "version": "3.10.0", + "version": "3.10.3", "source": { "type": "git", "url": "https://github.com/CarbonPHP/carbon.git", - "reference": "c1397390dd0a7e0f11660f0ae20f753d88c1f3d9" + "reference": "8e3643dcd149ae0fe1d2ff4f2c8e4bbfad7c165f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/CarbonPHP/carbon/zipball/c1397390dd0a7e0f11660f0ae20f753d88c1f3d9", - "reference": "c1397390dd0a7e0f11660f0ae20f753d88c1f3d9", + "url": "https://api.github.com/repos/CarbonPHP/carbon/zipball/8e3643dcd149ae0fe1d2ff4f2c8e4bbfad7c165f", + "reference": "8e3643dcd149ae0fe1d2ff4f2c8e4bbfad7c165f", "shasum": "" }, "require": { @@ -2774,13 +2769,13 @@ "require-dev": { "doctrine/dbal": "^3.6.3 || ^4.0", "doctrine/orm": "^2.15.2 || ^3.0", - "friendsofphp/php-cs-fixer": "^3.75.0", + "friendsofphp/php-cs-fixer": "^v3.87.1", "kylekatarnls/multi-tester": "^2.5.3", "phpmd/phpmd": "^2.15.0", "phpstan/extension-installer": "^1.4.3", - "phpstan/phpstan": "^2.1.17", - "phpunit/phpunit": "^10.5.46", - "squizlabs/php_codesniffer": "^3.13.0" + "phpstan/phpstan": "^2.1.22", + "phpunit/phpunit": "^10.5.53", + "squizlabs/php_codesniffer": "^3.13.4" }, "bin": [ "bin/carbon" @@ -2848,7 +2843,7 @@ "type": "tidelift" } ], - "time": "2025-06-12T10:24:28+00:00" + "time": "2025-09-06T13:39:36+00:00" }, { "name": "nikic/fast-route", @@ -2952,16 +2947,16 @@ }, { "name": "phpmailer/phpmailer", - "version": "v6.10.0", + "version": "v6.12.0", "source": { "type": "git", "url": "https://github.com/PHPMailer/PHPMailer.git", - "reference": "bf74d75a1fde6beaa34a0ddae2ec5fce0f72a144" + "reference": "d1ac35d784bf9f5e61b424901d5a014967f15b12" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPMailer/PHPMailer/zipball/bf74d75a1fde6beaa34a0ddae2ec5fce0f72a144", - "reference": "bf74d75a1fde6beaa34a0ddae2ec5fce0f72a144", + "url": "https://api.github.com/repos/PHPMailer/PHPMailer/zipball/d1ac35d784bf9f5e61b424901d5a014967f15b12", + "reference": "d1ac35d784bf9f5e61b424901d5a014967f15b12", "shasum": "" }, "require": { @@ -3021,7 +3016,7 @@ "description": "PHPMailer is a full-featured email creation and transfer class for PHP", "support": { "issues": "https://github.com/PHPMailer/PHPMailer/issues", - "source": "https://github.com/PHPMailer/PHPMailer/tree/v6.10.0" + "source": "https://github.com/PHPMailer/PHPMailer/tree/v6.12.0" }, "funding": [ { @@ -3029,7 +3024,7 @@ "type": "github" } ], - "time": "2025-04-24T15:19:31+00:00" + "time": "2025-10-15T16:49:08+00:00" }, { "name": "phpoffice/phpspreadsheet", @@ -3139,16 +3134,16 @@ }, { "name": "phpoption/phpoption", - "version": "1.9.3", + "version": "1.9.4", "source": { "type": "git", "url": "https://github.com/schmittjoh/php-option.git", - "reference": "e3fac8b24f56113f7cb96af14958c0dd16330f54" + "reference": "638a154f8d4ee6a5cfa96d6a34dfbe0cffa9566d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/schmittjoh/php-option/zipball/e3fac8b24f56113f7cb96af14958c0dd16330f54", - "reference": "e3fac8b24f56113f7cb96af14958c0dd16330f54", + "url": "https://api.github.com/repos/schmittjoh/php-option/zipball/638a154f8d4ee6a5cfa96d6a34dfbe0cffa9566d", + "reference": "638a154f8d4ee6a5cfa96d6a34dfbe0cffa9566d", "shasum": "" }, "require": { @@ -3156,7 +3151,7 @@ }, "require-dev": { "bamarni/composer-bin-plugin": "^1.8.2", - "phpunit/phpunit": "^8.5.39 || ^9.6.20 || ^10.5.28" + "phpunit/phpunit": "^8.5.44 || ^9.6.25 || ^10.5.53 || ^11.5.34" }, "type": "library", "extra": { @@ -3198,7 +3193,7 @@ ], "support": { "issues": "https://github.com/schmittjoh/php-option/issues", - "source": "https://github.com/schmittjoh/php-option/tree/1.9.3" + "source": "https://github.com/schmittjoh/php-option/tree/1.9.4" }, "funding": [ { @@ -3210,7 +3205,7 @@ "type": "tidelift" } ], - "time": "2024-07-20T21:41:07+00:00" + "time": "2025-08-21T11:53:16+00:00" }, { "name": "psr/cache", @@ -3801,13 +3796,7 @@ "type": "zip", "url": "https://api.github.com/repos/shopwwi/filesystem-oss/zipball/85cb385d1a5d11ca3c22bc485a7b18c588fe7645", "reference": "85cb385d1a5d11ca3c22bc485a7b18c588fe7645", - "shasum": "", - "mirrors": [ - { - "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", - "preferred": true - } - ] + "shasum": "" }, "require": { "aliyuncs/oss-sdk-php": "^2.4", @@ -3860,13 +3849,7 @@ "type": "zip", "url": "https://api.github.com/repos/shopwwi/webman-filesystem/zipball/26ac82515d3787943573c50d1a6f0ca339f0d3d4", "reference": "26ac82515d3787943573c50d1a6f0ca339f0d3d4", - "shasum": "", - "mirrors": [ - { - "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", - "preferred": true - } - ] + "shasum": "" }, "require": { "league/flysystem": "^2.3|^3.0", @@ -3909,16 +3892,16 @@ }, { "name": "symfony/cache", - "version": "v7.3.0", + "version": "v7.3.6", "source": { "type": "git", "url": "https://github.com/symfony/cache.git", - "reference": "c4b217b578c11ec764867aa0c73e602c602965de" + "reference": "1277a1ec61c8d93ea61b2a59738f1deb9bfb6701" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/cache/zipball/c4b217b578c11ec764867aa0c73e602c602965de", - "reference": "c4b217b578c11ec764867aa0c73e602c602965de", + "url": "https://api.github.com/repos/symfony/cache/zipball/1277a1ec61c8d93ea61b2a59738f1deb9bfb6701", + "reference": "1277a1ec61c8d93ea61b2a59738f1deb9bfb6701", "shasum": "" }, "require": { @@ -3987,7 +3970,7 @@ "psr6" ], "support": { - "source": "https://github.com/symfony/cache/tree/v7.3.0" + "source": "https://github.com/symfony/cache/tree/v7.3.6" }, "funding": [ { @@ -3998,12 +3981,16 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2025-05-06T19:00:13+00:00" + "time": "2025-10-30T13:22:58+00:00" }, { "name": "symfony/cache-contracts", @@ -4157,16 +4144,16 @@ }, { "name": "symfony/console", - "version": "v7.3.0", + "version": "v7.3.6", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "66c1440edf6f339fd82ed6c7caa76cb006211b44" + "reference": "c28ad91448f86c5f6d9d2c70f0cf68bf135f252a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/66c1440edf6f339fd82ed6c7caa76cb006211b44", - "reference": "66c1440edf6f339fd82ed6c7caa76cb006211b44", + "url": "https://api.github.com/repos/symfony/console/zipball/c28ad91448f86c5f6d9d2c70f0cf68bf135f252a", + "reference": "c28ad91448f86c5f6d9d2c70f0cf68bf135f252a", "shasum": "" }, "require": { @@ -4231,7 +4218,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v7.3.0" + "source": "https://github.com/symfony/console/tree/v7.3.6" }, "funding": [ { @@ -4242,12 +4229,16 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2025-05-24T10:34:04+00:00" + "time": "2025-11-04T01:21:42+00:00" }, { "name": "symfony/deprecation-contracts", @@ -4318,16 +4309,16 @@ }, { "name": "symfony/error-handler", - "version": "v7.3.0", + "version": "v7.3.6", "source": { "type": "git", "url": "https://github.com/symfony/error-handler.git", - "reference": "cf68d225bc43629de4ff54778029aee6dc191b83" + "reference": "bbe40bfab84323d99dab491b716ff142410a92a8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/error-handler/zipball/cf68d225bc43629de4ff54778029aee6dc191b83", - "reference": "cf68d225bc43629de4ff54778029aee6dc191b83", + "url": "https://api.github.com/repos/symfony/error-handler/zipball/bbe40bfab84323d99dab491b716ff142410a92a8", + "reference": "bbe40bfab84323d99dab491b716ff142410a92a8", "shasum": "" }, "require": { @@ -4375,7 +4366,7 @@ "description": "Provides tools to manage errors and ease debugging PHP code", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/error-handler/tree/v7.3.0" + "source": "https://github.com/symfony/error-handler/tree/v7.3.6" }, "funding": [ { @@ -4386,25 +4377,29 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2025-05-29T07:19:49+00:00" + "time": "2025-10-31T19:12:50+00:00" }, { "name": "symfony/event-dispatcher", - "version": "v7.3.0", + "version": "v7.3.3", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher.git", - "reference": "497f73ac996a598c92409b44ac43b6690c4f666d" + "reference": "b7dc69e71de420ac04bc9ab830cf3ffebba48191" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/497f73ac996a598c92409b44ac43b6690c4f666d", - "reference": "497f73ac996a598c92409b44ac43b6690c4f666d", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/b7dc69e71de420ac04bc9ab830cf3ffebba48191", + "reference": "b7dc69e71de420ac04bc9ab830cf3ffebba48191", "shasum": "" }, "require": { @@ -4455,7 +4450,7 @@ "description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/event-dispatcher/tree/v7.3.0" + "source": "https://github.com/symfony/event-dispatcher/tree/v7.3.3" }, "funding": [ { @@ -4466,12 +4461,16 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2025-04-22T09:11:45+00:00" + "time": "2025-08-13T11:49:31+00:00" }, { "name": "symfony/event-dispatcher-contracts", @@ -4551,16 +4550,16 @@ }, { "name": "symfony/filesystem", - "version": "v7.3.0", + "version": "v7.3.6", "source": { "type": "git", "url": "https://github.com/symfony/filesystem.git", - "reference": "b8dce482de9d7c9fe2891155035a7248ab5c7fdb" + "reference": "e9bcfd7837928ab656276fe00464092cc9e1826a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/filesystem/zipball/b8dce482de9d7c9fe2891155035a7248ab5c7fdb", - "reference": "b8dce482de9d7c9fe2891155035a7248ab5c7fdb", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/e9bcfd7837928ab656276fe00464092cc9e1826a", + "reference": "e9bcfd7837928ab656276fe00464092cc9e1826a", "shasum": "" }, "require": { @@ -4597,7 +4596,7 @@ "description": "Provides basic utilities for the filesystem", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/filesystem/tree/v7.3.0" + "source": "https://github.com/symfony/filesystem/tree/v7.3.6" }, "funding": [ { @@ -4608,25 +4607,29 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2024-10-25T15:15:23+00:00" + "time": "2025-11-05T09:52:27+00:00" }, { "name": "symfony/finder", - "version": "v7.3.0", + "version": "v7.3.5", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "ec2344cf77a48253bbca6939aa3d2477773ea63d" + "reference": "9f696d2f1e340484b4683f7853b273abff94421f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/ec2344cf77a48253bbca6939aa3d2477773ea63d", - "reference": "ec2344cf77a48253bbca6939aa3d2477773ea63d", + "url": "https://api.github.com/repos/symfony/finder/zipball/9f696d2f1e340484b4683f7853b273abff94421f", + "reference": "9f696d2f1e340484b4683f7853b273abff94421f", "shasum": "" }, "require": { @@ -4661,7 +4664,7 @@ "description": "Finds files and directories via an intuitive fluent interface", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/finder/tree/v7.3.0" + "source": "https://github.com/symfony/finder/tree/v7.3.5" }, "funding": [ { @@ -4672,25 +4675,29 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2024-12-30T19:00:26+00:00" + "time": "2025-10-15T18:45:57+00:00" }, { "name": "symfony/http-foundation", - "version": "v7.3.0", + "version": "v7.3.6", "source": { "type": "git", "url": "https://github.com/symfony/http-foundation.git", - "reference": "4236baf01609667d53b20371486228231eb135fd" + "reference": "6379e490d6ecfc5c4224ff3a754b90495ecd135c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-foundation/zipball/4236baf01609667d53b20371486228231eb135fd", - "reference": "4236baf01609667d53b20371486228231eb135fd", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/6379e490d6ecfc5c4224ff3a754b90495ecd135c", + "reference": "6379e490d6ecfc5c4224ff3a754b90495ecd135c", "shasum": "" }, "require": { @@ -4740,7 +4747,7 @@ "description": "Defines an object-oriented layer for the HTTP specification", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/http-foundation/tree/v7.3.0" + "source": "https://github.com/symfony/http-foundation/tree/v7.3.6" }, "funding": [ { @@ -4751,25 +4758,29 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2025-05-12T14:48:23+00:00" + "time": "2025-11-06T11:05:57+00:00" }, { "name": "symfony/http-kernel", - "version": "v7.3.0", + "version": "v7.3.6", "source": { "type": "git", "url": "https://github.com/symfony/http-kernel.git", - "reference": "ac7b8e163e8c83dce3abcc055a502d4486051a9f" + "reference": "f9a34dc0196677250e3609c2fac9de9e1551a262" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-kernel/zipball/ac7b8e163e8c83dce3abcc055a502d4486051a9f", - "reference": "ac7b8e163e8c83dce3abcc055a502d4486051a9f", + "url": "https://api.github.com/repos/symfony/http-kernel/zipball/f9a34dc0196677250e3609c2fac9de9e1551a262", + "reference": "f9a34dc0196677250e3609c2fac9de9e1551a262", "shasum": "" }, "require": { @@ -4854,7 +4865,7 @@ "description": "Provides a structured process for converting a Request into a Response", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/http-kernel/tree/v7.3.0" + "source": "https://github.com/symfony/http-kernel/tree/v7.3.6" }, "funding": [ { @@ -4865,25 +4876,29 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2025-05-29T07:47:32+00:00" + "time": "2025-11-06T20:58:12+00:00" }, { "name": "symfony/mime", - "version": "v7.3.0", + "version": "v7.3.4", "source": { "type": "git", "url": "https://github.com/symfony/mime.git", - "reference": "0e7b19b2f399c31df0cdbe5d8cbf53f02f6cfcd9" + "reference": "b1b828f69cbaf887fa835a091869e55df91d0e35" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/mime/zipball/0e7b19b2f399c31df0cdbe5d8cbf53f02f6cfcd9", - "reference": "0e7b19b2f399c31df0cdbe5d8cbf53f02f6cfcd9", + "url": "https://api.github.com/repos/symfony/mime/zipball/b1b828f69cbaf887fa835a091869e55df91d0e35", + "reference": "b1b828f69cbaf887fa835a091869e55df91d0e35", "shasum": "" }, "require": { @@ -4938,7 +4953,7 @@ "mime-type" ], "support": { - "source": "https://github.com/symfony/mime/tree/v7.3.0" + "source": "https://github.com/symfony/mime/tree/v7.3.4" }, "funding": [ { @@ -4949,16 +4964,20 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2025-02-19T08:51:26+00:00" + "time": "2025-09-16T08:38:17+00:00" }, { "name": "symfony/polyfill-ctype", - "version": "v1.32.0", + "version": "v1.33.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-ctype.git", @@ -5017,7 +5036,7 @@ "portable" ], "support": { - "source": "https://github.com/symfony/polyfill-ctype/tree/v1.32.0" + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.33.0" }, "funding": [ { @@ -5028,6 +5047,10 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" @@ -5037,16 +5060,16 @@ }, { "name": "symfony/polyfill-intl-grapheme", - "version": "v1.32.0", + "version": "v1.33.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-grapheme.git", - "reference": "b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe" + "reference": "380872130d3a5dd3ace2f4010d95125fde5d5c70" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe", - "reference": "b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/380872130d3a5dd3ace2f4010d95125fde5d5c70", + "reference": "380872130d3a5dd3ace2f4010d95125fde5d5c70", "shasum": "" }, "require": { @@ -5095,7 +5118,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.32.0" + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.33.0" }, "funding": [ { @@ -5106,16 +5129,20 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2024-09-09T11:45:10+00:00" + "time": "2025-06-27T09:58:17+00:00" }, { "name": "symfony/polyfill-intl-idn", - "version": "v1.32.0", + "version": "v1.33.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-idn.git", @@ -5178,7 +5205,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.32.0" + "source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.33.0" }, "funding": [ { @@ -5189,6 +5216,10 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" @@ -5198,7 +5229,7 @@ }, { "name": "symfony/polyfill-intl-normalizer", - "version": "v1.32.0", + "version": "v1.33.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-normalizer.git", @@ -5259,7 +5290,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.32.0" + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.33.0" }, "funding": [ { @@ -5270,6 +5301,10 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" @@ -5279,7 +5314,7 @@ }, { "name": "symfony/polyfill-mbstring", - "version": "v1.32.0", + "version": "v1.33.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-mbstring.git", @@ -5340,7 +5375,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.32.0" + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.33.0" }, "funding": [ { @@ -5351,6 +5386,10 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" @@ -5360,7 +5399,7 @@ }, { "name": "symfony/polyfill-php80", - "version": "v1.32.0", + "version": "v1.33.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php80.git", @@ -5420,7 +5459,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php80/tree/v1.32.0" + "source": "https://github.com/symfony/polyfill-php80/tree/v1.33.0" }, "funding": [ { @@ -5431,6 +5470,10 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" @@ -5440,16 +5483,16 @@ }, { "name": "symfony/polyfill-php83", - "version": "v1.32.0", + "version": "v1.33.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php83.git", - "reference": "2fb86d65e2d424369ad2905e83b236a8805ba491" + "reference": "17f6f9a6b1735c0f163024d959f700cfbc5155e5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/2fb86d65e2d424369ad2905e83b236a8805ba491", - "reference": "2fb86d65e2d424369ad2905e83b236a8805ba491", + "url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/17f6f9a6b1735c0f163024d959f700cfbc5155e5", + "reference": "17f6f9a6b1735c0f163024d959f700cfbc5155e5", "shasum": "" }, "require": { @@ -5496,7 +5539,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php83/tree/v1.32.0" + "source": "https://github.com/symfony/polyfill-php83/tree/v1.33.0" }, "funding": [ { @@ -5507,25 +5550,189 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2024-09-09T11:45:10+00:00" + "time": "2025-07-08T02:45:35+00:00" }, { - "name": "symfony/service-contracts", - "version": "v3.6.0", + "name": "symfony/polyfill-php84", + "version": "v1.33.0", "source": { "type": "git", - "url": "https://github.com/symfony/service-contracts.git", - "reference": "f021b05a130d35510bd6b25fe9053c2a8a15d5d4" + "url": "https://github.com/symfony/polyfill-php84.git", + "reference": "d8ced4d875142b6a7426000426b8abc631d6b191" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/service-contracts/zipball/f021b05a130d35510bd6b25fe9053c2a8a15d5d4", - "reference": "f021b05a130d35510bd6b25fe9053c2a8a15d5d4", + "url": "https://api.github.com/repos/symfony/polyfill-php84/zipball/d8ced4d875142b6a7426000426b8abc631d6b191", + "reference": "d8ced4d875142b6a7426000426b8abc631d6b191", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php84\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.4+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php84/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-06-24T13:30:11+00:00" + }, + { + "name": "symfony/polyfill-php85", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php85.git", + "reference": "d4e5fcd4ab3d998ab16c0db48e6cbb9a01993f91" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php85/zipball/d4e5fcd4ab3d998ab16c0db48e6cbb9a01993f91", + "reference": "d4e5fcd4ab3d998ab16c0db48e6cbb9a01993f91", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php85\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.5+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php85/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-06-23T16:12:55+00:00" + }, + { + "name": "symfony/service-contracts", + "version": "v3.6.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/service-contracts.git", + "reference": "45112560a3ba2d715666a509a0bc9521d10b6c43" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/45112560a3ba2d715666a509a0bc9521d10b6c43", + "reference": "45112560a3ba2d715666a509a0bc9521d10b6c43", "shasum": "" }, "require": { @@ -5579,7 +5786,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/service-contracts/tree/v3.6.0" + "source": "https://github.com/symfony/service-contracts/tree/v3.6.1" }, "funding": [ { @@ -5590,25 +5797,29 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2025-04-25T09:37:31+00:00" + "time": "2025-07-15T11:30:57+00:00" }, { "name": "symfony/string", - "version": "v7.3.0", + "version": "v7.3.4", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "f3570b8c61ca887a9e2938e85cb6458515d2b125" + "reference": "f96476035142921000338bad71e5247fbc138872" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/f3570b8c61ca887a9e2938e85cb6458515d2b125", - "reference": "f3570b8c61ca887a9e2938e85cb6458515d2b125", + "url": "https://api.github.com/repos/symfony/string/zipball/f96476035142921000338bad71e5247fbc138872", + "reference": "f96476035142921000338bad71e5247fbc138872", "shasum": "" }, "require": { @@ -5623,7 +5834,6 @@ }, "require-dev": { "symfony/emoji": "^7.1", - "symfony/error-handler": "^6.4|^7.0", "symfony/http-client": "^6.4|^7.0", "symfony/intl": "^6.4|^7.0", "symfony/translation-contracts": "^2.5|^3.0", @@ -5666,7 +5876,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v7.3.0" + "source": "https://github.com/symfony/string/tree/v7.3.4" }, "funding": [ { @@ -5677,32 +5887,30 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2025-04-20T20:19:01+00:00" + "time": "2025-09-11T14:36:48+00:00" }, { "name": "symfony/translation", - "version": "v7.3.0", + "version": "v7.3.4", "source": { "type": "git", "url": "https://github.com/symfony/translation.git", - "reference": "4aba29076a29a3aa667e09b791e5f868973a8667" + "reference": "ec25870502d0c7072d086e8ffba1420c85965174" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/translation/zipball/4aba29076a29a3aa667e09b791e5f868973a8667", - "reference": "4aba29076a29a3aa667e09b791e5f868973a8667", - "shasum": "", - "mirrors": [ - { - "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", - "preferred": true - } - ] + "url": "https://api.github.com/repos/symfony/translation/zipball/ec25870502d0c7072d086e8ffba1420c85965174", + "reference": "ec25870502d0c7072d086e8ffba1420c85965174", + "shasum": "" }, "require": { "php": ">=8.2", @@ -5768,7 +5976,7 @@ "description": "Provides tools to internationalize your application", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/translation/tree/v7.3.0" + "source": "https://github.com/symfony/translation/tree/v7.3.4" }, "funding": [ { @@ -5779,25 +5987,29 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2025-05-29T07:19:49+00:00" + "time": "2025-09-07T11:39:36+00:00" }, { "name": "symfony/translation-contracts", - "version": "v3.6.0", + "version": "v3.6.1", "source": { "type": "git", "url": "https://github.com/symfony/translation-contracts.git", - "reference": "df210c7a2573f1913b2d17cc95f90f53a73d8f7d" + "reference": "65a8bc82080447fae78373aa10f8d13b38338977" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/translation-contracts/zipball/df210c7a2573f1913b2d17cc95f90f53a73d8f7d", - "reference": "df210c7a2573f1913b2d17cc95f90f53a73d8f7d", + "url": "https://api.github.com/repos/symfony/translation-contracts/zipball/65a8bc82080447fae78373aa10f8d13b38338977", + "reference": "65a8bc82080447fae78373aa10f8d13b38338977", "shasum": "" }, "require": { @@ -5846,7 +6058,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/translation-contracts/tree/v3.6.0" + "source": "https://github.com/symfony/translation-contracts/tree/v3.6.1" }, "funding": [ { @@ -5857,25 +6069,29 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2024-09-27T08:32:26+00:00" + "time": "2025-07-15T13:41:35+00:00" }, { "name": "symfony/var-dumper", - "version": "v7.3.0", + "version": "v7.3.5", "source": { "type": "git", "url": "https://github.com/symfony/var-dumper.git", - "reference": "548f6760c54197b1084e1e5c71f6d9d523f2f78e" + "reference": "476c4ae17f43a9a36650c69879dcf5b1e6ae724d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-dumper/zipball/548f6760c54197b1084e1e5c71f6d9d523f2f78e", - "reference": "548f6760c54197b1084e1e5c71f6d9d523f2f78e", + "url": "https://api.github.com/repos/symfony/var-dumper/zipball/476c4ae17f43a9a36650c69879dcf5b1e6ae724d", + "reference": "476c4ae17f43a9a36650c69879dcf5b1e6ae724d", "shasum": "" }, "require": { @@ -5887,7 +6103,6 @@ "symfony/console": "<6.4" }, "require-dev": { - "ext-iconv": "*", "symfony/console": "^6.4|^7.0", "symfony/http-kernel": "^6.4|^7.0", "symfony/process": "^6.4|^7.0", @@ -5930,7 +6145,7 @@ "dump" ], "support": { - "source": "https://github.com/symfony/var-dumper/tree/v7.3.0" + "source": "https://github.com/symfony/var-dumper/tree/v7.3.5" }, "funding": [ { @@ -5941,25 +6156,29 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2025-04-27T18:39:23+00:00" + "time": "2025-09-27T09:00:46+00:00" }, { "name": "symfony/var-exporter", - "version": "v7.3.0", + "version": "v7.3.4", "source": { "type": "git", "url": "https://github.com/symfony/var-exporter.git", - "reference": "c9a1168891b5aaadfd6332ef44393330b3498c4c" + "reference": "0f020b544a30a7fe8ba972e53ee48a74c0bc87f4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-exporter/zipball/c9a1168891b5aaadfd6332ef44393330b3498c4c", - "reference": "c9a1168891b5aaadfd6332ef44393330b3498c4c", + "url": "https://api.github.com/repos/symfony/var-exporter/zipball/0f020b544a30a7fe8ba972e53ee48a74c0bc87f4", + "reference": "0f020b544a30a7fe8ba972e53ee48a74c0bc87f4", "shasum": "" }, "require": { @@ -6007,7 +6226,7 @@ "serialize" ], "support": { - "source": "https://github.com/symfony/var-exporter/tree/v7.3.0" + "source": "https://github.com/symfony/var-exporter/tree/v7.3.4" }, "funding": [ { @@ -6018,12 +6237,16 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2025-05-15T09:04:05+00:00" + "time": "2025-09-11T10:12:26+00:00" }, { "name": "topthink/think-container", @@ -6358,16 +6581,16 @@ }, { "name": "webman/console", - "version": "v2.1.8", + "version": "v2.1.9", "source": { "type": "git", "url": "https://github.com/webman-php/console.git", - "reference": "2d4ce527810f91e1b4f583ec90eae24a0334ab10" + "reference": "09c995bf3d9a136c7c471b6db837a1fd56b7d206" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/webman-php/console/zipball/2d4ce527810f91e1b4f583ec90eae24a0334ab10", - "reference": "2d4ce527810f91e1b4f583ec90eae24a0334ab10", + "url": "https://api.github.com/repos/webman-php/console/zipball/09c995bf3d9a136c7c471b6db837a1fd56b7d206", + "reference": "09c995bf3d9a136c7c471b6db837a1fd56b7d206", "shasum": "" }, "require": { @@ -6408,7 +6631,7 @@ "source": "https://github.com/webman-php/console", "wiki": "http://www.workerman.net/doc/webman" }, - "time": "2025-06-02T00:42:16+00:00" + "time": "2025-10-14T09:11:21+00:00" }, { "name": "webman/database", @@ -6553,16 +6776,16 @@ }, { "name": "workerman/coroutine", - "version": "v1.1.3", + "version": "v1.1.4", "source": { "type": "git", "url": "https://github.com/workerman-php/coroutine.git", - "reference": "df8fc428967d512a74a8a7d80355c1d40228c9fa" + "reference": "b0bebfa9d41b992ad0a835ddf2ee8fa5d58eca44" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/workerman-php/coroutine/zipball/df8fc428967d512a74a8a7d80355c1d40228c9fa", - "reference": "df8fc428967d512a74a8a7d80355c1d40228c9fa", + "url": "https://api.github.com/repos/workerman-php/coroutine/zipball/b0bebfa9d41b992ad0a835ddf2ee8fa5d58eca44", + "reference": "b0bebfa9d41b992ad0a835ddf2ee8fa5d58eca44", "shasum": "" }, "require": { @@ -6586,9 +6809,9 @@ "description": "Workerman coroutine", "support": { "issues": "https://github.com/workerman-php/coroutine/issues", - "source": "https://github.com/workerman-php/coroutine/tree/v1.1.3" + "source": "https://github.com/workerman-php/coroutine/tree/v1.1.4" }, - "time": "2025-02-17T03:34:21+00:00" + "time": "2025-10-11T15:09:08+00:00" }, { "name": "workerman/crontab", @@ -6696,23 +6919,17 @@ }, { "name": "workerman/redis", - "version": "v2.0.4", + "version": "v2.0.5", "source": { "type": "git", "url": "https://github.com/walkor/redis.git", - "reference": "82b398c5bdd5d683a3458aaf645b159b7b39b84e" + "reference": "49627c1809eff1ef7175eb8ee7549234a1d67ec5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/walkor/redis/zipball/82b398c5bdd5d683a3458aaf645b159b7b39b84e", - "reference": "82b398c5bdd5d683a3458aaf645b159b7b39b84e", - "shasum": "", - "mirrors": [ - { - "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", - "preferred": true - } - ] + "url": "https://api.github.com/repos/walkor/redis/zipball/49627c1809eff1ef7175eb8ee7549234a1d67ec5", + "reference": "49627c1809eff1ef7175eb8ee7549234a1d67ec5", + "shasum": "" }, "require": { "php": ">=7", @@ -6731,9 +6948,9 @@ "homepage": "http://www.workerman.net", "support": { "issues": "https://github.com/walkor/redis/issues", - "source": "https://github.com/walkor/redis/tree/v2.0.4" + "source": "https://github.com/walkor/redis/tree/v2.0.5" }, - "time": "2025-04-05T07:45:12+00:00" + "time": "2025-04-07T01:58:58+00:00" }, { "name": "workerman/redis-queue", @@ -6747,13 +6964,7 @@ "type": "zip", "url": "https://api.github.com/repos/walkor/redis-queue/zipball/75dbf7ed2ea228c45dc0df82c0fea35879b715d0", "reference": "75dbf7ed2ea228c45dc0df82c0fea35879b715d0", - "shasum": "", - "mirrors": [ - { - "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", - "preferred": true - } - ] + "shasum": "" }, "require": { "php": ">=7.0", @@ -6780,16 +6991,16 @@ }, { "name": "workerman/webman-framework", - "version": "v2.1.2", + "version": "v2.1.4", "source": { "type": "git", "url": "https://github.com/walkor/webman-framework.git", - "reference": "f803bd867f07bb0929faef060b59a19a44186bfc" + "reference": "f19d1f9e47cc50210555f0b63db5ae1dd584f793" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/walkor/webman-framework/zipball/f803bd867f07bb0929faef060b59a19a44186bfc", - "reference": "f803bd867f07bb0929faef060b59a19a44186bfc", + "url": "https://api.github.com/repos/walkor/webman-framework/zipball/f19d1f9e47cc50210555f0b63db5ae1dd584f793", + "reference": "f19d1f9e47cc50210555f0b63db5ae1dd584f793", "shasum": "" }, "require": { @@ -6797,7 +7008,7 @@ "nikic/fast-route": "^1.3", "php": ">=8.1", "psr/container": ">=1.0", - "psr/log": "^3.0", + "psr/log": "^2.0 || ^3.0", "workerman/workerman": "^5.1 || dev-master" }, "suggest": { @@ -6842,20 +7053,20 @@ "source": "https://github.com/walkor/webman-framework", "wiki": "https://doc.workerman.net/" }, - "time": "2025-03-10T11:52:22+00:00" + "time": "2025-11-10T06:59:23+00:00" }, { "name": "workerman/workerman", - "version": "v5.1.3", + "version": "v5.1.6", "source": { "type": "git", "url": "https://github.com/walkor/workerman.git", - "reference": "371f3a5decb28f1bd3464ae26d47ea1a4cf0a3c5" + "reference": "9b83bdf9fd0eaff9419a240b47da870447583112" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/walkor/workerman/zipball/371f3a5decb28f1bd3464ae26d47ea1a4cf0a3c5", - "reference": "371f3a5decb28f1bd3464ae26d47ea1a4cf0a3c5", + "url": "https://api.github.com/repos/walkor/workerman/zipball/9b83bdf9fd0eaff9419a240b47da870447583112", + "reference": "9b83bdf9fd0eaff9419a240b47da870447583112", "shasum": "" }, "require": { @@ -6868,9 +7079,9 @@ }, "require-dev": { "guzzlehttp/guzzle": "^7.0", - "mockery/mockery": "^1.6", + "mockery/mockery": "2.0.x-dev", "pestphp/pest": "2.x-dev", - "phpstan/phpstan": "1.11.x-dev" + "phpstan/phpstan": "2.1.x-dev" }, "suggest": { "ext-event": "For better performance. " @@ -6918,7 +7129,7 @@ "type": "patreon" } ], - "time": "2025-06-12T13:34:04+00:00" + "time": "2025-11-12T01:36:20+00:00" } ], "packages-dev": [], From 20188521fdf94e9f57cddc55dc070ede5b4d942c Mon Sep 17 00:00:00 2001 From: mzl Date: Thu, 13 Nov 2025 11:45:38 +0800 Subject: [PATCH 082/110] ts --- app/common/service/UserService.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/common/service/UserService.php b/app/common/service/UserService.php index f4aab85..bb8ae75 100644 --- a/app/common/service/UserService.php +++ b/app/common/service/UserService.php @@ -215,8 +215,9 @@ class UserService $config['where']['status'] = $p['status']; } + $config['with']= ['reportToInfo']; if (!in_array(1, $request->user->role_ids)) { - $config['with']= ['reportToInfo']; + $config['where']['reportToInfo.id'] = $request->user->uid; } // $config['dataLimit']['type'] = 'relation'; From 79d19a53e697ed61e108c2e5f7dcff9e444e3851 Mon Sep 17 00:00:00 2001 From: mzl Date: Mon, 24 Nov 2025 18:29:11 +0800 Subject: [PATCH 083/110] =?UTF-8?q?=E4=BA=8C=E6=89=8B=E8=BD=A6=E4=BF=A1?= =?UTF-8?q?=E6=81=AF=E7=88=AC=E8=99=AB=E6=9C=8D=E5=8A=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/common/service/UsedCarCrawlService.php | 647 +++++++++++++++++++++ app/controller/IndexController.php | 15 +- app/process/CheckWeb.php | 6 + 3 files changed, 664 insertions(+), 4 deletions(-) create mode 100644 app/common/service/UsedCarCrawlService.php diff --git a/app/common/service/UsedCarCrawlService.php b/app/common/service/UsedCarCrawlService.php new file mode 100644 index 0000000..587b03e --- /dev/null +++ b/app/common/service/UsedCarCrawlService.php @@ -0,0 +1,647 @@ + $scene, + 'url' => $url, + 'id' => $id + ], JSON_UNESCAPED_UNICODE)); + } + + /** + * 获取队列url + * + * $scene 二手信息 usedcar 配置信息 config + */ + public function rUrl() + { + return json_decode(Redis::rPop('usedcar:urls'), true); + } + + /** + * 执行抓取 + */ + public function doData() + { + try { + $raw = $this->rUrl(); + + if (!$raw) { + return false; + } +echo '开始抓取~:'.date('Y-m-d H:i:s').PHP_EOL; + $html = $this->getHtml($raw['url']); + + switch ($raw['scene']) { + case 'usedcar': + $info = $this->getUsedDetail($html, $raw['url']); + $id = $this->saveUsedcarBasicToDb($info); + $this->iUrl($info['car_id_url'], 'config', $id); + + echo '一阶段 抓取成功~:'.date('Y-m-d H:i:s').PHP_EOL; + break; + + case 'config': + $data = $this->getConfigDetail($html); + $this->saveUsedcarConfigToDb($data, $raw['id']); + + echo '二阶段 抓取成功~:'.date('Y-m-d H:i:s').PHP_EOL; + break; + } + + return true; + } catch (\Throwable $th) { + throw new BusinessException($th->getMessage().'|'.$th->getLine().'|'.$th->getFile()); + } + } + + /** + * 获取html + */ + public function getHtml($url) + { + $client = new Client(); + $res = $client->request('GET', $url, [ + 'headers' => [ + 'User-Agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) ' + . 'AppleWebKit/537.36 (KHTML, like Gecko) ' + . 'Chrome/120.0.0.0 Safari/537.36', + 'Accept-Language' => 'zh-CN,zh;q=0.9', + 'Referer' => 'https://www.dongchedi.com/' + ] + ]); + return (string)$res->getBody(); + } + + /** + * 获取二手信息 + */ + public function getUsedDetail(string $html = '', string $url = '') + { + $ql = QueryList::html($html); + + $title = $ql->find('h1')->text(); + $price = $ql->find('.head-info_price-wrap__Y4bxi > p')->text(); + $pageText = $ql->find('body')->text(); + $jsonText = $ql->find('#__NEXT_DATA__')->text(); + + // 新车指导价 + $newPrice = ''; + if ($pageText) { + if (preg_match('/新车指导价[::]\s*([^\r\n万]*万)/u', $pageText, $m)) { + $newPrice = trim(preg_replace('/[^0-9.]/', '', $m[1])); + } + } + + // 比新车省 + $priceDifference = ''; + if ($pageText) { + if (preg_match('/比新车省[::]\s*([^\r\n万]*万)/u', $pageText, $m)) { + $priceDifference = trim(preg_replace('/[^0-9.]/', '', $m[1])); + } + } + + // 车况介绍 + $conditionDesc = ''; + if ($pageText) { + if (preg_match('/车况介绍\s*(.*?)\s*推荐理由/su', $pageText, $m)) { + $conditionDesc = trim($m[1]); + } + } + + $jsonArray = json_decode($jsonText, true); + + // 车辆相册 + $images = $jsonArray['props']['pageProps']['skuDetail']['head_images'] ?? []; + + // 车辆ID + $carId = $jsonArray['props']['pageProps']['skuDetail']['car_info']['car_id'] ?? 0; + + // 车辆年份 + $year = $jsonArray['props']['pageProps']['skuDetail']['car_info']['year'] ?? 0; + + // 车辆ID URL + $carIdUrl = $carId ? ('https://www.dongchedi.com/auto/params-carIds-' . $carId) : ''; + + // 能源类型 + $fuelForm = $jsonArray['props']['pageProps']['skuDetail']['car_config_overview']['power']['fuel_form'] ?? ''; + + // 使用里程 + $importantText = $jsonArray['props']['pageProps']['skuDetail']['important_text'] ?? ''; + $importantTextArray = explode(' | ', $importantText); + $usageMileage = str_replace('万公里', '', isset($importantTextArray[1]) ? $importantTextArray[1] : ''); + + // 车辆名称 品牌、车系、车型 + $brandName = $jsonArray['props']['pageProps']['skuDetail']['car_info']['brand_name'] ?? ''; + $seriesName = $jsonArray['props']['pageProps']['skuDetail']['car_info']['series_name'] ?? ''; + $carName = $jsonArray['props']['pageProps']['skuDetail']['car_info']['car_name'] ?? ''; + + // 商家信息 + $shopInfo = $jsonArray['props']['pageProps']['skuDetail']['shop_info'] ?? []; + $formatShopInfo = [ + 'shop_id' => $shopInfo['shop_id'] ?? 0, + 'shop_name' => $shopInfo['shop_short_name'] ?? '', + 'shop_address' => $shopInfo['shop_address'] ?? '' + ]; + + // 简介参数 + $otherParams = $jsonArray['props']['pageProps']['skuDetail']['other_params'] ?? []; + // 排量 + $displacement = $this->getOtherParamValue($otherParams, '排量'); + // 变速箱 + $gearbox = $this->getOtherParamValue($otherParams, '变速箱'); + // 格式化车辆信息 + $formatCarInfo = [ + 'displacement' => $displacement, + 'gearbox' => $gearbox + ]; + + // 整理车辆信息 + $info['used_car_id'] = $this->getLastIdFromUrl($url); + $info['car_id'] = $carId; + $info['url'] = $url; + $info['title'] = $title; + $info['price'] = str_replace('万', '', $this->decodeDongchediPrice($price)); + $info['new_price'] = str_replace('万', '', $newPrice); + $info['price_difference'] = str_replace('万', '', $priceDifference); + $info['condition_desc'] = $conditionDesc; + $info['images'] = json_encode($images); + $info['car_id_url'] = $carIdUrl; + $info['used_detail'] = $jsonText; + $info['format_shop_info'] = json_encode($formatShopInfo); + $info['fuel_form'] = $fuelForm; + $info['classify_energy'] = $this->classifyEnergy($fuelForm); + $info['usage_mileage'] = $usageMileage; + $info['brand_name'] = $brandName; + $info['series_name'] = $seriesName; + $info['car_name'] = $carName; + $info['year'] = $year; + $info['format_car_info'] = json_encode($formatCarInfo); + + return $info; + } + + /** + * 获取配置信息 + */ + public function getConfigDetail(string $html = '') + { + $ql = QueryList::html($html); + + $jsonText = $ql->find('#__NEXT_DATA__')->text(); + + $raw = json_decode($jsonText, true); + $properties = $raw['props']['pageProps']['rawData']['properties'] ?? []; + $carInfo = $raw['props']['pageProps']['rawData']['car_info'][0] ?? []; + + $valuesMap = $this->buildValuesMapFromCarInfo($carInfo); + $groups = $this->buildCarSpecGroups($properties, $valuesMap); + + $data['format_car_info'] = [ + 'market_time' => $this->getFieldValue($groups, 'market_time'), // 上市时间 + 'driver_form' => $this->getFieldValue($groups, 'driver_form'), // 驱动形式 + 'seat_count' => $this->getFieldValue($groups, 'seat_count'), // 座位数 + 'engine_description' => $this->getFieldValue($groups, 'engine_description'), // 发动机 + 'engine_max_power_horsepower' => $this->getFieldValue($groups, 'engine_max_power_horsepower'), // 发动机最大功率 + 'electric_description' => $this->getFieldValue($groups, 'electric_description'), //电动机 + 'battery_capacity' => $this->getFieldValue($groups, 'battery_capacity'), //电池容量 + 'battery_type' => $this->getFieldValue($groups, 'battery_type'), // 电池类型 + 'recharge_mileage' => $this->getFieldValue($groups, 'recharge_mileage') // 续航里程 + ]; + $data['groups'] = $groups; + return $data; + } + + + public function getFuelFormByConfigInfo(array $data) + { + $fuelForm = null; + foreach ($data[0]['items'] as $item) { + if ($item['key'] === 'fuel_form') { + $fuelForm = $item['value']; + break; + } + } + return $fuelForm; + } + + /** + * 从车辆配置 JSON 数据中根据字段 key 获取对应的 value + * + * @param string|array $data JSON 字符串 或 已解析的数组 + * @param string $field 要查找的字段 key(如 'fuel_form') + * @return string|null 找到则返回 value,否则返回 null + */ + public function getFieldValue($data, string $field) + { + // 如果传入的是 JSON 字符串,先解码 + if (is_string($data)) { + $data = json_decode($data, true); + if (json_last_error() !== JSON_ERROR_NONE) { + return null; // JSON 格式错误 + } + } + + // 遍历第一层(假设结构是 [ { "items": [...] } ]) + foreach ($data as $group) { + if (isset($group['items']) && is_array($group['items'])) { + foreach ($group['items'] as $item) { + if (isset($item['key']) && $item['key'] === $field) { + return $item['value'] ?? null; + } + } + } + } + + return null; // 未找到 + } + + /** + * 从 other_params 数组中根据 name 获取对应的 value + * + * @param array $otherParams 格式为 [ ['name' => 'xxx', 'value' => 'yyy'], ... ] + * @param string $name 要查找的字段名称(如 '上牌地') + * @return string|null 找到则返回 value,否则返回 null + */ + public function getOtherParamValue(array $otherParams, string $name): ?string + { + foreach ($otherParams as $item) { + if (isset($item['name']) && $item['name'] === $name) { + return $item['value'] ?? null; + } + } + return null; + } + + /** + * 获取能源类型 + */ + public function classifyEnergy($input) + { + $ce = ['汽油', '柴油', '天然气']; + $re = ['纯电', '电动', '氢能', '氢燃料', '太阳能', '油电混动', '混动', '插电混动', '插电式', '增程式', '增程']; + + // 标准化输入:去除空格等 + $input = trim($input); + + // 1. 精确或子串匹配(优先) + foreach ($re as $keyword) { + if (strpos($input, $keyword) !== false) { + return 'RE'; // 新能源 + } + } + foreach ($ce as $keyword) { + if (strpos($input, $keyword) !== false) { + return 'CE'; // 传统能源 + } + } + + // 2. 如果没匹配上,尝试模糊匹配(相似度 > 80%) + $bestMatch = null; + $bestScore = 0; + $bestType = null; + + // 检查 RE + foreach ($re as $keyword) { + similar_text($input, $keyword, $percent); + if ($percent > $bestScore && $percent >= 80) { + $bestScore = $percent; + $bestMatch = $keyword; + $bestType = 'RE'; + } + } + + // 检查 CE + foreach ($ce as $keyword) { + similar_text($input, $keyword, $percent); + if ($percent > $bestScore && $percent >= 80) { + $bestScore = $percent; + $bestMatch = $keyword; + $bestType = 'CE'; + } + } + + return $bestType ?? 'UNKNOWN'; // 未识别 + } + + /** + * 二手信息入库 + */ + public function saveUsedcarBasicToDb(array $data = []) + { + $data['created_at'] = time(); + $data['updated_at'] = time(); + return Db::table('usedcar_crawl_results')->insertGetId($data); + } + + /** + * 更新信息入库 + */ + public function saveUsedcarConfigToDb(array $data = [], int $id = 0) + { + $info = Db::table('usedcar_crawl_results')->find($id); + + if (!$info) { + return false; + } + + $groups = $data['groups']; + $formatCarInfo = array_merge($data['format_car_info'], json_decode($info->format_car_info, true)); + + return Db::table('usedcar_crawl_results') + ->where('id', $id) + ->update([ + 'format_car_info' => json_encode($formatCarInfo, JSON_UNESCAPED_UNICODE), + 'config_detail' => json_encode($groups, JSON_UNESCAPED_UNICODE), + 'updated_at' => time() + ]); + } + + /** + * 把 car_info 扁平成一个 [key => ['value' => xxx]] 的 map + * + * @param array $carInfo rawData['car_info'][0] + * @return array + */ + public function buildValuesMapFromCarInfo(array $carInfo): array + { + $valuesMap = []; + + // 1) car_info 顶层的一些字段(official_price、series_name 等) + foreach ($carInfo as $k => $v) { + if ($k === 'info' || is_array($v)) { + continue; + } + $valuesMap[$k] = ['value' => $v]; + } + + // 2) info 里的配置项 + if (!empty($carInfo['info']) && is_array($carInfo['info'])) { + foreach ($carInfo['info'] as $k => $v) { + if (is_array($v) && array_key_exists('value', $v)) { + $valuesMap[$k] = $v; + } + } + } + + return $valuesMap; + } + + /** + * 根据 type 重新分组: + * - type=0,2 为分组(左侧分类) + * - 其它 type 都视为该分组下的字段 + * + * @param array $properties rawData['properties'] + * @param array $valuesMap 来自 buildValuesMapFromCarInfo() + * @return array + */ + public function buildCarSpecGroups(array $properties, array $valuesMap): array + { + $groups = []; + $currentKey = null; // 当前分组 key + + foreach ($properties as $item) { + $type = (int)($item['type'] ?? 1); + $key = $item['key'] ?? ''; + $text = $item['text'] ?? ''; + $subList = $item['sub_list'] ?? null; + + // ---------- 1) 分类字段:type = 0 / 2 ---------- + if (in_array($type, [0, 2], true)) { + $currentKey = $key ?: ($text ?: ('group_' . count($groups))); + + if (!isset($groups[$currentKey])) { + $groups[$currentKey] = [ + 'group_key' => $currentKey, + 'group_text' => $text, + 'group_type' => $type, + 'items' => [], + ]; + } + + continue; + } + + // ---------- 2) 普通字段:挂到最近的分类下 ---------- + if ($currentKey === null) { + // 如果一开始就没有遇到分类,给一个默认分组 + $currentKey = '_default'; + $groups[$currentKey] = [ + 'group_key' => '_default', + 'group_text' => '其他', + 'group_type' => -1, + 'items' => [], + ]; + } + + // 取 value + $value = ''; + if ($key && isset($valuesMap[$key])) { + $value = trim((string)($valuesMap[$key]['value'] ?? '')); + } + + $node = [ + 'key' => $key, + 'text' => $text, + 'value' => $value, + ]; + + // ---------- 3) sub_list 子项(有就当成 children) ---------- + if (is_array($subList) && !empty($subList)) { + $children = []; + foreach ($subList as $sub) { + $subKey = $sub['key'] ?? ''; + $subText = $sub['text'] ?? ''; + $subValue = ''; + + if ($subKey && isset($valuesMap[$subKey])) { + $subValue = trim((string)($valuesMap[$subKey]['value'] ?? '')); + } + + $children[] = [ + 'key' => $subKey, + 'text' => $subText, + 'value' => $subValue, + ]; + } + + $node['children'] = $children; + } + + $groups[$currentKey]['items'][] = $node; + } + + // 你可以按需返回关联数组或索引数组,这里转成索引数组方便前端遍历 + return array_values($groups); + } + + /** + * 转义价格 + */ + protected function decodeDongchediPrice(string $str): string + { + // 映射表:PUA 字符 => 数字 / 万 + $map = [ + "\u{E53D}" => '1', + "\u{E3F0}" => '2', + "\u{E422}" => '3', + "\u{E42C}" => '4', + "\u{E49C}" => '5', + "\u{E42B}" => '6', + "\u{E4FE}" => '7', + "\u{E548}" => '8', + "\u{E4C8}" => '9', + "\u{E453}" => '0', + "\u{E45F}" => '万', + ]; + + return strtr($str, $map); + } + + /** + * 获取URL最后ID + */ + protected function getLastIdFromUrl($url) + { + // 方法1:使用正则表达式提取最后的数字ID + if (preg_match('/\/(\d+)$/', $url, $matches)) { + return $matches[1]; + } + return false; + } + + /** + * 下载单张图片 + * + * @param string $url 图片完整 URL + * @param string $saveDir 保存目录(如 /www/usedcar_images/2025-11-20) + * @param string|null $filename 指定文件名(可选,不含路径),为空则自动生成 + * @param array $headers 额外 HTTP 头(可选) + * + * @return string|null 成功返回保存路径,失败返回 null + */ + protected function downloadImage(string $url, string $saveDir, ?string $filename = null, array $headers = []): ?string + { + // 1. 目录检查 & 创建 + if (!is_dir($saveDir)) { + if (!mkdir($saveDir, 0777, true) && !is_dir($saveDir)) { + echo "[Image] 创建目录失败:{$saveDir}\n"; + return null; + } + } + + // 2. 自动生成文件名(基于 URL md5 + 原扩展名) + if ($filename === null) { + $path = parse_url($url, PHP_URL_PATH); // /xxx/xxx.jpg + $ext = pathinfo($path, PATHINFO_EXTENSION); // jpg 或 webp 之类 + + if ($ext === '') { + $ext = 'jpg'; // 没有扩展名时兜底 + } + + $filename = md5($url) . '.' . $ext; + } + + $savePath = rtrim($saveDir, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . $filename; + + // 3. 组装 HTTP 头 + $defaultHeaders = [ + 'User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) ' + . 'AppleWebKit/537.36 (KHTML, like Gecko) ' + . 'Chrome/120.0.0.0 Safari/537.36', + 'Accept: image/avif,image/webp,image/apng,image/*,*/*;q=0.8', + 'Referer: https://www.dongchedi.com/', // 很多图床需要 Referer + ]; + + foreach ($headers as $h) { + $defaultHeaders[] = $h; + } + + // 4. 使用 cURL 下载 + $ch = curl_init($url); + curl_setopt_array($ch, [ + CURLOPT_RETURNTRANSFER => true, + CURLOPT_FOLLOWLOCATION => true, + CURLOPT_CONNECTTIMEOUT => 5, + CURLOPT_TIMEOUT => 15, + CURLOPT_SSL_VERIFYPEER => false, + CURLOPT_SSL_VERIFYHOST => false, + CURLOPT_HTTPHEADER => $defaultHeaders, + ]); + + $data = curl_exec($ch); + $errNo = curl_errno($ch); + $errMsg = curl_error($ch); + $code = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE); + curl_close($ch); + + if ($errNo !== 0) { + echo "[Image] 下载失败(cURL):{$url} | {$errNo} {$errMsg}\n"; + return null; + } + + if ($code !== 200 || $data === false || $data === '') { + echo "[Image] 下载失败(HTTP {$code}):{$url}\n"; + return null; + } + + // 5. 写入本地文件 + $bytes = file_put_contents($savePath, $data); + if ($bytes === false) { + echo "[Image] 保存失败:{$savePath}\n"; + return null; + } + + echo "[Image] 下载成功:{$url} => {$savePath}\n"; + + return $savePath; + } + + /** + * 批量下载相册 + * + * @param array $urls 图片 URL 数组 + * @param string $saveDir 保存目录 + * + * @return array [原始 URL => 保存路径或 null] + */ + protected function downloadImageAlbum(array $urls, string $saveDir): array + { + $result = []; + + foreach ($urls as $url) { + $url = trim($url); + if ($url === '') { + continue; + } + + // 如果存在 // 开头的相对协议,补全 + if (strpos($url, '//') === 0) { + $url = 'https:' . $url; + } + + $path = $this->downloadImage($url, $saveDir); + $result[$url] = $path; + } + + return $result; + } +} diff --git a/app/controller/IndexController.php b/app/controller/IndexController.php index 1e83e3d..d424030 100644 --- a/app/controller/IndexController.php +++ b/app/controller/IndexController.php @@ -159,9 +159,16 @@ class IndexController public function test(Request $request) { - $result = [ - 'desc' => 'demo' - ]; - return json(['code' => 0, 'msg' => 'ok', 'data' => $result]); + $service = new \app\common\service\UsedCarCrawlService(); + + $result = $service->iUrl('https://www.dongchedi.com/usedcar/21557932'); + + // $result = $service->doData(); + + // $result = $service->decodeDongchediPrice('.'); + + // $result = $service->classifyEnergy('插电式混合动力'); + + return json($result); } } \ No newline at end of file diff --git a/app/process/CheckWeb.php b/app/process/CheckWeb.php index 8b99d61..c052352 100644 --- a/app/process/CheckWeb.php +++ b/app/process/CheckWeb.php @@ -22,6 +22,12 @@ class CheckWeb // }); // } + // 每秒执行:队列二手车数据处理 + new Crontab('* * * * * *', function(){ + $service = new \app\common\service\UsedCarCrawlService(); + $service->doData(); + }); + new Crontab('*/50 * * * * *', function(){ $userIds = User::where('status', 1)->pluck('id')->toArray(); foreach ($userIds as $userId) { From 134505be8cb6a8ea2093dbcb06ba2e8617050fe1 Mon Sep 17 00:00:00 2001 From: mzl Date: Tue, 25 Nov 2025 10:55:56 +0800 Subject: [PATCH 084/110] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E7=88=AC=E8=99=AB?= =?UTF-8?q?=E6=9C=8D=E5=8A=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/common/service/UsedCarCrawlService.php | 117 +++++++++++++-------- app/controller/IndexController.php | 4 +- 2 files changed, 75 insertions(+), 46 deletions(-) diff --git a/app/common/service/UsedCarCrawlService.php b/app/common/service/UsedCarCrawlService.php index 587b03e..c28d3a7 100644 --- a/app/common/service/UsedCarCrawlService.php +++ b/app/common/service/UsedCarCrawlService.php @@ -46,7 +46,7 @@ class UsedCarCrawlService if (!$raw) { return false; } -echo '开始抓取~:'.date('Y-m-d H:i:s').PHP_EOL; + $html = $this->getHtml($raw['url']); switch ($raw['scene']) { @@ -54,15 +54,11 @@ echo '开始抓取~:'.date('Y-m-d H:i:s').PHP_EOL; $info = $this->getUsedDetail($html, $raw['url']); $id = $this->saveUsedcarBasicToDb($info); $this->iUrl($info['car_id_url'], 'config', $id); - - echo '一阶段 抓取成功~:'.date('Y-m-d H:i:s').PHP_EOL; break; case 'config': $data = $this->getConfigDetail($html); $this->saveUsedcarConfigToDb($data, $raw['id']); - - echo '二阶段 抓取成功~:'.date('Y-m-d H:i:s').PHP_EOL; break; } @@ -142,7 +138,9 @@ echo '开始抓取~:'.date('Y-m-d H:i:s').PHP_EOL; // 能源类型 $fuelForm = $jsonArray['props']['pageProps']['skuDetail']['car_config_overview']['power']['fuel_form'] ?? ''; - + $newFuelForm = $jsonArray['props']['pageProps']['skuDetail']['car_config_overview']['new_energy_power']['fuel_form'] ?? ''; + $resultFuelForm = $fuelForm === '' ? $newFuelForm : $fuelForm; + // 使用里程 $importantText = $jsonArray['props']['pageProps']['skuDetail']['important_text'] ?? ''; $importantTextArray = explode(' | ', $importantText); @@ -186,8 +184,8 @@ echo '开始抓取~:'.date('Y-m-d H:i:s').PHP_EOL; $info['car_id_url'] = $carIdUrl; $info['used_detail'] = $jsonText; $info['format_shop_info'] = json_encode($formatShopInfo); - $info['fuel_form'] = $fuelForm; - $info['classify_energy'] = $this->classifyEnergy($fuelForm); + $info['fuel_form'] = $resultFuelForm; + $info['classify_energy'] = $this->detectEnergyType($resultFuelForm); $info['usage_mileage'] = $usageMileage; $info['brand_name'] = $brandName; $info['series_name'] = $seriesName; @@ -291,54 +289,85 @@ echo '开始抓取~:'.date('Y-m-d H:i:s').PHP_EOL; } /** - * 获取能源类型 + * 根据给定字符串判断车辆能源类型 + * + * 返回: + * - new_energy 新能源(纯电、插电混动、增程、氢燃料等) + * - traditional 传统能源(汽油、柴油、天然气、非插电混动等) + * - unknown 无法判断 + * + * @param string $text 例如:'2022款 某某牌 2.0L 油电混合 双擎' + * @return string */ - public function classifyEnergy($input) + public function detectEnergyType(string $text): string { - $ce = ['汽油', '柴油', '天然气']; - $re = ['纯电', '电动', '氢能', '氢燃料', '太阳能', '油电混动', '混动', '插电混动', '插电式', '增程式', '增程']; - - // 标准化输入:去除空格等 - $input = trim($input); - - // 1. 精确或子串匹配(优先) - foreach ($re as $keyword) { - if (strpos($input, $keyword) !== false) { - return 'RE'; // 新能源 - } + if (function_exists('mb_internal_encoding')) { + mb_internal_encoding('UTF-8'); } - foreach ($ce as $keyword) { - if (strpos($input, $keyword) !== false) { - return 'CE'; // 传统能源 + + // 明确定义的新能源关键字(纯电 / 插电 / 增程 / 氢 / 太阳能 等) + $newEnergyKeywords = [ + // 纯电 + '纯电', '纯电动', '电动车', 'BEV', + // 插电式混合 + '插电混动', '插电式混合', '插电式', '插电', 'PHEV', + // 增程式 + '增程式', '增程', 'REEV', + // 氢能 / 燃料电池 + '氢能', '氢燃料', '燃料电池', 'FCEV', + // 其它 + '太阳能', + ]; + + // 传统能源关键字(包括燃油/燃气) + $traditionalKeywords = [ + '汽油', + '柴油', + '天然气', 'LNG', 'CNG', + ]; + + // 普通混动关键字(按你的要求,统一归为传统能源) + $hybridKeywords = [ + '油电混合', '油电混动', '油电', + '混动', // 不带“插电”的混动 + '双擎', // 丰田常见表达 + 'HEV', + ]; + + // 辅助:不区分大小写的包含判断,优先 mb_stripos + $contains = function (string $haystack, string $needle): bool { + if ($needle === '') { + return false; + } + if (function_exists('mb_stripos')) { + return mb_stripos($haystack, $needle) !== false; + } + return stripos($haystack, $needle) !== false; + }; + + // 1. 先判断“明确新能源”关键词(插电、增程、氢、纯电等) + foreach ($newEnergyKeywords as $kw) { + if ($contains($text, $kw)) { + return 'RE'; } } - // 2. 如果没匹配上,尝试模糊匹配(相似度 > 80%) - $bestMatch = null; - $bestScore = 0; - $bestType = null; - - // 检查 RE - foreach ($re as $keyword) { - similar_text($input, $keyword, $percent); - if ($percent > $bestScore && $percent >= 80) { - $bestScore = $percent; - $bestMatch = $keyword; - $bestType = 'RE'; + // 2. 再判断“普通油电混动”——按照你的业务,一律归为传统能源 + foreach ($hybridKeywords as $kw) { + if ($contains($text, $kw)) { + return 'CE'; } } - // 检查 CE - foreach ($ce as $keyword) { - similar_text($input, $keyword, $percent); - if ($percent > $bestScore && $percent >= 80) { - $bestScore = $percent; - $bestMatch = $keyword; - $bestType = 'CE'; + // 3. 再判断传统能源(纯燃油/气) + foreach ($traditionalKeywords as $kw) { + if ($contains($text, $kw)) { + return 'CE'; } } - return $bestType ?? 'UNKNOWN'; // 未识别 + // 4. 都没匹配上 + return 'UNKNOWN'; } /** diff --git a/app/controller/IndexController.php b/app/controller/IndexController.php index d424030..b27f703 100644 --- a/app/controller/IndexController.php +++ b/app/controller/IndexController.php @@ -161,13 +161,13 @@ class IndexController { $service = new \app\common\service\UsedCarCrawlService(); - $result = $service->iUrl('https://www.dongchedi.com/usedcar/21557932'); + $result = $service->iUrl('https://www.dongchedi.com/usedcar/21725410'); // $result = $service->doData(); // $result = $service->decodeDongchediPrice('.'); - // $result = $service->classifyEnergy('插电式混合动力'); + // $result = $service->detectEnergyType('油电混合'); return json($result); } From 2c295cd631a613c67f0fd012b7ad5f9953606fc0 Mon Sep 17 00:00:00 2001 From: mzl Date: Wed, 26 Nov 2025 15:51:05 +0800 Subject: [PATCH 085/110] tz --- .../UsedcarCrawlResultsController.php | 33 + .../UsedcarCrawlResultsImagesController.php | 33 + .../logic/UsedcarCrawlResultsImagesLogic.php | 17 + app/common/logic/UsedcarCrawlResultsLogic.php | 17 + app/common/model/UsedcarCrawlResults.php | 49 + .../model/UsedcarCrawlResultsImages.php | 49 + .../UsedcarCrawlResultsImagesService.php | 31 + .../service/UsedcarCrawlResultsService.php | 31 + .../utils/crawl/service/UsedCarCrawl.php | 857 ++++++++++++++++++ .../crawl/service/UsedCarCrawl_bak.php} | 17 +- .../utils/crawl/service/UsedCarCrawl_bak2.php | 798 ++++++++++++++++ .../UsedcarCrawlResultsImagesValidate.php | 29 + .../validate/UsedcarCrawlResultsValidate.php | 29 + app/controller/IndexController.php | 16 +- app/process/CheckWeb.php | 4 +- app/process/Task.php | 2 +- 16 files changed, 1995 insertions(+), 17 deletions(-) create mode 100644 app/common/controller/UsedcarCrawlResultsController.php create mode 100644 app/common/controller/UsedcarCrawlResultsImagesController.php create mode 100644 app/common/logic/UsedcarCrawlResultsImagesLogic.php create mode 100644 app/common/logic/UsedcarCrawlResultsLogic.php create mode 100644 app/common/model/UsedcarCrawlResults.php create mode 100644 app/common/model/UsedcarCrawlResultsImages.php create mode 100644 app/common/service/UsedcarCrawlResultsImagesService.php create mode 100644 app/common/service/UsedcarCrawlResultsService.php create mode 100644 app/common/utils/crawl/service/UsedCarCrawl.php rename app/common/{service/UsedCarCrawlService.php => utils/crawl/service/UsedCarCrawl_bak.php} (98%) create mode 100644 app/common/utils/crawl/service/UsedCarCrawl_bak2.php create mode 100644 app/common/validate/UsedcarCrawlResultsImagesValidate.php create mode 100644 app/common/validate/UsedcarCrawlResultsValidate.php diff --git a/app/common/controller/UsedcarCrawlResultsController.php b/app/common/controller/UsedcarCrawlResultsController.php new file mode 100644 index 0000000..bdeac97 --- /dev/null +++ b/app/common/controller/UsedcarCrawlResultsController.php @@ -0,0 +1,33 @@ +service = new UsedcarCrawlResultsService(); + } + + /** + * 列表页数据格式化 + * + * @param array $data + * @return array + */ + // protected function indexFormat($data) + // { + // if (isset($data['created_at'])) + // $data['created_at'] = date('Y-m-d H:i:s', $data['created_at']); + // if (isset($data['updated_at'])) + // $data['updated_at'] = date('Y-m-d H:i:s', $data['updated_at']); + // return $data; + // } +} \ No newline at end of file diff --git a/app/common/controller/UsedcarCrawlResultsImagesController.php b/app/common/controller/UsedcarCrawlResultsImagesController.php new file mode 100644 index 0000000..5560fda --- /dev/null +++ b/app/common/controller/UsedcarCrawlResultsImagesController.php @@ -0,0 +1,33 @@ +service = new UsedcarCrawlResultsImagesService(); + } + + /** + * 列表页数据格式化 + * + * @param array $data + * @return array + */ + // protected function indexFormat($data) + // { + // if (isset($data['created_at'])) + // $data['created_at'] = date('Y-m-d H:i:s', $data['created_at']); + // if (isset($data['updated_at'])) + // $data['updated_at'] = date('Y-m-d H:i:s', $data['updated_at']); + // return $data; + // } +} \ No newline at end of file diff --git a/app/common/logic/UsedcarCrawlResultsImagesLogic.php b/app/common/logic/UsedcarCrawlResultsImagesLogic.php new file mode 100644 index 0000000..428bc16 --- /dev/null +++ b/app/common/logic/UsedcarCrawlResultsImagesLogic.php @@ -0,0 +1,17 @@ +validator; + } +} \ No newline at end of file diff --git a/app/common/service/UsedcarCrawlResultsService.php b/app/common/service/UsedcarCrawlResultsService.php new file mode 100644 index 0000000..5aa1cdd --- /dev/null +++ b/app/common/service/UsedcarCrawlResultsService.php @@ -0,0 +1,31 @@ +validator; + } +} \ No newline at end of file diff --git a/app/common/utils/crawl/service/UsedCarCrawl.php b/app/common/utils/crawl/service/UsedCarCrawl.php new file mode 100644 index 0000000..7e99090 --- /dev/null +++ b/app/common/utils/crawl/service/UsedCarCrawl.php @@ -0,0 +1,857 @@ + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', + 'Accept-Language' => 'zh-CN,zh;q=0.9', + 'Referer' => 'https://www.dongchedi.com/', + ]; + + // Redis队列键名 + private const REDIS_QUEUE_KEY = 'usedcar:urls'; + + // 能源类型常量 + public const ENERGY_TYPE_RENEWABLE = 'RE'; + public const ENERGY_TYPE_CONVENTIONAL = 'CE'; + public const ENERGY_TYPE_UNKNOWN = 'UNKNOWN'; + + /** + * 插入队列url + * + * @param string $url URL地址 + * @param string $scene 场景:usedcar(二手信息) config(配置信息) + * @param int $id 记录ID + * @return int + */ + public function pushUrl(string $url = '', string $scene = 'usedcar', int $id = 0): int + { + $data = [ + 'scene' => $scene, + 'url' => $url, + 'id' => $id, + ]; + + return Redis::lpush( + self::REDIS_QUEUE_KEY, + json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) + ); + } + + /** + * 获取队列url + * + * @return array|null + * @throws BusinessException + */ + public function popUrl(): ?array + { + $data = Redis::rPop(self::REDIS_QUEUE_KEY); + if (!$data) { + return null; + } + + $task = json_decode($data, true); + if (json_last_error() !== JSON_ERROR_NONE || !is_array($task)) { + throw new BusinessException('任务数据解析失败:' . json_last_error_msg()); + } + + // 基本字段校验 + if (empty($task['url']) || empty($task['scene'])) { + throw new BusinessException('队列任务数据缺少必要字段:url 或 scene'); + } + + return $task; + } + + /** + * 执行抓取任务 + * + * @return bool + * @throws BusinessException + */ + public function execute(): bool + { + try { + $task = $this->popUrl(); + + if (!$task) { + // 队列为空,任务结束 + return false; + } + + $url = (string)$task['url']; + $scene = (string)$task['scene']; + $id = (int)($task['id'] ?? 0); + + $html = $this->fetchHtml($url); + if ($html === '') { + throw new BusinessException("页面内容为空: {$url}"); + } + + switch ($scene) { + case 'usedcar': + $info = $this->parseUsedCarDetail($html, $url); + $insertId = $this->saveUsedCarBasicInfo($info); + + // 下载图片 + 上传 + 入库(图片失败不影响主记录) + $imagesArray = json_decode($info['images'] ?? '[]', true); + if (json_last_error() !== JSON_ERROR_NONE) { + $imagesArray = []; + } + $saveDir = public_path('uploads/usedcar/' . $insertId); + $this->uploadAndRemove($imagesArray, $saveDir, $info['used_car_id'], $insertId); + + // 继续派发配置页任务 + if (!empty($info['car_id_url'])) { + $this->pushUrl($info['car_id_url'], 'config', $insertId); + } + break; + + case 'config': + if ($id <= 0) { + throw new BusinessException("配置任务缺少主记录ID"); + } + + $data = $this->parseConfigDetail($html); + $this->saveUsedCarConfigInfo($data, $id); + break; + + default: + throw new BusinessException("未知的任务场景: " . $scene); + } + + return true; + } catch (GuzzleException $e) { + throw new BusinessException("网络请求失败: " . $e->getMessage()); + } catch (BusinessException $e) { + // 业务异常原样抛出,方便上层知道是逻辑问题 + throw $e; + } catch (Throwable $th) { + // 兜底异常,避免 worker 直接挂掉 + throw new BusinessException( + "执行任务出错: " . $th->getMessage() + . ' | 行号: ' . $th->getLine() + . ' | 文件: ' . $th->getFile() + ); + } + } + + /** + * 获取网页HTML内容 + * + * @param string $url + * @return string + * @throws GuzzleException + */ + public function fetchHtml(string $url): string + { + $client = new Client(); + $response = $client->request('GET', $url, [ + 'headers' => self::DEFAULT_HEADERS, + 'timeout' => 10, + ]); + + return (string)$response->getBody(); + } + + /** + * 解析二手车详情页信息 + * + * @param string $html + * @param string $url + * @return array + * @throws BusinessException + */ + public function parseUsedCarDetail(string $html = '', string $url = ''): array + { + try { + $ql = QueryList::html($html); + } catch (Throwable $e) { + throw new BusinessException('解析 HTML 失败(详情页): ' . $e->getMessage()); + } + + $title = trim($ql->find('h1')->text()); + $price = trim($ql->find('.head-info_price-wrap__Y4bxi > p')->text()); + $pageText = $ql->find('body')->text(); + $jsonText = $ql->find('#__NEXT_DATA__')->text(); + + if ($jsonText === '') { + throw new BusinessException('详情页未找到 __NEXT_DATA__ JSON'); + } + + $jsonArray = $this->decodeJson($jsonText, '详情页 __NEXT_DATA__'); + + // 车辆相册 + $images = $jsonArray['props']['pageProps']['skuDetail']['head_images'] ?? []; + + // 车辆ID / 年份 + $carInfo = $jsonArray['props']['pageProps']['skuDetail']['car_info'] ?? []; + $carId = $carInfo['car_id'] ?? 0; + $year = (int)($carInfo['year'] ?? 0); + + // 车辆ID URL + $carIdUrl = $carId ? ('https://www.dongchedi.com/auto/params-carIds-' . $carId) : ''; + + // 能源类型(两个字段择一) + $overview = $jsonArray['props']['pageProps']['skuDetail']['car_config_overview'] ?? []; + $fuelForm = $overview['power']['fuel_form'] ?? ''; + $newFuelForm = $overview['new_energy_power']['fuel_form'] ?? ''; + $resultFuelForm = $fuelForm === '' ? $newFuelForm : $fuelForm; + + // 使用里程 + $importantText = $jsonArray['props']['pageProps']['skuDetail']['important_text'] ?? ''; + $importantTextArray = $importantText ? explode(' | ', $importantText) : []; + $usageMileage = ''; + if (isset($importantTextArray[1])) { + $usageMileage = str_replace('万公里', '', $importantTextArray[1]); + } + + // 车辆名称 品牌、车系、车型 + $brandName = $carInfo['brand_name'] ?? ''; + $seriesName = $carInfo['series_name'] ?? ''; + $carName = $carInfo['car_name'] ?? ''; + + // 商家信息 + $shopInfo = $jsonArray['props']['pageProps']['skuDetail']['shop_info'] ?? []; + $formatShopInfo = [ + 'shop_id' => $shopInfo['shop_id'] ?? 0, + 'shop_name' => $shopInfo['shop_short_name'] ?? '', + 'shop_address' => $shopInfo['shop_address'] ?? '', + ]; + + // 简介参数 + $otherParams = $jsonArray['props']['pageProps']['skuDetail']['other_params'] ?? []; + $displacement = $this->getParamValue($otherParams, '排量'); + $gearbox = $this->getParamValue($otherParams, '变速箱'); + + $formatCarInfo = [ + 'displacement' => $displacement, + 'gearbox' => $gearbox, + ]; + + // 提取新车指导价 / 比新车省金额 + $newPrice = $this->extractPatternValue($pageText, '/新车指导价[::]\s*([^\r\n万]*万)/u'); + $priceDifference = $this->extractPatternValue($pageText, '/比新车省[::]\s*([^\r\n万]*万)/u'); + + // 提取车况介绍 + $conditionDesc = ''; + if ($pageText && preg_match('/车况介绍\s*(.*?)\s*推荐理由/su', $pageText, $m)) { + $conditionDesc = trim($m[1]); + } + + return [ + 'used_car_id' => $this->extractIdFromUrl($url), + 'car_id' => $carId, + 'url' => $url, + 'title' => $title, + 'price' => str_replace('万', '', $this->decodePrice($price)), + 'new_price' => str_replace('万', '', $newPrice), + 'price_difference' => str_replace('万', '', $priceDifference), + 'condition_desc' => $conditionDesc, + 'images' => json_encode($images, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), + 'car_id_url' => $carIdUrl, + 'used_detail' => $jsonText, + 'format_shop_info' => json_encode($formatShopInfo, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), + 'fuel_form' => $resultFuelForm, + 'classify_energy' => $this->detectEnergyType($resultFuelForm), + 'usage_mileage' => $usageMileage, + 'brand_name' => $brandName, + 'series_name' => $seriesName, + 'car_name' => $carName, + 'year' => $year, + 'format_car_info' => json_encode($formatCarInfo, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), + ]; + } + + /** + * 解析配置详情页信息 + * + * @param string $html + * @return array + * @throws BusinessException + */ + public function parseConfigDetail(string $html = ''): array + { + try { + $ql = QueryList::html($html); + } catch (Throwable $e) { + throw new BusinessException('解析 HTML 失败(配置页): ' . $e->getMessage()); + } + + $jsonText = $ql->find('#__NEXT_DATA__')->text(); + if ($jsonText === '') { + throw new BusinessException('配置页未找到 __NEXT_DATA__ JSON'); + } + + $raw = $this->decodeJson($jsonText, '配置页 __NEXT_DATA__'); + + $properties = $raw['props']['pageProps']['rawData']['properties'] ?? []; + $carInfo = $raw['props']['pageProps']['rawData']['car_info'][0] ?? []; + + $valuesMap = $this->buildValuesMap($carInfo); + $groups = $this->buildCarSpecGroups($properties, $valuesMap); + + return [ + 'format_car_info' => [ + 'market_time' => $this->getFieldValue($groups, 'market_time'), // 上市时间 + 'driver_form' => $this->getFieldValue($groups, 'driver_form'), // 驱动形式 + 'seat_count' => $this->getFieldValue($groups, 'seat_count'), // 座位数 + 'engine_description' => $this->getFieldValue($groups, 'engine_description'), // 发动机 + 'engine_max_power_horsepower' => $this->getFieldValue($groups, 'engine_max_power_horsepower'), // 发动机最大功率 + 'electric_description' => $this->getFieldValue($groups, 'electric_description'), // 电动机 + 'battery_capacity' => $this->getFieldValue($groups, 'battery_capacity'), // 电池容量 + 'battery_type' => $this->getFieldValue($groups, 'battery_type'), // 电池类型 + 'recharge_mileage' => $this->getFieldValue($groups, 'recharge_mileage'), // 续航里程 + ], + 'groups' => $groups, + ]; + } + + /** + * 从车辆配置中获取能源类型 + * + * @param array $data JSON数据 + * @return string|null 能源类型 + */ + public function getFuelFormByConfig(array $data): ?string + { + if (empty($data[0]['items']) || !is_array($data[0]['items'])) { + return null; + } + + foreach ($data[0]['items'] as $item) { + if (isset($item['key']) && $item['key'] === 'fuel_form') { + return $item['value'] ?? null; + } + } + + return null; + } + + /** + * 从数据中根据字段key获取值 + * + * @param mixed $data 数据 + * @param string $field 字段名 + * @return string|null + */ + public function getFieldValue($data, string $field): ?string + { + if (is_string($data)) { + $data = json_decode($data, true); + if (json_last_error() !== JSON_ERROR_NONE) { + return null; + } + } + + if (!is_array($data)) { + return null; + } + + foreach ($data as $group) { + if (isset($group['items']) && is_array($group['items'])) { + foreach ($group['items'] as $item) { + if (($item['key'] ?? null) === $field) { + return $item['value'] ?? null; + } + + if (!empty($item['children']) && is_array($item['children'])) { + foreach ($item['children'] as $child) { + if (($child['key'] ?? null) === $field) { + return $child['value'] ?? null; + } + } + } + } + } + } + + return null; + } + + /** + * 从参数列表中获取指定名称的值 + */ + public function getParamValue(array $params, string $name): ?string + { + foreach ($params as $item) { + if (($item['name'] ?? null) === $name) { + return $item['value'] ?? null; + } + } + return null; + } + + /** + * 判断车辆能源类型 + */ + public function detectEnergyType(string $text): string + { + if (function_exists('mb_internal_encoding')) { + mb_internal_encoding('UTF-8'); + } + + // 明确定义的新能源关键字 + $newEnergyKeywords = [ + '纯电', '纯电动', '电动车', 'BEV', + '插电混动', '插电式混合', '插电式', '插电', 'PHEV', + '增程式', '增程', 'REEV', + '氢能', '氢燃料', '燃料电池', 'FCEV', + '太阳能', + ]; + + // 传统能源关键字 + $traditionalKeywords = [ + '汽油', + '柴油', + '天然气', 'LNG', 'CNG', + ]; + + // 普通混动关键字(归为传统能源) + $hybridKeywords = [ + '油电混合', '油电混动', '油电', + '混动', + '双擎', + 'HEV', + ]; + + $contains = function (string $haystack, string $needle): bool { + if ($needle === '') { + return false; + } + if (function_exists('mb_stripos')) { + return mb_stripos($haystack, $needle) !== false; + } + return stripos($haystack, $needle) !== false; + }; + + foreach ($newEnergyKeywords as $kw) { + if ($contains($text, $kw)) { + return self::ENERGY_TYPE_RENEWABLE; + } + } + + foreach ($hybridKeywords as $kw) { + if ($contains($text, $kw)) { + return self::ENERGY_TYPE_CONVENTIONAL; + } + } + + foreach ($traditionalKeywords as $kw) { + if ($contains($text, $kw)) { + return self::ENERGY_TYPE_CONVENTIONAL; + } + } + + return self::ENERGY_TYPE_UNKNOWN; + } + + /** + * 保存二手车基本信息到数据库 + * + * @param array $data 数据 + * @return int 插入记录ID + * @throws BusinessException + */ + public function saveUsedCarBasicInfo(array $data): int + { + try { + $time = time(); + $data['created_at'] = $data['created_at'] ?? $time; + $data['updated_at'] = $time; + + return (int)Db::table('usedcar_crawl_results')->insertGetId($data); + } catch (Throwable $e) { + throw new BusinessException('保存二手车基础信息失败:' . $e->getMessage()); + } + } + + /** + * 更新配置信息到数据库 + * + * @param array $data 配置数据 + * @param int $id 记录ID + * @return bool|int + * @throws BusinessException + */ + public function saveUsedCarConfigInfo(array $data, int $id) + { + try { + $record = Db::table('usedcar_crawl_results')->find($id); + + if (!$record) { + // 找不到记录就算了,不抛致命错误,视业务情况也可以抛异常 + return false; + } + + $groups = $data['groups'] ?? []; + $oldFormat = []; + + if (!empty($record->format_car_info)) { + $decoded = json_decode($record->format_car_info, true); + if (json_last_error() === JSON_ERROR_NONE && is_array($decoded)) { + $oldFormat = $decoded; + } + } + + $formatCarInfo = array_merge( + $oldFormat, + $data['format_car_info'] ?? [] + ); + + return Db::table('usedcar_crawl_results') + ->where('id', $id) + ->update([ + 'format_car_info' => json_encode($formatCarInfo, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), + 'config_detail' => json_encode($groups, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), + 'updated_at' => time(), + ]); + } catch (Throwable $e) { + throw new BusinessException('保存车辆配置信息失败:' . $e->getMessage()); + } + } + + /** + * 构建值映射表 + */ + public function buildValuesMap(array $carInfo): array + { + $valuesMap = []; + + // 顶层字段 + foreach ($carInfo as $k => $v) { + if ($k === 'info' || is_array($v)) { + continue; + } + $valuesMap[$k] = ['value' => $v]; + } + + // info中的配置项 + if (!empty($carInfo['info']) && is_array($carInfo['info'])) { + foreach ($carInfo['info'] as $k => $v) { + if (is_array($v) && array_key_exists('value', $v)) { + $valuesMap[$k] = $v; + } + } + } + + return $valuesMap; + } + + /** + * 构建车辆规格分组 + */ + public function buildCarSpecGroups(array $properties, array $valuesMap): array + { + $groups = []; + $currentKey = null; + + foreach ($properties as $item) { + $type = (int)($item['type'] ?? 1); + $key = $item['key'] ?? ''; + $text = $item['text'] ?? ''; + $subList = $item['sub_list'] ?? null; + + // 分类字段:type = 0 / 2 + if (in_array($type, [0, 2], true)) { + $currentKey = $key ?: ($text ?: ('group_' . count($groups))); + + if (!isset($groups[$currentKey])) { + $groups[$currentKey] = [ + 'group_key' => $currentKey, + 'group_text' => $text, + 'group_type' => $type, + 'items' => [], + ]; + } + + continue; + } + + // 普通字段:挂到最近的分类下 + if ($currentKey === null) { + $currentKey = '_default'; + $groups[$currentKey] = [ + 'group_key' => '_default', + 'group_text' => '其他', + 'group_type' => -1, + 'items' => [], + ]; + } + + $value = ''; + if ($key && isset($valuesMap[$key])) { + $value = trim((string)($valuesMap[$key]['value'] ?? '')); + } + + $node = [ + 'key' => $key, + 'text' => $text, + 'value' => $value, + ]; + + if (is_array($subList) && !empty($subList)) { + $children = []; + foreach ($subList as $sub) { + $subKey = $sub['key'] ?? ''; + $subText = $sub['text'] ?? ''; + $subValue = ''; + + if ($subKey && isset($valuesMap[$subKey])) { + $subValue = trim((string)($valuesMap[$subKey]['value'] ?? '')); + } + + $children[] = [ + 'key' => $subKey, + 'text' => $subText, + 'value' => $subValue, + ]; + } + + $node['children'] = $children; + } + + $groups[$currentKey]['items'][] = $node; + } + + return array_values($groups); + } + + /** + * 解码懂车帝价格 + */ + protected function decodePrice(string $str): string + { + $map = [ + "\u{E53D}" => '1', + "\u{E3F0}" => '2', + "\u{E422}" => '3', + "\u{E42C}" => '4', + "\u{E49C}" => '5', + "\u{E42B}" => '6', + "\u{E4FE}" => '7', + "\u{E548}" => '8', + "\u{E4C8}" => '9', + "\u{E453}" => '0', + "\u{E45F}" => '万', + ]; + + return strtr($str, $map); + } + + /** + * 从URL中提取ID + */ + protected function extractIdFromUrl(string $url) + { + if (preg_match('/\/(\d+)$/', $url, $matches)) { + return (int)$matches[1]; + } + return false; + } + + /** + * 提取符合正则表达式的值(只保留数字和小数点) + */ + protected function extractPatternValue(string $text, string $pattern): string + { + $value = ''; + if ($text && preg_match($pattern, $text, $m)) { + $value = trim(preg_replace('/[^0-9.]/', '', $m[1])); + } + return $value; + } + + /** + * 下载单张图片 + */ + public function downloadImage(string $url, string $saveDir, ?string $filename = null, array $headers = []): ?string + { + if (!is_dir($saveDir)) { + if (!@mkdir($saveDir, 0777, true) && !is_dir($saveDir)) { + // echo "[Image] 创建目录失败:{$saveDir}\n"; + return null; + } + } + + if ($filename === null) { + $path = parse_url($url, PHP_URL_PATH); + $ext = pathinfo($path ?? '', PATHINFO_EXTENSION); + + if ($ext === '') { + $ext = 'jpg'; + } + + $filename = md5($url) . '.' . $ext; + } + + $savePath = rtrim($saveDir, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . $filename; + + $defaultHeaders = [ + 'User-Agent: ' . self::DEFAULT_HEADERS['User-Agent'], + 'Accept: image/avif,image/webp,image/apng,image/*,*/*;q=0.8', + 'Referer: ' . self::DEFAULT_HEADERS['Referer'], + ]; + + foreach ($headers as $h) { + $defaultHeaders[] = $h; + } + + $ch = curl_init($url); + curl_setopt_array($ch, [ + CURLOPT_RETURNTRANSFER => true, + CURLOPT_FOLLOWLOCATION => true, + CURLOPT_CONNECTTIMEOUT => 5, + CURLOPT_TIMEOUT => 15, + CURLOPT_SSL_VERIFYPEER => false, + CURLOPT_SSL_VERIFYHOST => false, + CURLOPT_HTTPHEADER => $defaultHeaders, + ]); + + $data = curl_exec($ch); + $errNo = curl_errno($ch); + $code = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE); + curl_close($ch); + + if ($errNo !== 0 || $code !== 200 || $data === false || $data === '') { + return null; + } + + $bytes = @file_put_contents($savePath, $data); + if ($bytes === false) { + return null; + } + + return $savePath; + } + + /** + * 批量下载相册 + * + * @param array $urls 图片URL数组 + * @param string $saveDir 保存目录 + * @return array [原始URL => 保存路径或null] + */ + public function downloadImageAlbum(array $urls, string $saveDir): array + { + $result = []; + + foreach ($urls as $url) { + $url = trim((string)$url); + if ($url === '') { + continue; + } + + if (strpos($url, '//') === 0) { + $url = 'https:' . $url; + } + + try { + $path = $this->downloadImage($url, $saveDir); + } catch (Throwable $e) { + $path = null; + } + + $result[$url] = $path; + } + + return $result; + } + + /** + * 下载 -> 上传 -> 删除本地 + * + * @param array $images 原始相册URL列表 + * @param string $path 本地保存目录 + * @param int $usedCarId 懂车帝 used_car_id + * @param int $id 本地 crawl 记录id + * @return bool + */ + public function uploadAndRemove(array $images, string $path, $usedCarId, $id): bool + { + if (empty($images)) { + return false; + } + + // 下载图片 + $filePatch = $this->downloadImageAlbum($images, $path); + if (empty($filePatch)) { + return false; + } + + $uploadService = new UploadService(); + + foreach ($filePatch as $url => $filePath) { + if ($filePath === null) { + continue; + } + + try { + // 上传云端 + $param = [ + 'path' => $filePath, + 'scene' => 'usedcar_crawl', + 'category' => 'usedcar', + ]; + + $result = $uploadService->uploadLocalFile($param); + if (empty($result['file_name'])) { + // 上传失败则跳过,但不抛异常影响主流程 + continue; + } + + // 保存图片信息 + UsedcarCrawlResultsImages::create([ + 'uc_id' => $id, + 'used_car_id' => $usedCarId, + 'original_name'=> $result['original_name'] ?? '', + 'file_path' => $result['file_name'], + ]); + } catch (Throwable $e) { + // 单张图失败不影响其他 + // 可以按需写日志 + } finally { + // 删除本地文件 + if (is_file($filePath)) { + @unlink($filePath); + } + } + } + + // 删除目录 + rmdir($path); + + return true; + } + + /** + * 安全解 JSON + * + * @param string $json + * @param string $context + * @return array + * @throws BusinessException + */ + protected function decodeJson(string $json, string $context = 'JSON'): array + { + $data = json_decode($json, true); + if (json_last_error() !== JSON_ERROR_NONE || !is_array($data)) { + throw new BusinessException($context . ' 解析失败:' . json_last_error_msg()); + } + return $data; + } +} \ No newline at end of file diff --git a/app/common/service/UsedCarCrawlService.php b/app/common/utils/crawl/service/UsedCarCrawl_bak.php similarity index 98% rename from app/common/service/UsedCarCrawlService.php rename to app/common/utils/crawl/service/UsedCarCrawl_bak.php index c28d3a7..11ea8c8 100644 --- a/app/common/service/UsedCarCrawlService.php +++ b/app/common/utils/crawl/service/UsedCarCrawl_bak.php @@ -1,6 +1,6 @@ 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', + 'Accept-Language' => 'zh-CN,zh;q=0.9', + 'Referer' => 'https://www.dongchedi.com/' + ]; + + // Redis队列键名 + private const REDIS_QUEUE_KEY = 'usedcar:urls'; + + // 能源类型常量 + public const ENERGY_TYPE_RENEWABLE = 'RE'; + public const ENERGY_TYPE_CONVENTIONAL = 'CE'; + public const ENERGY_TYPE_UNKNOWN = 'UNKNOWN'; + + /** + * 插入队列url + * + * @param string $url URL地址 + * @param string $scene 场景:usedcar(二手信息) config(配置信息) + * @param int $id 记录ID + * @return int + */ + public function pushUrl(string $url = '', string $scene = 'usedcar', int $id = 0): int + { + $data = [ + 'scene' => $scene, + 'url' => $url, + 'id' => $id + ]; + + return Redis::lpush(self::REDIS_QUEUE_KEY, json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)); + } + + /** + * 获取队列url + * + * @return array|null + */ + public function popUrl(): ?array + { + $data = Redis::rPop(self::REDIS_QUEUE_KEY); + if (!$data) { + return null; + } + + return json_decode($data, true); + } + + /** + * 执行抓取任务 + * + * @return bool + * @throws BusinessException + */ + public function execute(): bool + { + try { + $task = $this->popUrl(); + + if (!$task) { + return false; + } + + $html = $this->fetchHtml($task['url']); + + switch ($task['scene']) { + case 'usedcar': + $info = $this->parseUsedCarDetail($html, $task['url']); + $id = $this->saveUsedCarBasicInfo($info); + $this->uploadAndRemove(json_decode($info['images'], true), public_path('uploads/usedcar/' . $id), $info['used_car_id'], $id); + $this->pushUrl($info['car_id_url'], 'config', $id); + break; + + case 'config': + $data = $this->parseConfigDetail($html); + $this->saveUsedCarConfigInfo($data, $task['id']); + break; + + default: + throw new BusinessException("未知的任务场景: " . $task['scene']); + } + + return true; + } catch (GuzzleException $e) { + throw new BusinessException("网络请求失败: " . $e->getMessage()); + } catch (Throwable $th) { + throw new BusinessException("执行任务出错: " . $th->getMessage() . ' | 行号: ' . $th->getLine() . ' | 文件: ' . $th->getFile()); + } + } + + /** + * 获取网页HTML内容 + * + * @param string $url + * @return string + * @throws GuzzleException + */ + public function fetchHtml(string $url): string + { + $client = new Client(); + $response = $client->request('GET', $url, [ + 'headers' => self::DEFAULT_HEADERS + ]); + return (string)$response->getBody(); + } + + /** + * 解析二手车详情页信息 + * + * @param string $html + * @param string $url + * @return array + */ + public function parseUsedCarDetail(string $html = '', string $url = ''): array + { + $ql = QueryList::html($html); + + $title = $ql->find('h1')->text(); + $price = $ql->find('.head-info_price-wrap__Y4bxi > p')->text(); + $pageText = $ql->find('body')->text(); + $jsonText = $ql->find('#__NEXT_DATA__')->text(); + + // 解析JSON数据 + $jsonArray = json_decode($jsonText, true); + if (json_last_error() !== JSON_ERROR_NONE) { + throw new BusinessException("JSON解析失败: " . json_last_error_msg()); + } + + // 车辆相册 + $images = $jsonArray['props']['pageProps']['skuDetail']['head_images'] ?? []; + + // 车辆ID + $carId = $jsonArray['props']['pageProps']['skuDetail']['car_info']['car_id'] ?? 0; + + // 车辆年份 + $year = $jsonArray['props']['pageProps']['skuDetail']['car_info']['year'] ?? 0; + + // 车辆ID URL + $carIdUrl = $carId ? ('https://www.dongchedi.com/auto/params-carIds-' . $carId) : ''; + + // 能源类型 + $fuelForm = $jsonArray['props']['pageProps']['skuDetail']['car_config_overview']['power']['fuel_form'] ?? ''; + $newFuelForm = $jsonArray['props']['pageProps']['skuDetail']['car_config_overview']['new_energy_power']['fuel_form'] ?? ''; + $resultFuelForm = $fuelForm === '' ? $newFuelForm : $fuelForm; + + // 使用里程 + $importantText = $jsonArray['props']['pageProps']['skuDetail']['important_text'] ?? ''; + $importantTextArray = explode(' | ', $importantText); + $usageMileage = str_replace('万公里', '', isset($importantTextArray[1]) ? $importantTextArray[1] : ''); + + // 车辆名称 品牌、车系、车型 + $brandName = $jsonArray['props']['pageProps']['skuDetail']['car_info']['brand_name'] ?? ''; + $seriesName = $jsonArray['props']['pageProps']['skuDetail']['car_info']['series_name'] ?? ''; + $carName = $jsonArray['props']['pageProps']['skuDetail']['car_info']['car_name'] ?? ''; + + // 商家信息 + $shopInfo = $jsonArray['props']['pageProps']['skuDetail']['shop_info'] ?? []; + $formatShopInfo = [ + 'shop_id' => $shopInfo['shop_id'] ?? 0, + 'shop_name' => $shopInfo['shop_short_name'] ?? '', + 'shop_address' => $shopInfo['shop_address'] ?? '' + ]; + + // 简介参数 + $otherParams = $jsonArray['props']['pageProps']['skuDetail']['other_params'] ?? []; + // 排量 + $displacement = $this->getParamValue($otherParams, '排量'); + // 变速箱 + $gearbox = $this->getParamValue($otherParams, '变速箱'); + // 格式化车辆信息 + $formatCarInfo = [ + 'displacement' => $displacement, + 'gearbox' => $gearbox + ]; + + // 提取新车指导价 + $newPrice = $this->extractPatternValue($pageText, '/新车指导价[::]\s*([^\r\n万]*万)/u'); + + // 提取比新车省金额 + $priceDifference = $this->extractPatternValue($pageText, '/比新车省[::]\s*([^\r\n万]*万)/u'); + + // 提取车况介绍 + $conditionDesc = ''; + if ($pageText) { + if (preg_match('/车况介绍\s*(.*?)\s*推荐理由/su', $pageText, $m)) { + $conditionDesc = trim($m[1]); + } + } + + // 整理车辆信息 + return [ + 'used_car_id' => $this->extractIdFromUrl($url), + 'car_id' => $carId, + 'url' => $url, + 'title' => $title, + 'price' => str_replace('万', '', $this->decodePrice($price)), + 'new_price' => str_replace('万', '', $newPrice), + 'price_difference' => str_replace('万', '', $priceDifference), + 'condition_desc' => $conditionDesc, + 'images' => json_encode($images, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), + 'car_id_url' => $carIdUrl, + 'used_detail' => $jsonText, + 'format_shop_info' => json_encode($formatShopInfo, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), + 'fuel_form' => $resultFuelForm, + 'classify_energy' => $this->detectEnergyType($resultFuelForm), + 'usage_mileage' => $usageMileage, + 'brand_name' => $brandName, + 'series_name' => $seriesName, + 'car_name' => $carName, + 'year' => $year, + 'format_car_info' => json_encode($formatCarInfo, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) + ]; + } + + /** + * 解析配置详情页信息 + * + * @param string $html + * @return array + */ + public function parseConfigDetail(string $html = ''): array + { + $ql = QueryList::html($html); + $jsonText = $ql->find('#__NEXT_DATA__')->text(); + + $raw = json_decode($jsonText, true); + if (json_last_error() !== JSON_ERROR_NONE) { + throw new BusinessException("配置页面JSON解析失败: " . json_last_error_msg()); + } + + $properties = $raw['props']['pageProps']['rawData']['properties'] ?? []; + $carInfo = $raw['props']['pageProps']['rawData']['car_info'][0] ?? []; + + $valuesMap = $this->buildValuesMap($carInfo); + $groups = $this->buildCarSpecGroups($properties, $valuesMap); + + return [ + 'format_car_info' => [ + 'market_time' => $this->getFieldValue($groups, 'market_time'), // 上市时间 + 'driver_form' => $this->getFieldValue($groups, 'driver_form'), // 驱动形式 + 'seat_count' => $this->getFieldValue($groups, 'seat_count'), // 座位数 + 'engine_description' => $this->getFieldValue($groups, 'engine_description'), // 发动机 + 'engine_max_power_horsepower' => $this->getFieldValue($groups, 'engine_max_power_horsepower'), // 发动机最大功率 + 'electric_description' => $this->getFieldValue($groups, 'electric_description'), //电动机 + 'battery_capacity' => $this->getFieldValue($groups, 'battery_capacity'), //电池容量 + 'battery_type' => $this->getFieldValue($groups, 'battery_type'), // 电池类型 + 'recharge_mileage' => $this->getFieldValue($groups, 'recharge_mileage') // 续航里程 + ], + 'groups' => $groups + ]; + } + + /** + * 从车辆配置中获取能源类型 + * + * @param array $data JSON数据 + * @return string|null 能源类型 + */ + public function getFuelFormByConfig(array $data): ?string + { + if (empty($data[0]['items'])) { + return null; + } + + foreach ($data[0]['items'] as $item) { + if (isset($item['key']) && $item['key'] === 'fuel_form') { + return $item['value'] ?? null; + } + } + + return null; + } + + /** + * 从数据中根据字段key获取值 + * + * @param mixed $data 数据 + * @param string $field 字段名 + * @return string|null + */ + public function getFieldValue($data, string $field): ?string + { + // 如果传入的是 JSON 字符串,先解码 + if (is_string($data)) { + $data = json_decode($data, true); + if (json_last_error() !== JSON_ERROR_NONE) { + return null; + } + } + + // 如果不是数组,直接返回null + if (!is_array($data)) { + return null; + } + + // 遍历寻找对应字段 + foreach ($data as $group) { + if (isset($group['items']) && is_array($group['items'])) { + foreach ($group['items'] as $item) { + if (isset($item['key']) && $item['key'] === $field) { + return $item['value'] ?? null; + } + + // 检查子元素 + if (isset($item['children']) && is_array($item['children'])) { + foreach ($item['children'] as $child) { + if (isset($child['key']) && $child['key'] === $field) { + return $child['value'] ?? null; + } + } + } + } + } + } + + return null; + } + + /** + * 从参数列表中获取指定名称的值 + * + * @param array $params 参数列表 + * @param string $name 名称 + * @return string|null + */ + public function getParamValue(array $params, string $name): ?string + { + foreach ($params as $item) { + if (isset($item['name']) && $item['name'] === $name) { + return $item['value'] ?? null; + } + } + return null; + } + + /** + * 判断车辆能源类型 + * + * @param string $text 描述文本 + * @return string 能源类型标识 + */ + public function detectEnergyType(string $text): string + { + if (function_exists('mb_internal_encoding')) { + mb_internal_encoding('UTF-8'); + } + + // 明确定义的新能源关键字 + $newEnergyKeywords = [ + // 纯电 + '纯电', '纯电动', '电动车', 'BEV', + // 插电式混合 + '插电混动', '插电式混合', '插电式', '插电', 'PHEV', + // 增程式 + '增程式', '增程', 'REEV', + // 氢能 / 燃料电池 + '氢能', '氢燃料', '燃料电池', 'FCEV', + // 其它 + '太阳能', + ]; + + // 传统能源关键字 + $traditionalKeywords = [ + '汽油', + '柴油', + '天然气', 'LNG', 'CNG', + ]; + + // 普通混动关键字(归为传统能源) + $hybridKeywords = [ + '油电混合', '油电混动', '油电', + '混动', // 不带"插电"的混动 + '双擎', // 丰田常见表达 + 'HEV', + ]; + + // 辅助函数:不区分大小写的包含判断 + $contains = function (string $haystack, string $needle): bool { + if ($needle === '') { + return false; + } + if (function_exists('mb_stripos')) { + return mb_stripos($haystack, $needle) !== false; + } + return stripos($haystack, $needle) !== false; + }; + + // 1. 先判断"明确新能源"关键词 + foreach ($newEnergyKeywords as $kw) { + if ($contains($text, $kw)) { + return self::ENERGY_TYPE_RENEWABLE; + } + } + + // 2. 再判断"普通油电混动" + foreach ($hybridKeywords as $kw) { + if ($contains($text, $kw)) { + return self::ENERGY_TYPE_CONVENTIONAL; + } + } + + // 3. 再判断传统能源 + foreach ($traditionalKeywords as $kw) { + if ($contains($text, $kw)) { + return self::ENERGY_TYPE_CONVENTIONAL; + } + } + + // 4. 都没匹配上 + return self::ENERGY_TYPE_UNKNOWN; + } + + /** + * 保存二手车基本信息到数据库 + * + * @param array $data 数据 + * @return int 插入记录ID + */ + public function saveUsedCarBasicInfo(array $data): int + { + $data['created_at'] = time(); + $data['updated_at'] = time(); + return Db::table('usedcar_crawl_results')->insertGetId($data); + } + + /** + * 更新配置信息到数据库 + * + * @param array $data 配置数据 + * @param int $id 记录ID + * @return bool|int + */ + public function saveUsedCarConfigInfo(array $data, int $id) + { + $record = Db::table('usedcar_crawl_results')->find($id); + + if (!$record) { + return false; + } + + $groups = $data['groups']; + $formatCarInfo = array_merge( + $data['format_car_info'], + json_decode($record->format_car_info, true) + ); + + return Db::table('usedcar_crawl_results') + ->where('id', $id) + ->update([ + 'format_car_info' => json_encode($formatCarInfo, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), + 'config_detail' => json_encode($groups, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), + 'updated_at' => time() + ]); + } + + /** + * 构建值映射表 + * + * @param array $carInfo 车辆信息 + * @return array + */ + public function buildValuesMap(array $carInfo): array + { + $valuesMap = []; + + // 处理顶层字段 + foreach ($carInfo as $k => $v) { + if ($k === 'info' || is_array($v)) { + continue; + } + $valuesMap[$k] = ['value' => $v]; + } + + // 处理info中的配置项 + if (!empty($carInfo['info']) && is_array($carInfo['info'])) { + foreach ($carInfo['info'] as $k => $v) { + if (is_array($v) && array_key_exists('value', $v)) { + $valuesMap[$k] = $v; + } + } + } + + return $valuesMap; + } + + /** + * 构建车辆规格分组 + * + * @param array $properties 属性列表 + * @param array $valuesMap 值映射表 + * @return array + */ + public function buildCarSpecGroups(array $properties, array $valuesMap): array + { + $groups = []; + $currentKey = null; + + foreach ($properties as $item) { + $type = (int)($item['type'] ?? 1); + $key = $item['key'] ?? ''; + $text = $item['text'] ?? ''; + $subList = $item['sub_list'] ?? null; + + // 分类字段:type = 0 / 2 + if (in_array($type, [0, 2], true)) { + $currentKey = $key ?: ($text ?: ('group_' . count($groups))); + + if (!isset($groups[$currentKey])) { + $groups[$currentKey] = [ + 'group_key' => $currentKey, + 'group_text' => $text, + 'group_type' => $type, + 'items' => [], + ]; + } + + continue; + } + + // 普通字段:挂到最近的分类下 + if ($currentKey === null) { + // 如果一开始就没有遇到分类,给一个默认分组 + $currentKey = '_default'; + $groups[$currentKey] = [ + 'group_key' => '_default', + 'group_text' => '其他', + 'group_type' => -1, + 'items' => [], + ]; + } + + // 取 value + $value = ''; + if ($key && isset($valuesMap[$key])) { + $value = trim((string)($valuesMap[$key]['value'] ?? '')); + } + + $node = [ + 'key' => $key, + 'text' => $text, + 'value' => $value, + ]; + + // 处理子项 + if (is_array($subList) && !empty($subList)) { + $children = []; + foreach ($subList as $sub) { + $subKey = $sub['key'] ?? ''; + $subText = $sub['text'] ?? ''; + $subValue = ''; + + if ($subKey && isset($valuesMap[$subKey])) { + $subValue = trim((string)($valuesMap[$subKey]['value'] ?? '')); + } + + $children[] = [ + 'key' => $subKey, + 'text' => $subText, + 'value' => $subValue, + ]; + } + + $node['children'] = $children; + } + + $groups[$currentKey]['items'][] = $node; + } + + // 转成索引数组方便前端遍历 + return array_values($groups); + } + + /** + * 解码懂车帝价格 + * + * @param string $str 加密字符串 + * @return string 解码后的字符串 + */ + protected function decodePrice(string $str): string + { + // 映射表:PUA 字符 => 数字 / 万 + $map = [ + "\u{E53D}" => '1', + "\u{E3F0}" => '2', + "\u{E422}" => '3', + "\u{E42C}" => '4', + "\u{E49C}" => '5', + "\u{E42B}" => '6', + "\u{E4FE}" => '7', + "\u{E548}" => '8', + "\u{E4C8}" => '9', + "\u{E453}" => '0', + "\u{E45F}" => '万', + ]; + + return strtr($str, $map); + } + + /** + * 从URL中提取ID + * + * @param string $url URL + * @return int|false + */ + protected function extractIdFromUrl(string $url) + { + if (preg_match('/\/(\d+)$/', $url, $matches)) { + return (int)$matches[1]; + } + return false; + } + + /** + * 提取符合正则表达式的值 + * + * @param string $text 文本 + * @param string $pattern 正则表达式 + * @return string + */ + protected function extractPatternValue(string $text, string $pattern): string + { + $value = ''; + if ($text) { + if (preg_match($pattern, $text, $m)) { + $value = trim(preg_replace('/[^0-9.]/', '', $m[1])); + } + } + return $value; + } + + /** + * 下载单张图片 + * + * @param string $url 图片完整URL + * @param string $saveDir 保存目录 + * @param string|null $filename 指定文件名 + * @param array $headers 额外HTTP头 + * @return string|null 成功返回保存路径,失败返回null + */ + public function downloadImage(string $url, string $saveDir, ?string $filename = null, array $headers = []): ?string + { + // 目录检查 & 创建 + if (!is_dir($saveDir)) { + if (!mkdir($saveDir, 0777, true) && !is_dir($saveDir)) { + echo "[Image] 创建目录失败:{$saveDir}\n"; + return null; + } + } + + // 自动生成文件名 + if ($filename === null) { + $path = parse_url($url, PHP_URL_PATH); + $ext = pathinfo($path, PATHINFO_EXTENSION); + + if ($ext === '') { + $ext = 'jpg'; + } + + $filename = md5($url) . '.' . $ext; + } + + $savePath = rtrim($saveDir, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . $filename; + + // 组装HTTP头 + $defaultHeaders = [ + 'User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', + 'Accept: image/avif,image/webp,image/apng,image/*,*/*;q=0.8', + 'Referer: https://www.dongchedi.com/', + ]; + + foreach ($headers as $h) { + $defaultHeaders[] = $h; + } + + // 使用cURL下载 + $ch = curl_init($url); + curl_setopt_array($ch, [ + CURLOPT_RETURNTRANSFER => true, + CURLOPT_FOLLOWLOCATION => true, + CURLOPT_CONNECTTIMEOUT => 5, + CURLOPT_TIMEOUT => 15, + CURLOPT_SSL_VERIFYPEER => false, + CURLOPT_SSL_VERIFYHOST => false, + CURLOPT_HTTPHEADER => $defaultHeaders, + ]); + + $data = curl_exec($ch); + $errNo = curl_errno($ch); + $errMsg = curl_error($ch); + $code = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE); + curl_close($ch); + + if ($errNo !== 0) { + // echo "[Image] 下载失败(cURL):{$url} | {$errNo} {$errMsg}\n"; + return null; + } + + if ($code !== 200 || $data === false || $data === '') { + // echo "[Image] 下载失败(HTTP {$code}):{$url}\n"; + return null; + } + + // 写入本地文件 + $bytes = file_put_contents($savePath, $data); + if ($bytes === false) { + // echo "[Image] 保存失败:{$savePath}\n"; + return null; + } + + // echo "[Image] 下载成功:{$url} => {$savePath}\n"; + + return $savePath; + } + + /** + * 批量下载相册 + * + * @param array $urls 图片URL数组 + * @param string $saveDir 保存目录 + * @return array [原始URL => 保存路径或null] + */ + public function downloadImageAlbum(array $urls, string $saveDir): array + { + $result = []; + + foreach ($urls as $url) { + $url = trim($url); + if ($url === '') { + continue; + } + + // 补全协议 + if (strpos($url, '//') === 0) { + $url = 'https:' . $url; + } + + $path = $this->downloadImage($url, $saveDir); + $result[$url] = $path; + } + + return $result; + } + + + public function uploadAndRemove($images, $path, $usedCarId, $id) + { + // 下载图片 + $filePatch = $this->downloadImageAlbum($images, $path); + + // 检查图片 + if (empty($filePatch)) { + return false; + } + + // 上传图片 + foreach ($filePatch as $url => $path) { + if ($path === null) { + continue; + } + + // 上传云端 + $param = [ + 'path' => $path, + 'scene' => 'usedcar_crawl', + 'category' => 'usedcar' + ]; + $up = new \app\common\service\UploadService(); + $result = $up->uploadLocalFile($param); + + // 保存图片信息 + UsedcarCrawlResultsImages::create([ + 'uc_id' => $id, + 'used_car_id' => $usedCarId, + 'original_name' => $result['original_name'], + 'file_path' => $result['file_name'], + ]); + + // 删除本地文件 + @unlink($path); + } + } +} \ No newline at end of file diff --git a/app/common/validate/UsedcarCrawlResultsImagesValidate.php b/app/common/validate/UsedcarCrawlResultsImagesValidate.php new file mode 100644 index 0000000..e6cbbd5 --- /dev/null +++ b/app/common/validate/UsedcarCrawlResultsImagesValidate.php @@ -0,0 +1,29 @@ + [ + + ], + + 'update' => [ + + ] + ]; +} \ No newline at end of file diff --git a/app/common/validate/UsedcarCrawlResultsValidate.php b/app/common/validate/UsedcarCrawlResultsValidate.php new file mode 100644 index 0000000..f0766bd --- /dev/null +++ b/app/common/validate/UsedcarCrawlResultsValidate.php @@ -0,0 +1,29 @@ + [ + + ], + + 'update' => [ + + ] + ]; +} \ No newline at end of file diff --git a/app/controller/IndexController.php b/app/controller/IndexController.php index b27f703..4b4fb43 100644 --- a/app/controller/IndexController.php +++ b/app/controller/IndexController.php @@ -74,25 +74,25 @@ class IndexController $options = [ 'controller' => [ 'module' => 'common', - 'name' => 'finance_recon_dubai_files' + 'name' => 'usedcar_crawl_results_images' ], 'model' => [ 'module' => 'common', - 'name' => 'finance_recon_dubai_files' + 'name' => 'usedcar_crawl_results_images' ], 'service' => [ 'module' => 'common', - 'name' => 'finance_recon_dubai_files' + 'name' => 'usedcar_crawl_results_images' ], 'logic' => [ 'module' => 'common', - 'name' => 'finance_recon_dubai_files' + 'name' => 'usedcar_crawl_results_images' ], 'validate' => [ 'module' => 'common', - 'name' => 'finance_recon_dubai_files' + 'name' => 'usedcar_crawl_results_images' ], - 'table' => 'finance_recon_dubai_files' + 'table' => 'usedcar_crawl_results_images' ]; $result = (new Generate($options))->doGenerate(); @@ -159,9 +159,9 @@ class IndexController public function test(Request $request) { - $service = new \app\common\service\UsedCarCrawlService(); + $service = new \app\common\utils\crawl\service\UsedCarCrawl(); - $result = $service->iUrl('https://www.dongchedi.com/usedcar/21725410'); + $result = $service->pushUrl('https://www.dongchedi.com/usedcar/21631642'); // $result = $service->doData(); diff --git a/app/process/CheckWeb.php b/app/process/CheckWeb.php index c052352..f3439ef 100644 --- a/app/process/CheckWeb.php +++ b/app/process/CheckWeb.php @@ -24,8 +24,8 @@ class CheckWeb // 每秒执行:队列二手车数据处理 new Crontab('* * * * * *', function(){ - $service = new \app\common\service\UsedCarCrawlService(); - $service->doData(); + $service = new \app\common\utils\crawl\service\UsedCarCrawl(); + $service->execute(); }); new Crontab('*/50 * * * * *', function(){ diff --git a/app/process/Task.php b/app/process/Task.php index 7d8059c..c90ff85 100644 --- a/app/process/Task.php +++ b/app/process/Task.php @@ -15,7 +15,7 @@ class Task { // 每半小时执行一次,通知待跟进客户 new Crontab('0 */30 * * * *', function(){ - echo date('Y-m-d H:i:s') . "\n"; + // echo date('Y-m-d H:i:s') . "\n"; Log::info('send follow start',[time()]); $where = [ From 00bd5ada53f3867af5eb5ab041dd95e295126b85 Mon Sep 17 00:00:00 2001 From: mzl Date: Fri, 28 Nov 2025 17:37:10 +0800 Subject: [PATCH 086/110] ts --- app/command/VehMoneyCommand.php | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 app/command/VehMoneyCommand.php diff --git a/app/command/VehMoneyCommand.php b/app/command/VehMoneyCommand.php new file mode 100644 index 0000000..0485e09 --- /dev/null +++ b/app/command/VehMoneyCommand.php @@ -0,0 +1,29 @@ + Date: Sat, 29 Nov 2025 15:44:23 +0800 Subject: [PATCH 087/110] =?UTF-8?q?=E8=AF=A2=E5=8D=95=E8=BD=AC=E5=8F=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/admin/controller/InquiryTransmitController.php | 4 ++++ config/route.php | 10 +++++----- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/app/admin/controller/InquiryTransmitController.php b/app/admin/controller/InquiryTransmitController.php index ee789c2..a41973b 100644 --- a/app/admin/controller/InquiryTransmitController.php +++ b/app/admin/controller/InquiryTransmitController.php @@ -11,6 +11,10 @@ class InquiryTransmitController extends BaseController { use CrudControllerTrait; + protected $noNeedLogin = ['form']; + + protected $noNeedHeaderData = ['form']; + protected $service; public function __construct() diff --git a/config/route.php b/config/route.php index 11631e4..82e2ced 100644 --- a/config/route.php +++ b/config/route.php @@ -39,6 +39,11 @@ Route::any('/upload', [app\common\controller\UploadController::class, 'uploadFil Route::any('/upload/image', [app\common\controller\UploadController::class, 'uploadImage']); Route::any('/upload/delete', [app\common\controller\UploadController::class, 'deleteFile']); +// 询单转发 +Route::group('/inquiry/transmit', function () { + Route::any('/form', [app\admin\controller\InquiryTransmitController::class, 'form']); +}); + // 公共 Route::group('/common', function () { Route::group('/auth', function () { @@ -194,11 +199,6 @@ Route::group('/admin', function () { Route::any('/update/sign', [app\admin\controller\UserController::class, 'updateSign']); }); - // 询单 - Route::group('/inquiry/transmit', function () { - Route::any('/form', [app\admin\controller\TransmitController::class, 'form']); - }); - // 询单 Route::group('/inquiry', function () { Route::any('/list', [app\inquiry\controller\InquiryLogController::class, 'index']); From d53f18181a9902b74ca733a2b05589dc02e5f93f Mon Sep 17 00:00:00 2001 From: mzl Date: Tue, 2 Dec 2025 14:13:48 +0800 Subject: [PATCH 088/110] ts --- .gitignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index c08e119..b395ecd 100644 --- a/.gitignore +++ b/.gitignore @@ -10,4 +10,5 @@ /webman /start.php /windows.php -/bootstrap.php \ No newline at end of file +/bootstrap.php +/composer.lock \ No newline at end of file From f1826843784b3ef45063ef8068b1d03abf1ef3e7 Mon Sep 17 00:00:00 2001 From: mzl Date: Tue, 2 Dec 2025 14:15:09 +0800 Subject: [PATCH 089/110] Merge branch 'master' into develop --- .gitignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index c08e119..92bf9b0 100644 --- a/.gitignore +++ b/.gitignore @@ -10,4 +10,5 @@ /webman /start.php /windows.php -/bootstrap.php \ No newline at end of file +/bootstrap.php +/composer.lock From 32a7752f8b4ec4104948b576084a0c97d27528f0 Mon Sep 17 00:00:00 2001 From: mzl Date: Tue, 2 Dec 2025 14:17:25 +0800 Subject: [PATCH 090/110] Merge branch 'develop' into feature/mzl_dev # Conflicts: # .gitignore # composer.lock --- .gitignore | 2 +- .../InquiryTransmitConfigController.php | 33 +++ .../controller/InquiryTransmitController.php | 280 ++++++++++++++++++ app/admin/service/BrandSeriesService.php | 72 +++-- .../logic/InquiryTransmitConfigLogic.php | 17 ++ app/common/logic/InquiryTransmitLogic.php | 17 ++ app/common/model/InquiryTransmit.php | 49 +++ app/common/model/InquiryTransmitConfig.php | 49 +++ app/common/service/CrawlService.php | 2 +- .../service/InquiryTransmitConfigService.php | 67 +++++ app/common/service/InquiryTransmitService.php | 92 ++++++ app/common/utils/email/Emails.php | 5 +- .../InquiryTransmitConfigValidate.php | 29 ++ .../validate/InquiryTransmitValidate.php | 29 ++ app/crm/controller/CustomerController.php | 6 + .../controller/CustomerController.php | 11 +- app/crmAdmin/logic/CustomerLogic.php | 3 +- app/crmAdmin/service/CustomerService.php | 2 +- composer.json | 3 +- composer.lock | 82 ++++- config/route.php | 23 ++ 21 files changed, 838 insertions(+), 35 deletions(-) create mode 100644 app/admin/controller/InquiryTransmitConfigController.php create mode 100644 app/admin/controller/InquiryTransmitController.php create mode 100644 app/common/logic/InquiryTransmitConfigLogic.php create mode 100644 app/common/logic/InquiryTransmitLogic.php create mode 100644 app/common/model/InquiryTransmit.php create mode 100644 app/common/model/InquiryTransmitConfig.php create mode 100644 app/common/service/InquiryTransmitConfigService.php create mode 100644 app/common/service/InquiryTransmitService.php create mode 100644 app/common/validate/InquiryTransmitConfigValidate.php create mode 100644 app/common/validate/InquiryTransmitValidate.php diff --git a/.gitignore b/.gitignore index b395ecd..92bf9b0 100644 --- a/.gitignore +++ b/.gitignore @@ -11,4 +11,4 @@ /start.php /windows.php /bootstrap.php -/composer.lock \ No newline at end of file +/composer.lock diff --git a/app/admin/controller/InquiryTransmitConfigController.php b/app/admin/controller/InquiryTransmitConfigController.php new file mode 100644 index 0000000..87761bf --- /dev/null +++ b/app/admin/controller/InquiryTransmitConfigController.php @@ -0,0 +1,33 @@ +service = new InquiryTransmitConfigService(); + } + + /** + * 列表页数据格式化 + * + * @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/InquiryTransmitController.php b/app/admin/controller/InquiryTransmitController.php new file mode 100644 index 0000000..f4ac093 --- /dev/null +++ b/app/admin/controller/InquiryTransmitController.php @@ -0,0 +1,280 @@ +service = new InquiryTransmitService(); + } + + /** + * 列表页数据格式化 + * + * @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 form(Request $request) + { + try { + $params = $request->all(); + $url = $request->header('referer'); + $domain = parse_url($url, PHP_URL_HOST); + $transmitConfig = InquiryTransmitConfig::where('domain', $domain)->first(); + if ($transmitConfig) { + $mailto = $transmitConfig->mailto; + $fromDomain = $domain; + } else { + $mailto = '1696136552@qq.com'; + $fromDomain = 'referer Failed to obtain'; + } + + if ($request->method() == 'GET') { + $config = [ + 'iLang' => 'en', + 'Act' => 'Inquiry', + 'post_url' => getenv('MAIN_DOMAIN').'/inquiry/transmit/form'.'?m=Data&a=save', + 'mailto' => $mailto, + 'fromDomain' => $fromDomain, + 'fromPath' => parse_url($params['fromlink'], PHP_URL_PATH), + 'm' => $params['m'], + 'a' => $params['a'], + 'tid' => $params['tid'], + 'cid' => $params['cid'], + 'version' => $params['version'], + 'fromlink' => $params['fromlink'] + ]; + + $html = match ($params['tid']) { + 'en-5-LP-pnx' => $this->formHmtl($config), + 'en-3-P-vdx' => $this->formHmtl2($config) + }; + + return $this->success(['html' => $html], 'success'); +// return json([ +// 'code' => 200, +// 'data' => [ +// 'html' => $html +// ], +// 'message' => 'success' +// ]); + } + + if ($request->method() == 'POST') { + $ip = $request->getRealIp($safe_mode=true); + $edata = [ + 'mailto' => $params['mailto'], + + 'Date' => date('Y-m-d H:i:s', time()), + 'Email' => $params['userEMail'], + 'Name' => $params['userName'], + 'Tel' => $params['userTel'], + 'Company' => '', + 'Country' => $params['uCountry'], + 'Form Url' => $params['fromlink'], + 'Message' => $params['Message'], + 'Ip' => $ip, + 'IpLocation' => ip2region($ip).'(仅供参考)', + + 'iLang' => $params['iLang'], + 'mailto' => $params['mailto'], + 'navigator_language' => $params['navigator_language'], + 'navigator_platform' => $params['navigator_platform'], + 'screen_width' => $params['screen_width'], + 'screen_height' => $params['screen_height'], + 'uCountry' => $params['uCountry'] + ]; + + $this->service->serviceStore($request, $edata, $domain); + + $this->emailHtml($edata); + + return $this->success([], 'Thank you for your message! We will contact with you soon.'); +// return json([ +// 'code' => 200, +// 'data' => [], +// 'message' => 'Thank you for your message! We will contact with you soon.' +// ]); + } + } catch (\Throwable $th) { + Log::info('Data:'.date('Y-m-d H:i:s').':'.$th->getMessage().'|'.$th->getLine().'|'.$th->getFile()); + } + } + + public function formHmtl($config) + { + // https://v1.xzgoogle.com/form.php?m=Temp&a=getTpl&tid=en-5-LP-pnx&cid=760&version=v4&fromlink=https://magicbridgeauto.com/contact-us/ + $html = '
'; + return $html; + } + + public function formHmtl2($config) + { + // https://v1.xzgoogle.com/form.php?m=Temp&a=getTpl&tid=en-3-P-vdx&cid=760&version=v4&fromlink=https://magicbridgeauto.com/news/ + $html = '
'; + return $html; + } + + + public function emailHtml($data) + { + $html = ' + + + + Lead Preview + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + +
+ Preview +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Date:'.$data['Date'].'
Email: + '.$data['Email'].' +
Name:'.$data['Name'].'
Tel:'.$data['Tel'].'
Company:
Country:'.$data['Country'].'
Form Url: + '.$data['Form Url'].' +
Message:'.$data['Message'].'
Ip:'.$data['Ip'].'
IpLocation:'.$data['IpLocation'].'
+
+
 
+
More
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
iLang:'.$data['iLang'].'
mailto: + '.$data['mailto'].' +
navigator_language:'.$data['navigator_language'].'
navigator_platform:'.$data['navigator_platform'].'
screen_width:'.$data['screen_width'].'
screen_height:'.$data['screen_height'].'
uCountry:'.$data['uCountry'].'
+
+ +
+ + '; + +// $email = new Email(); + //return $email->send($data['mailto'], 'Inquiry_Of_'.$data['Name'], $html); + return \app\common\utils\email\MailSend::send($data['mailto'], 'Inquiry_Of_'.$data['Name'], $html); + } +} \ No newline at end of file diff --git a/app/admin/service/BrandSeriesService.php b/app/admin/service/BrandSeriesService.php index 594605b..9542378 100644 --- a/app/admin/service/BrandSeriesService.php +++ b/app/admin/service/BrandSeriesService.php @@ -328,38 +328,64 @@ class BrandSeriesService // 执行抓取 $crawlService = new CrawlService(); - //懂车帝模糊搜索返回车系地址 + //懂车帝模糊搜索返回车系地址|单个车系id抓取 $href = $crawlService->modelSearch($keyword); - $seriesId = $this->extractSeriesId($href); + $seriesId = $this->extractSeriesId($href); //懂车帝车系id - if (empty($seriesId)) { +// if (empty($seriesId)) { //没有抓取到懂车帝系列id时,按关键词为品牌名抓取所有的系列名 +// $crawlSeries = $crawlService->brandSeries($keyword); +// } + //按关键词为品牌名抓取所有的系列名 + $crawlSeries = $crawlService->brandSeries($keyword); + + if (empty($seriesId) && empty($crawlSeries)) { throw new \Exception('抓取失败,未找到该系列车型'); } - //抓取品牌与系列 - $brandSeries = $crawlService->crawlBrandSeries($seriesId); - - - // 验证品牌是否存在 - $brand = Brand::where('brand_name', $brandSeries['brand'])->first(); - // 获取品牌信息 - $series = BrandSeries::where('series_name', $brandSeries['series'])->first(); - $result['brand'] = ['brand_name' => $brandSeries['brand'], 'exists' => $brand?1:0]; - if($brand){ - $result['brand']['id'] = $brand['id']; - } - $result['series'] = ['series_name' => $brandSeries['series'], 'exists' => $series?1:0]; - if($series){ - $result['series']['id'] = $series['id']; - if(empty($result['brand']['id'])){ - $result['brand']['id'] = $series['brand_id']; + $brandSerieses = []; + if(empty($seriesId) && !empty($crawlSeries) || count($crawlSeries)>1){ //关键词为品牌名时抓取所有车系的懂车帝id + $href = $crawlService->modelSearch($crawlSeries[0]); + $seriesId = $this->extractSeriesId($href); //懂车帝车系id + //抓取品牌与系列 + $brandSeries = $crawlService->crawlBrandSeries($seriesId); + foreach ($crawlSeries as $item) { +// $brandSerieses[] = $crawlService->brandSeries($keyword); + $brandSerieses[] = ['brand' => $brandSeries['brand'], 'series' => $item]; } + }elseif (!empty($seriesId) && empty($crawlSeries)){ + //抓取品牌与系列 + $brandSerieses[] = $crawlService->crawlBrandSeries($seriesId); } + $result = []; try { - //获取车型数据 - $result['model'] = $this->crawlSeriesModel($seriesId); - return $result; + foreach ($brandSerieses as $brandSeries) { + $item = []; + // 验证品牌是否存在 + $brand = Brand::where('brand_name', $brandSeries['brand'])->first(); + // 获取品牌信息 + $series = BrandSeries::where('series_name', $brandSeries['series'])->first(); + $item['brand'] = ['brand_name' => $brandSeries['brand'], 'exists' => $brand ? 1 : 0]; + if ($brand) { + $item['brand']['id'] = $brand['id']; + } + $item['series'] = ['series_name' => $brandSeries['series'], 'exists' => $series ? 1 : 0]; + if ($series) { + $item['series']['id'] = $series['id']; + if (empty($item['brand']['id'])) { + $item['brand']['id'] = $series['brand_id']; + } + } + + //获取车型数据 + $item['model'] = []; + if (count($brandSerieses)<2) { + $item['model'] = $this->crawlSeriesModel($seriesId); + } + $result[] = $item; + } + + return ['is_series' => (count($brandSerieses)<2)?1:0, 'data' => $result]; } catch (\Exception $e) { throw new \Exception($e->getMessage()); } diff --git a/app/common/logic/InquiryTransmitConfigLogic.php b/app/common/logic/InquiryTransmitConfigLogic.php new file mode 100644 index 0000000..3c6782d --- /dev/null +++ b/app/common/logic/InquiryTransmitConfigLogic.php @@ -0,0 +1,17 @@ +find('div.card-name')->texts()->map(function($item) use(&$series) { diff --git a/app/common/service/InquiryTransmitConfigService.php b/app/common/service/InquiryTransmitConfigService.php new file mode 100644 index 0000000..930124a --- /dev/null +++ b/app/common/service/InquiryTransmitConfigService.php @@ -0,0 +1,67 @@ +validator; + } + + /** + * 构造 CrudUtil 查询配置 + */ + protected function buildQueryConfig(Request $request, string $scene = 'default'): array + { + $p = $request->all(); + + $config = []; + $config['model'] = $this->modelClass; + switch ($scene) { + case 'default': + $config['where']['status'] = 1; + $config['order']['id'] = 'desc'; + break; + case 'index': + $config['where']['status'] = 1; + $config['order']['id'] = 'desc'; + break; + case 'show': + $config['where']['id'] = $p['id']; + $config['where']['status'] = 1; + break; + case 'store': + break; + case 'update': + $config['where']['id'] = $p['id']; + break; + case 'destroy': + $config['where']['id'] = $p['id']; + break; + } + + return $config; + } +} \ No newline at end of file diff --git a/app/common/service/InquiryTransmitService.php b/app/common/service/InquiryTransmitService.php new file mode 100644 index 0000000..7b0fed4 --- /dev/null +++ b/app/common/service/InquiryTransmitService.php @@ -0,0 +1,92 @@ +validator; + } + + /** 创建 */ + public function serviceStore(Request $request, array $data = [], string $domain = '') + { + $data['email'] = $data['Email']; + $data['name'] = $data['Name']; + $data['tel'] = $data['Tel']; + $data['company'] = $data['Company']; + $data['country'] = $data['Country']; + $data['form_Url'] = $data['Form Url']; + $data['message'] = $data['Message']; + $data['ip_location'] = $data['IpLocation']; + $data['ip'] = $data['Ip']; + $data['domain'] = $domain; + + return InquiryTransmit::create($data); + } + + /** + * 构造 CrudUtil 查询配置 + */ + protected function buildQueryConfig(Request $request, string $scene = 'default'): array + { + $p = $request->all(); + + $config = []; + $config['model'] = $this->modelClass; + switch ($scene) { + case 'default': + $config['order']['id'] = 'desc'; + break; + case 'index': + // 域名 + if (isset($p['domain']) && !empty($p['domain'])) { + $config['where']['domain'] = $p['domain']; + } + // 关键字 + if (isset($p['keyword']) && $p['keyword']) { + $k = '%' . $p['keyword'] . '%'; + foreach (['email', 'name', 'tel', 'country', 'message', 'form_url'] as $col) { + $config['orWhere'][] = [$col, 'like', $k]; + } + } + $config['order']['id'] = 'desc'; + break; + case 'show': + $config['where']['id'] = $p['id']; + break; + case 'store': + break; + case 'update': + $config['where']['id'] = $p['id']; + break; + case 'destroy': + $config['where']['id'] = $p['id']; + break; + } + + return $config; + } +} \ No newline at end of file diff --git a/app/common/utils/email/Emails.php b/app/common/utils/email/Emails.php index 0742274..b3caca8 100644 --- a/app/common/utils/email/Emails.php +++ b/app/common/utils/email/Emails.php @@ -434,9 +434,12 @@ class Emails $channel = 12; $channelInfo = InquiryChannel::where('id', $channel)->first(); + // 从库实时获取 + $users = \app\common\model\User::where('corp_id', 3)->where('status', 1)->orderBy('id')->pluck('id')->toArray(); + // 初始化 UID 轮询器 $uidRotator = new \app\common\utils\RedisUidRotator('email_parsed_result_20250804'); - $uidRotator->setUids([64, 63, 93, 108, 132, 145, 150, 143, 156, 157, 158], false); + $uidRotator->setUids($users, false); EmailParsedResult::where('status', 0) ->chunkById($batchSize, function ($records) use ($delaySeconds, $throttleAfter, $throttleSeconds, $enterUser, $channelInfo, $uidRotator) { diff --git a/app/common/validate/InquiryTransmitConfigValidate.php b/app/common/validate/InquiryTransmitConfigValidate.php new file mode 100644 index 0000000..f80ec8d --- /dev/null +++ b/app/common/validate/InquiryTransmitConfigValidate.php @@ -0,0 +1,29 @@ + [ + + ], + + 'update' => [ + + ] + ]; +} \ No newline at end of file diff --git a/app/common/validate/InquiryTransmitValidate.php b/app/common/validate/InquiryTransmitValidate.php new file mode 100644 index 0000000..58c120a --- /dev/null +++ b/app/common/validate/InquiryTransmitValidate.php @@ -0,0 +1,29 @@ + [ + + ], + + 'update' => [ + + ] + ]; +} \ No newline at end of file diff --git a/app/crm/controller/CustomerController.php b/app/crm/controller/CustomerController.php index 25b186e..13dad20 100644 --- a/app/crm/controller/CustomerController.php +++ b/app/crm/controller/CustomerController.php @@ -435,6 +435,11 @@ class CustomerController extends BaseController { $id = Util::getParam($request, 'id'); $scene = Util::getParam($request, 'scene',''); + $status_desc = Util::getParam($request, 'status_desc',''); + + if($scene == 'force' && empty($status_desc)){ + return $this->error( '移除原因不能为空'); + } $userId = $request->user->uid; $where = [ @@ -450,6 +455,7 @@ class CustomerController extends BaseController if ($scene == 'force') { //status=5:业务员自己删除 $customer->status = 5; + $customer->status_desc = $status_desc; $res = $customer->save(); }else{ $customer->user_id = 0; diff --git a/app/crmAdmin/controller/CustomerController.php b/app/crmAdmin/controller/CustomerController.php index 6104fcd..161c4f7 100644 --- a/app/crmAdmin/controller/CustomerController.php +++ b/app/crmAdmin/controller/CustomerController.php @@ -144,6 +144,12 @@ class CustomerController extends BaseController { try { $param = $request->all(); + $scene = $param['scene'] ?? ''; + $status_desc = $param['status_desc'] ?? ''; + + if($scene != 'force' && empty($param['status_desc'])){ + return $this->error( '移除原因不能为空'); + } if (empty($param['customer_id'])) { return $this->error('customer_id'.trans('not_null')); } @@ -153,13 +159,12 @@ class CustomerController extends BaseController if(!$query->exists()){ return $this->error( trans('fail')); } - $scene = $param['scene'] ?? ''; if ($scene === 'list') { //管理端客户列表移除 - $res = $query->update(['status' => 4, 'deleted_at' => time()]); + $res = $query->update(['status' => 4, 'status_desc' => $status_desc, 'deleted_at' => time()]); } elseif ($scene === 'force') { //强删除 $res = $query->forceDelete(); } else { //公海软删除 - $res = $query->update(['status' => 3, 'deleted_at' => time()]); + $res = $query->update(['status' => 3, 'status_desc' => $status_desc, 'deleted_at' => time()]); } return $this->success($res); diff --git a/app/crmAdmin/logic/CustomerLogic.php b/app/crmAdmin/logic/CustomerLogic.php index 13efe9c..5c4192a 100644 --- a/app/crmAdmin/logic/CustomerLogic.php +++ b/app/crmAdmin/logic/CustomerLogic.php @@ -21,12 +21,13 @@ class CustomerLogic return $res; } - public function forceDelete(int $id) + public function forceDelete(int $id ,string $status_desc = '') { $customer = Customer::withTrashed()->find($id); //强制删除 // $res = $customer->forceDelete(); $customer->status = 3; + $customer->status_desc = $status_desc; $res = $customer->save(); return $res; } diff --git a/app/crmAdmin/service/CustomerService.php b/app/crmAdmin/service/CustomerService.php index 6b5573a..ecd2a17 100644 --- a/app/crmAdmin/service/CustomerService.php +++ b/app/crmAdmin/service/CustomerService.php @@ -325,7 +325,7 @@ class CustomerService $audit->status = 3; $audit->save(); } - return $logic->forceDelete($params['id']); + return $logic->forceDelete($params['id'],$audit->remark); } else { if(!empty($audit)){ $audit->status = 2; diff --git a/composer.json b/composer.json index 7ff8ace..693315e 100644 --- a/composer.json +++ b/composer.json @@ -47,7 +47,8 @@ "phpmailer/phpmailer": "^6.10", "webman/gateway-worker": "^1.0", "mpdf/mpdf": "^8.2", - "jaeger/querylist": "^4.4" + "jaeger/querylist": "^4.4", + "zoujingli/ip2region": "3.0" }, "suggest": { "ext-event": "For better performance. " diff --git a/composer.lock b/composer.lock index 1dbd689..2bf0ca9 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": "865e38bae84e7de0c95098cc87135440", + "content-hash": "ac2fa5a625069729975d072614509e1f", "packages": [ { "name": "aliyuncs/oss-sdk-php", @@ -18,7 +18,13 @@ "type": "zip", "url": "https://api.github.com/repos/aliyun/aliyun-oss-php-sdk/zipball/483dd0b8bff5d47f0e4ffc99f6077a295c5ccbb5", "reference": "483dd0b8bff5d47f0e4ffc99f6077a295c5ccbb5", - "shasum": "" + "shasum": "", + "mirrors": [ + { + "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", + "preferred": true + } + ] }, "require": { "php": ">=5.3" @@ -7129,7 +7135,77 @@ "type": "patreon" } ], - "time": "2025-11-12T01:36:20+00:00" + "time": "2025-06-12T13:34:04+00:00" + }, + { + "name": "zoujingli/ip2region", + "version": "v3.0.0", + "source": { + "type": "git", + "url": "https://github.com/zoujingli/ip2region.git", + "reference": "fd31d7a023f28f4e2f3e63f46f1fd51f8b2672f6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/zoujingli/ip2region/zipball/fd31d7a023f28f4e2f3e63f46f1fd51f8b2672f6", + "reference": "fd31d7a023f28f4e2f3e63f46f1fd51f8b2672f6", + "shasum": "" + }, + "require": { + "php": ">=5.4" + }, + "suggest": { + "ext-opcache": "启用 OPcache 以获得更好的性能" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0.x-dev" + } + }, + "autoload": { + "files": [ + "ip2region.php" + ], + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "Anyon", + "email": "zoujingli@qq.com", + "homepage": "https://thinkadmin.top" + } + ], + "description": "Ip2Region v3.0 for PHP - 企业级 IP 地理位置查询库,支持 IPv4/IPv6,分片文件管理,智能缓存,零依赖", + "homepage": "https://github.com/zoujingli/Ip2Region", + "keywords": [ + "IP", + "IP定位", + "Ip2Region", + "geoip", + "geolocation", + "ipv4", + "ipv6", + "location", + "企业级", + "分片数据库", + "压缩", + "地理位置", + "地理位置查询", + "缓存", + "高性能" + ], + "support": { + "issues": "https://github.com/zoujingli/ip2region/issues", + "source": "https://github.com/zoujingli/ip2region/tree/v3.0.0" + }, + "time": "2025-09-15T12:27:22+00:00" } ], "packages-dev": [], diff --git a/config/route.php b/config/route.php index c49f9dc..7b092f3 100644 --- a/config/route.php +++ b/config/route.php @@ -39,6 +39,11 @@ Route::any('/upload', [app\common\controller\UploadController::class, 'uploadFil Route::any('/upload/image', [app\common\controller\UploadController::class, 'uploadImage']); Route::any('/upload/delete', [app\common\controller\UploadController::class, 'deleteFile']); +// 询单转发 +Route::group('/inquiry/transmit', function () { + Route::any('/form', [app\admin\controller\InquiryTransmitController::class, 'form']); +}); + // 公共 Route::group('/common', function () { Route::group('/auth', function () { @@ -215,6 +220,24 @@ Route::group('/admin', function () { }); }); + // 询单转发 + Route::group('/inquiry', function () { + Route::group('/transmit', function () { + Route::any('/list', [app\admin\controller\InquiryTransmitController::class, 'index']); + Route::any('/show', [app\admin\controller\InquiryTransmitController::class, 'show']); + Route::any('/store', [app\admin\controller\InquiryTransmitController::class, 'store']); + Route::any('/update', [app\admin\controller\InquiryTransmitController::class, 'update']); + Route::any('/destroy', [app\admin\controller\InquiryTransmitController::class, 'destroy']); + }); + Route::group('/config', function () { + Route::any('/list', [app\admin\controller\InquiryTransmitConfigController::class, 'index']); + Route::any('/show', [app\admin\controller\InquiryTransmitConfigController::class, 'show']); + Route::any('/store', [app\admin\controller\InquiryTransmitConfigController::class, 'store']); + Route::any('/update', [app\admin\controller\InquiryTransmitConfigController::class, 'update']); + Route::any('/destroy', [app\admin\controller\InquiryTransmitConfigController::class, 'destroy']); + }); + }); + // crm Route::group('/crm', function () { Route::group('/customer', function () { From 399eaa2b5cd8d9804475a95bf44b632e5dd78b6a Mon Sep 17 00:00:00 2001 From: mzl Date: Fri, 5 Dec 2025 09:22:53 +0000 Subject: [PATCH 091/110] =?UTF-8?q?=E7=89=A9=E5=93=81=E9=A2=86=E7=94=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/admin/controller/InvItemController.php | 33 + .../controller/InvRequisitionController.php | 34 + .../InvRequisitionAttachmentController.php | 33 + .../InvRequisitionItemController.php | 33 + ...uisitionItemReturnAttachmentController.php | 33 + .../InvRequisitionItemReturnController.php | 33 + app/common/logic/InvItemLogic.php | 17 + .../logic/InvRequisitionAttachmentLogic.php | 17 + app/common/logic/InvRequisitionItemLogic.php | 17 + ...nvRequisitionItemReturnAttachmentLogic.php | 17 + .../logic/InvRequisitionItemReturnLogic.php | 17 + app/common/logic/InvRequisitionLogic.php | 17 + app/common/model/InvItem.php | 52 ++ app/common/model/InvRequisition.php | 58 ++ app/common/model/InvRequisitionAttachment.php | 49 ++ app/common/model/InvRequisitionItem.php | 49 ++ app/common/model/InvRequisitionItemReturn.php | 49 ++ .../InvRequisitionItemReturnAttachment.php | 49 ++ app/common/service/InvItemService.php | 65 ++ .../InvRequisitionAttachmentService.php | 31 + ...RequisitionItemReturnAttachmentService.php | 31 + .../InvRequisitionItemReturnService.php | 297 +++++++++ .../service/InvRequisitionItemService.php | 31 + app/common/service/InvRequisitionService.php | 208 ++++++ app/common/validate/InvItemValidate.php | 29 + .../InvRequisitionAttachmentValidate.php | 29 + ...equisitionItemReturnAttachmentValidate.php | 29 + .../InvRequisitionItemReturnValidate.php | 29 + .../validate/InvRequisitionItemValidate.php | 29 + .../validate/InvRequisitionValidate.php | 55 ++ app/controller/IndexController.php | 12 +- .../controller/InvRequisitionController.php | 55 ++ .../InvRequisitionItemReturnController.php | 56 ++ app/process/CheckWeb.php | 10 - composer.json | 3 +- composer.lock | 624 ++++++++++-------- config/plugin/webman/log/app.php | 19 + config/plugin/webman/log/middleware.php | 21 + config/route.php | 23 + public/db_log/物品领用.sql | 174 +++++ 40 files changed, 2159 insertions(+), 308 deletions(-) create mode 100644 app/admin/controller/InvItemController.php create mode 100644 app/admin/controller/InvRequisitionController.php create mode 100644 app/common/controller/InvRequisitionAttachmentController.php create mode 100644 app/common/controller/InvRequisitionItemController.php create mode 100644 app/common/controller/InvRequisitionItemReturnAttachmentController.php create mode 100644 app/common/controller/InvRequisitionItemReturnController.php create mode 100644 app/common/logic/InvItemLogic.php create mode 100644 app/common/logic/InvRequisitionAttachmentLogic.php create mode 100644 app/common/logic/InvRequisitionItemLogic.php create mode 100644 app/common/logic/InvRequisitionItemReturnAttachmentLogic.php create mode 100644 app/common/logic/InvRequisitionItemReturnLogic.php create mode 100644 app/common/logic/InvRequisitionLogic.php create mode 100644 app/common/model/InvItem.php create mode 100644 app/common/model/InvRequisition.php create mode 100644 app/common/model/InvRequisitionAttachment.php create mode 100644 app/common/model/InvRequisitionItem.php create mode 100644 app/common/model/InvRequisitionItemReturn.php create mode 100644 app/common/model/InvRequisitionItemReturnAttachment.php create mode 100644 app/common/service/InvItemService.php create mode 100644 app/common/service/InvRequisitionAttachmentService.php create mode 100644 app/common/service/InvRequisitionItemReturnAttachmentService.php create mode 100644 app/common/service/InvRequisitionItemReturnService.php create mode 100644 app/common/service/InvRequisitionItemService.php create mode 100644 app/common/service/InvRequisitionService.php create mode 100644 app/common/validate/InvItemValidate.php create mode 100644 app/common/validate/InvRequisitionAttachmentValidate.php create mode 100644 app/common/validate/InvRequisitionItemReturnAttachmentValidate.php create mode 100644 app/common/validate/InvRequisitionItemReturnValidate.php create mode 100644 app/common/validate/InvRequisitionItemValidate.php create mode 100644 app/common/validate/InvRequisitionValidate.php create mode 100644 app/mobile/controller/InvRequisitionController.php create mode 100644 app/mobile/controller/InvRequisitionItemReturnController.php create mode 100644 config/plugin/webman/log/app.php create mode 100644 config/plugin/webman/log/middleware.php create mode 100644 public/db_log/物品领用.sql diff --git a/app/admin/controller/InvItemController.php b/app/admin/controller/InvItemController.php new file mode 100644 index 0000000..287e734 --- /dev/null +++ b/app/admin/controller/InvItemController.php @@ -0,0 +1,33 @@ +service = new InvItemService(); + } + + /** + * 列表页数据格式化 + * + * @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/InvRequisitionController.php b/app/admin/controller/InvRequisitionController.php new file mode 100644 index 0000000..79b4375 --- /dev/null +++ b/app/admin/controller/InvRequisitionController.php @@ -0,0 +1,34 @@ +service = new InvRequisitionService(); + } + + /** + * 列表页数据格式化 + * + * @param array $data + * @return array + */ + // protected function indexFormat($data) + // { + // if (isset($data['created_at'])) + // $data['created_at'] = date('Y-m-d H:i:s', $data['created_at']); + // if (isset($data['updated_at'])) + // $data['updated_at'] = date('Y-m-d H:i:s', $data['updated_at']); + // return $data; + // } +} \ No newline at end of file diff --git a/app/common/controller/InvRequisitionAttachmentController.php b/app/common/controller/InvRequisitionAttachmentController.php new file mode 100644 index 0000000..8ba33e9 --- /dev/null +++ b/app/common/controller/InvRequisitionAttachmentController.php @@ -0,0 +1,33 @@ +service = new InvRequisitionAttachmentService(); + } + + /** + * 列表页数据格式化 + * + * @param array $data + * @return array + */ + // protected function indexFormat($data) + // { + // if (isset($data['created_at'])) + // $data['created_at'] = date('Y-m-d H:i:s', $data['created_at']); + // if (isset($data['updated_at'])) + // $data['updated_at'] = date('Y-m-d H:i:s', $data['updated_at']); + // return $data; + // } +} \ No newline at end of file diff --git a/app/common/controller/InvRequisitionItemController.php b/app/common/controller/InvRequisitionItemController.php new file mode 100644 index 0000000..bf1897c --- /dev/null +++ b/app/common/controller/InvRequisitionItemController.php @@ -0,0 +1,33 @@ +service = new InvRequisitionItemService(); + } + + /** + * 列表页数据格式化 + * + * @param array $data + * @return array + */ + // protected function indexFormat($data) + // { + // if (isset($data['created_at'])) + // $data['created_at'] = date('Y-m-d H:i:s', $data['created_at']); + // if (isset($data['updated_at'])) + // $data['updated_at'] = date('Y-m-d H:i:s', $data['updated_at']); + // return $data; + // } +} \ No newline at end of file diff --git a/app/common/controller/InvRequisitionItemReturnAttachmentController.php b/app/common/controller/InvRequisitionItemReturnAttachmentController.php new file mode 100644 index 0000000..8580b28 --- /dev/null +++ b/app/common/controller/InvRequisitionItemReturnAttachmentController.php @@ -0,0 +1,33 @@ +service = new InvRequisitionItemReturnAttachmentService(); + } + + /** + * 列表页数据格式化 + * + * @param array $data + * @return array + */ + // protected function indexFormat($data) + // { + // if (isset($data['created_at'])) + // $data['created_at'] = date('Y-m-d H:i:s', $data['created_at']); + // if (isset($data['updated_at'])) + // $data['updated_at'] = date('Y-m-d H:i:s', $data['updated_at']); + // return $data; + // } +} \ No newline at end of file diff --git a/app/common/controller/InvRequisitionItemReturnController.php b/app/common/controller/InvRequisitionItemReturnController.php new file mode 100644 index 0000000..7623719 --- /dev/null +++ b/app/common/controller/InvRequisitionItemReturnController.php @@ -0,0 +1,33 @@ +service = new InvRequisitionItemReturnService(); + } + + /** + * 列表页数据格式化 + * + * @param array $data + * @return array + */ + // protected function indexFormat($data) + // { + // if (isset($data['created_at'])) + // $data['created_at'] = date('Y-m-d H:i:s', $data['created_at']); + // if (isset($data['updated_at'])) + // $data['updated_at'] = date('Y-m-d H:i:s', $data['updated_at']); + // return $data; + // } +} \ No newline at end of file diff --git a/app/common/logic/InvItemLogic.php b/app/common/logic/InvItemLogic.php new file mode 100644 index 0000000..364c5c6 --- /dev/null +++ b/app/common/logic/InvItemLogic.php @@ -0,0 +1,17 @@ +validator; + } + + /** + * 生成 8-12 位的物品编码(默认10位) + * + * @param int $length 编码长度,默认为10(范围 8-12) + * @return string 返回大写的字母数字组合编码 + */ + private function generateItemCode($length = 10) + { + // 排除易混淆字符:0, O, o, 1, I, l + $chars = '23456789ABCDEFGHJKLMNPQRSTUVWXYZ'; + $code = ''; + $maxIndex = strlen($chars) - 1; + + for ($i = 0; $i < $length; $i++) { + $code .= $chars[mt_rand(0, $maxIndex)]; + } + + return $code; + } + + + public function beforeServiceStore($data, Request $request): array + { + $data['item_code'] = $this->generateItemCode(); + $data['category_id'] = 0; + $data['spec_model'] = ''; + $data['unit'] = '件'; + $data['is_consumable'] = 0; + $data['stock_qty'] = 1; + $data['location'] = ''; + return $data; + } +} \ No newline at end of file diff --git a/app/common/service/InvRequisitionAttachmentService.php b/app/common/service/InvRequisitionAttachmentService.php new file mode 100644 index 0000000..aeec408 --- /dev/null +++ b/app/common/service/InvRequisitionAttachmentService.php @@ -0,0 +1,31 @@ +validator; + } +} \ No newline at end of file diff --git a/app/common/service/InvRequisitionItemReturnAttachmentService.php b/app/common/service/InvRequisitionItemReturnAttachmentService.php new file mode 100644 index 0000000..fd70cd1 --- /dev/null +++ b/app/common/service/InvRequisitionItemReturnAttachmentService.php @@ -0,0 +1,31 @@ +validator; + } +} \ No newline at end of file diff --git a/app/common/service/InvRequisitionItemReturnService.php b/app/common/service/InvRequisitionItemReturnService.php new file mode 100644 index 0000000..5955a77 --- /dev/null +++ b/app/common/service/InvRequisitionItemReturnService.php @@ -0,0 +1,297 @@ +validator; + } + + /** + * 将二维数组按指定 key 重组为关联数组,该 key 的值作为新数组的键, + * 其余元素组成子数组作为值。 + * + * @param array $array 原始二维数组 + * @param string $keyField 用作新键的字段名(如 'id') + * @return array 重组后的数组 + */ + private function groupArrayByKey($array, $keyField = 'id') + { + $result = []; + foreach ($array as $item) { + if (!isset($item[$keyField])) { + continue; // 跳过没有该 key 的项 + } + $key = $item[$keyField]; + // 移除用作键的字段 + unset($item[$keyField]); + $result[$key] = $item; + } + return $result; + } + + + /** + * 创建物品归还申请前置处理 + * + * @param array $data 归还申请数据 + * @param Request $request 请求对象 + * @return array 处理后的数据 + */ + public function beforeServiceStore($data, Request $request): array + { + try { + // // 模拟数据 + // $data = [ + // [ + // 'requisition_item_id' => 9, + // 'return_qty' => 1, + // 'apply_remark' => '申请备注', + // 'attachment' => [ + // [ + // 'original_name' => '附件1.jpg', + // 'file_path' => '/uploads/2023/07/01/attachment1.jpg' + // ], + // [ + // 'original_name' => '附件2.jpg', + // 'file_path' => '/uploads/2023/07/01/attachment2.jpg' + // ] + // ] + // ] + // ]; + + // 获取所有申请项ID + $reqItemIds = array_column($data, 'requisition_item_id'); + + // 参数校验 + if (empty($reqItemIds)) { + throw new BusinessException('未提供有效的申请项ID'); + } + + // 将申请项按 ID 重组为关联数组 + $newReqItems = $this->groupArrayByKey($data, 'requisition_item_id'); + + // 获取申请项明细 + $reqItemsArr = InvRequisitionItem::whereIn('id', $reqItemIds)->get()->toArray(); + + // 检查是否获取到有效数据 + if (empty($reqItemsArr)) { + throw new BusinessException('未找到对应的申请项'); + } + + // 获取用户信息 + $userService = new UserService(); + $user = $userService->getUserInfo($request->user->uid, ''); + + $arr = []; + foreach ($reqItemsArr as $reqItem) { + $reqItemId = $reqItem['id']; + + // 检查是否有对应的新申请项数据 + if (!isset($newReqItems[$reqItemId])) { + continue; + } + + $arr[] = [ + 'requisition_id' => $reqItem['requisition_id'], + 'requisition_item_id' => $reqItemId, + 'item_id' => $reqItem['item_id'], + 'apply_user_id' => $user['user']['id'], + 'apply_user_name' => $user['user']['name'], + 'return_qty' => $newReqItems[$reqItemId]['return_qty'], + 'apply_remark' => $newReqItems[$reqItemId]['apply_remark'] ?? '', + 'attachment' => $newReqItems[$reqItemId]['attachment'], + 'return_at' => time() + ]; + } + + return $arr; + } catch (BusinessException $e) { + throw $e; + } catch (\Throwable $th) { + throw new BusinessException('创建物品归还申请失败:' . $th->getMessage()); + } + } + + /** + * 处理物品申请 + * + * @param Request $request + * @param array $data + * @return array + */ + public function serviceStore(Request $request, array $data, string $scene = '') + { + try { + // 关联表增加事务处理 + return Db::transaction(function () use ($request, $data) { + $data = $this->preProcess('Store', $data, $request); + $dl = $this->getDataLimitFromRequest($request); + foreach ($data as $item) { + $created = $this->logicCreate($item, $dl); + $this->processRequisitionItemReturnAttachment($created, $item['attachment']); + } + return $this->postProcess('Store', $created); + }); + } catch (\Throwable $e) { + throw new BusinessException("创建物品申请失败:" . $e->getMessage()); + } + } + + + /** + * 更新物品归还申请前置处理 + * + * @param array $data 归还申请数据 + * @param Request $request 请求对象 + * @return array 处理后的数据 + */ + public function beforeServiceUpdate($data, Request $request): array + { + // // 模拟数据 + // $data = [ + // 'id' => 1, + // 'requisition_item_id' => 9, + // 'return_qty' => 1, + // 'apply_remark' => '申请备注22', + // 'attachment' => [ + // [ + // 'original_name' => '附件1.jpg', + // 'file_path' => '/uploads/2023/07/01/attachment1.jpg' + // ], + // [ + // 'original_name' => '附件2.jpg', + // 'file_path' => '/uploads/2023/07/01/attachment2.jpg' + // ], + // [ + // 'original_name' => '附件3.jpg', + // 'file_path' => '/uploads/2023/07/01/attachment3.jpg' + // ] + // ] + // ]; + + $data['return_at'] = time(); + return $data; + } + + /** + * 更新物品归还申请 + * + * @param Request $request + * @param array $data + * @return array + */ + public function serviceUpdate(Request $request, array $data, string $scene = '') + { + try { + // 关联表增加事务处理 + return Db::transaction(function () use ($request, $data) { + $data = $this->preProcess('Update', $data, $request); + $config = $this->buildQueryConfig($request, 'update'); + + $attachment = $data['attachment']; + unset($data['attachment']); + + $this->logicUpdateBy($config, $data); + // 获取更新后的完整记录 + $updated = $this->logicShow($config); + // 处理附件更新 + $this->processRequisitionItemReturnAttachment($updated, $attachment, true); + return $this->postProcess('Update', $updated); + }); + } catch (\Throwable $e) { + throw new BusinessException("更新物品归还申请失败:" . $e->getMessage()); + } + } + + /** + * 处理物品申请附件 + * + * @param Model $created + * @param array $items + * @param bool $isUpdate + * @return void + */ + private function processRequisitionItemReturnAttachment(Model $created, array $attachment, bool $isUpdate = false): void + { + if ($isUpdate) { + $this->clearRequisitionItemReturnAttachment($created->id); + } + + foreach ($attachment as $file) { + $attachmentData = [ + 'return_id' => $created->id, + 'file_name' => $file['original_name'], + 'file_path' => $file['file_path'] + ]; + InvRequisitionItemReturnAttachment::create($attachmentData); + } + } + + /** + * 清空物品申请附件 + * + * @param int $returnId + * @return void + */ + private function clearRequisitionItemReturnAttachment(int $returnId): void + { + InvRequisitionItemReturnAttachment::where('return_id', $returnId)->delete(); + } + + + + public function returnConfirm(Request $request, array $data, string $scene = '') + { + try { + + // 关联表增加事务处理 + return Db::transaction(function () use ($request, $data) { + $data = $this->preProcess('Update', $data, $request); + $config = $this->buildQueryConfig($request, 'update'); + + $attachment = $data['attachment']; + unset($data['attachment']); + + $this->logicUpdateBy($config, $data); + // 获取更新后的完整记录 + $updated = $this->logicShow($config); + // 处理附件更新 + $this->processRequisitionItemReturnAttachment($updated, $attachment, true); + return $this->postProcess('Update', $updated); + }); + + } catch (\Throwable $e) { + throw new BusinessException("确认归还物品失败:" . $e->getMessage()); + } + } +} \ No newline at end of file diff --git a/app/common/service/InvRequisitionItemService.php b/app/common/service/InvRequisitionItemService.php new file mode 100644 index 0000000..13206b3 --- /dev/null +++ b/app/common/service/InvRequisitionItemService.php @@ -0,0 +1,31 @@ +validator; + } +} \ No newline at end of file diff --git a/app/common/service/InvRequisitionService.php b/app/common/service/InvRequisitionService.php new file mode 100644 index 0000000..dfff880 --- /dev/null +++ b/app/common/service/InvRequisitionService.php @@ -0,0 +1,208 @@ +validator; + } + + + /** + * 创建物品申请 前置 + * + * @param array $data + * @param Request $request + * @return array + */ + public function beforeServiceStore($data, Request $request): array + { + $user = (new UserService())->getUserInfo($request->user->uid, ''); + + $data['order_no'] = $this->generateOrderNo(); + $data['applicant_id'] = $user['user']['id']; + $data['applicant_name'] = $user['user']['name']; + $data['leader_id'] = $user['user']['report_to']; + $data['status'] = InvRequisition::STATUS_LEADER; + + return $data; + } + + /** + * 处理物品申请 + * + * @param Request $request + * @param array $data + * @return array + */ + public function serviceStore(Request $request, array $data, string $scene = '') + { + try { + // 关联表增加事务处理 + return Db::transaction(function () use ($request, $data) { + $data = $this->preProcess('Store', $data, $request); + $dl = $this->getDataLimitFromRequest($request); + $created = $this->logicCreate($data, $dl); + // 处理物品申请项 + $this->processRequisitionItems($created, $data['items']); + // 处理物品申请附件 + $this->processRequisitionAttachmentItems($created, $data['attachment']); + return $this->postProcess('Store', $created); + }); + } catch (\Throwable $e) { + throw new BusinessException("创建物品申请失败:" . $e->getMessage()); + } + } + + /** + * 更新物品申请 + * + * @param Request $request + * @param array $data + * @return array + */ + public function serviceUpdate(Request $request, array $data, string $scene = '') + { + try { + // 关联表增加事务处理 + return Db::transaction(function () use ($request, $data) { + $data = $this->preProcess('Update', $data, $request); + $config = $this->buildQueryConfig($request, 'update'); + + // 获取申请项/附件 + $items = $data['items']; + $attachment = $data['attachment']; + unset($data['items']); + unset($data['attachment']); + + // 执行更新操作 + $this->logicUpdateBy($config, $data); + + // 获取更新后的完整记录 + $updated = $this->logicShow($config); + + // 重新创建新的申请项 + $this->processRequisitionItems((object)$updated, $items, true); + + // 重新创建新的附件 + $this->processRequisitionAttachmentItems((object)$updated, $attachment, true); + + return $this->postProcess('Update', $updated); + }); + } catch (\Throwable $e) { + throw new BusinessException("更新物品申请失败:" . $e->getMessage()."|".$e->getLine()."|".$e->getFile()); + } + } + + /** + * 处理物品申请项 + * + * @param Model $created + * @param array $items + * @return void + */ + private function processRequisitionItems(Model $created, array $items, bool $isUpdate = false): void + { + if ($isUpdate) { + $this->clearRequisitionItems($created->id); + } + + foreach ($items as $item) { + $itemData = [ + 'requisition_id' => $created->id, + 'item_id' => $item['item_id'], + 'item_name_snap' => $item['item_name_snap'], // 修改:使用 snap 字段 + 'spec_model_snap' => $item['spec_model_snap'], // 修改:使用 snap 字段 + 'unit_snap' => $item['unit_snap'], // 修改:使用 snap 字段 + 'is_consumable_snap' => $item['is_consumable_snap'], // 修改:使用 snap 字段 + 'request_qty' => (int) $item['request_qty'] ?? 1, // 动态获取数量,转为整型 + 'approved_qty' => (int) $item['request_qty'] ?? 1, // 初始审批数量等于申请数量 + 'remark' => $item['remark'] ?? '' + ]; + InvRequisitionItem::create($itemData); + } + } + + /** + * 处理物品申请项 + * + * @param Model $created + * @param array $items + * @return void + */ + private function processRequisitionAttachmentItems(Model $created, array $attachment, bool $isUpdate = false): void + { + if ($isUpdate) { + $this->clearRequisitionAttachmentItems($created->id); + } + + foreach ($attachment as $file) { + $attachmentData = [ + 'requisition_id' => $created->id, + 'file_name' => $file['original_name'], + 'file_path' => $file['file_path'] + ]; + InvRequisitionAttachment::create($attachmentData); + } + } + + /** + * 删除申请项 + * + * @param int $requisitionId + * @return void + */ + public function clearRequisitionItems(int $requisitionId): void + { + InvRequisitionItem::where('requisition_id', $requisitionId)->delete(); + } + + /** + * 删除申请项 + * + * @param int $requisitionId + * @return void + */ + public function clearRequisitionAttachmentItems(int $requisitionId): void + { + InvRequisitionAttachment::where('requisition_id', $requisitionId)->delete(); + } + + /** + * 生成申请单号 + * + * @return string + */ + private function generateOrderNo(): string + { + return 'RQ' . date('Ymd') . strtoupper(substr(uniqid(), -6)); + } +} \ No newline at end of file diff --git a/app/common/validate/InvItemValidate.php b/app/common/validate/InvItemValidate.php new file mode 100644 index 0000000..14ebf6d --- /dev/null +++ b/app/common/validate/InvItemValidate.php @@ -0,0 +1,29 @@ + [ + + ], + + 'update' => [ + + ] + ]; +} \ No newline at end of file diff --git a/app/common/validate/InvRequisitionAttachmentValidate.php b/app/common/validate/InvRequisitionAttachmentValidate.php new file mode 100644 index 0000000..b9f685f --- /dev/null +++ b/app/common/validate/InvRequisitionAttachmentValidate.php @@ -0,0 +1,29 @@ + [ + + ], + + 'update' => [ + + ] + ]; +} \ No newline at end of file diff --git a/app/common/validate/InvRequisitionItemReturnAttachmentValidate.php b/app/common/validate/InvRequisitionItemReturnAttachmentValidate.php new file mode 100644 index 0000000..26f3ee0 --- /dev/null +++ b/app/common/validate/InvRequisitionItemReturnAttachmentValidate.php @@ -0,0 +1,29 @@ + [ + + ], + + 'update' => [ + + ] + ]; +} \ No newline at end of file diff --git a/app/common/validate/InvRequisitionItemReturnValidate.php b/app/common/validate/InvRequisitionItemReturnValidate.php new file mode 100644 index 0000000..df63c90 --- /dev/null +++ b/app/common/validate/InvRequisitionItemReturnValidate.php @@ -0,0 +1,29 @@ + [ + + ], + + 'update' => [ + + ] + ]; +} \ No newline at end of file diff --git a/app/common/validate/InvRequisitionItemValidate.php b/app/common/validate/InvRequisitionItemValidate.php new file mode 100644 index 0000000..fed7231 --- /dev/null +++ b/app/common/validate/InvRequisitionItemValidate.php @@ -0,0 +1,29 @@ + [ + + ], + + 'update' => [ + + ] + ]; +} \ No newline at end of file diff --git a/app/common/validate/InvRequisitionValidate.php b/app/common/validate/InvRequisitionValidate.php new file mode 100644 index 0000000..584020d --- /dev/null +++ b/app/common/validate/InvRequisitionValidate.php @@ -0,0 +1,55 @@ + 'number', + 'borrow_reason' => 'require|max:255', + 'use_start_date' => 'require|date', + 'use_end_date' => 'date', + 'items' => 'require|array', + 'items.*.item_id' => 'require|number', + 'items.*.request_qty' => 'require|number|gt:0', + ]; + + // 提示 + protected $message = [ + 'applicant_id.number' => '申请人格式错误', + 'borrow_reason.require' => '领用事由不能为空', + 'borrow_reason.max' => '领用事由最多不能超过255个字符', + 'use_start_date.require' => '使用开始日期不能为空', + 'use_start_date.date' => '使用开始日期格式错误', + 'use_end_date.date' => '预计归还日期格式错误', + 'items.require' => '申请物品明细不能为空', + 'items.array' => '申请物品明细格式错误', + 'items.*.item_id.require' => '物品ID不能为空', + 'items.*.item_id.number' => '物品ID格式错误', + 'items.*.request_qty.require' => '申请数量不能为空', + 'items.*.request_qty.number' => '申请数量格式错误', + 'items.*.request_qty.gt' => '申请数量必须大于0', + ]; + + // 场景 + protected $scene = [ + 'store' => [ + + ], + + 'update' => [ + + ], + + 'apply' => [ + 'applicant_id', + 'borrow_reason', + 'use_start_date', + 'use_end_date', + 'items' + ] + ]; +} \ No newline at end of file diff --git a/app/controller/IndexController.php b/app/controller/IndexController.php index 4b4fb43..08922ab 100644 --- a/app/controller/IndexController.php +++ b/app/controller/IndexController.php @@ -74,25 +74,25 @@ class IndexController $options = [ 'controller' => [ 'module' => 'common', - 'name' => 'usedcar_crawl_results_images' + 'name' => 'inv_requisition_item_return_attachment' ], 'model' => [ 'module' => 'common', - 'name' => 'usedcar_crawl_results_images' + 'name' => 'inv_requisition_item_return_attachment' ], 'service' => [ 'module' => 'common', - 'name' => 'usedcar_crawl_results_images' + 'name' => 'inv_requisition_item_return_attachment' ], 'logic' => [ 'module' => 'common', - 'name' => 'usedcar_crawl_results_images' + 'name' => 'inv_requisition_item_return_attachment' ], 'validate' => [ 'module' => 'common', - 'name' => 'usedcar_crawl_results_images' + 'name' => 'inv_requisition_item_return_attachment' ], - 'table' => 'usedcar_crawl_results_images' + 'table' => 'inv_requisition_item_return_attachment' ]; $result = (new Generate($options))->doGenerate(); diff --git a/app/mobile/controller/InvRequisitionController.php b/app/mobile/controller/InvRequisitionController.php new file mode 100644 index 0000000..21d369e --- /dev/null +++ b/app/mobile/controller/InvRequisitionController.php @@ -0,0 +1,55 @@ +service = new InvRequisitionService(); + } + + /** + * 提交物品申请 + * + * @param Request $request + * @return mixed + */ + public function apply(Request $request) + { + try { + // 验证申请数据 + $data = $this->validate($this->service->getValidator(), $request->post(), 'apply'); + + // 处理申请逻辑 + $result = $this->service->serviceStore($request, $data, 'apply'); + + return $this->success($result, '申请提交成功'); + } catch (\Throwable $e) { + return $this->error($e->getMessage()); + } + } + + /** + * 列表页数据格式化 + * + * @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/mobile/controller/InvRequisitionItemReturnController.php b/app/mobile/controller/InvRequisitionItemReturnController.php new file mode 100644 index 0000000..4a6ebbe --- /dev/null +++ b/app/mobile/controller/InvRequisitionItemReturnController.php @@ -0,0 +1,56 @@ +service = new InvRequisitionItemReturnService(); + } + + /** + * 列表页数据格式化 + * + * @param array $data + * @return array + */ + // protected function indexFormat($data) + // { + // if (isset($data['created_at'])) + // $data['created_at'] = date('Y-m-d H:i:s', $data['created_at']); + // if (isset($data['updated_at'])) + // $data['updated_at'] = date('Y-m-d H:i:s', $data['updated_at']); + // return $data; + // } + + + /** + * 物品兑换确认 + * + * @param Request $request + * @return mixed + */ + public function returnConfirm(Request $request) + { + try { + // 验证申请数据 + $data = $this->validate($this->service->getValidator(), $request->post(), 'returnConfirm'); + + // 处理申请逻辑 + $result = $this->service->returnConfirm($request, $data, 'returnConfirm'); + + return $this->success($result, '申请提交成功'); + } catch (\Throwable $e) { + return $this->error($e->getMessage()); + } + } +} \ No newline at end of file diff --git a/app/process/CheckWeb.php b/app/process/CheckWeb.php index f3439ef..aeca44c 100644 --- a/app/process/CheckWeb.php +++ b/app/process/CheckWeb.php @@ -12,16 +12,6 @@ class CheckWeb public function onWorkerStart() { - // if (getenv('ENVIRONMENT') === 'master') { - // new Crontab('0 */10 * * * *', function(){ - // $result = $this->tocurl('https://magicbridgeauto.com'); - // if (!preg_match('/magicbridgeauto/', $result['body'])) { - // flg("【magicbridgeauto】疑似被篡改"); - // } - // return true; - // }); - // } - // 每秒执行:队列二手车数据处理 new Crontab('* * * * * *', function(){ $service = new \app\common\utils\crawl\service\UsedCarCrawl(); diff --git a/composer.json b/composer.json index a696240..d502105 100644 --- a/composer.json +++ b/composer.json @@ -48,7 +48,8 @@ "webman/gateway-worker": "^1.0", "mpdf/mpdf": "^8.2", "jaeger/querylist": "^4.4", - "zoujingli/ip2region": "^3.0" + "zoujingli/ip2region": "^3.0", + "webman/log": "^2.1" }, "suggest": { "ext-event": "For better performance. " diff --git a/composer.lock b/composer.lock index 2bf0ca9..daaebaa 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": "ac2fa5a625069729975d072614509e1f", + "content-hash": "bf987c3cd5510fc0637d6b64d460dde1", "packages": [ { "name": "aliyuncs/oss-sdk-php", @@ -18,13 +18,7 @@ "type": "zip", "url": "https://api.github.com/repos/aliyun/aliyun-oss-php-sdk/zipball/483dd0b8bff5d47f0e4ffc99f6077a295c5ccbb5", "reference": "483dd0b8bff5d47f0e4ffc99f6077a295c5ccbb5", - "shasum": "", - "mirrors": [ - { - "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", - "preferred": true - } - ] + "shasum": "" }, "require": { "php": ">=5.3" @@ -59,16 +53,16 @@ }, { "name": "brick/math", - "version": "0.14.0", + "version": "0.14.1", "source": { "type": "git", "url": "https://github.com/brick/math.git", - "reference": "113a8ee2656b882d4c3164fa31aa6e12cbb7aaa2" + "reference": "f05858549e5f9d7bb45875a75583240a38a281d0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/brick/math/zipball/113a8ee2656b882d4c3164fa31aa6e12cbb7aaa2", - "reference": "113a8ee2656b882d4c3164fa31aa6e12cbb7aaa2", + "url": "https://api.github.com/repos/brick/math/zipball/f05858549e5f9d7bb45875a75583240a38a281d0", + "reference": "f05858549e5f9d7bb45875a75583240a38a281d0", "shasum": "" }, "require": { @@ -107,7 +101,7 @@ ], "support": { "issues": "https://github.com/brick/math/issues", - "source": "https://github.com/brick/math/tree/0.14.0" + "source": "https://github.com/brick/math/tree/0.14.1" }, "funding": [ { @@ -115,7 +109,7 @@ "type": "github" } ], - "time": "2025-08-29T12:40:03+00:00" + "time": "2025-11-24T14:40:29+00:00" }, { "name": "carbonphp/carbon-doctrine-types", @@ -964,16 +958,16 @@ }, { "name": "illuminate/bus", - "version": "v12.37.0", + "version": "v12.41.1", "source": { "type": "git", "url": "https://github.com/illuminate/bus.git", - "reference": "797f768b7a52fb98443b8127cda71840cc2dbc5b" + "reference": "fdb568a20ad96461caa83b98ffc36d9e95894491" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/illuminate/bus/zipball/797f768b7a52fb98443b8127cda71840cc2dbc5b", - "reference": "797f768b7a52fb98443b8127cda71840cc2dbc5b", + "url": "https://api.github.com/repos/illuminate/bus/zipball/fdb568a20ad96461caa83b98ffc36d9e95894491", + "reference": "fdb568a20ad96461caa83b98ffc36d9e95894491", "shasum": "" }, "require": { @@ -1013,20 +1007,20 @@ "issues": "https://github.com/laravel/framework/issues", "source": "https://github.com/laravel/framework" }, - "time": "2025-10-30T12:22:29+00:00" + "time": "2025-11-30T17:14:13+00:00" }, { "name": "illuminate/collections", - "version": "v12.37.0", + "version": "v12.41.1", "source": { "type": "git", "url": "https://github.com/illuminate/collections.git", - "reference": "deb291b109b6f7fd776a3550a120771137b3c5d1" + "reference": "1cf6115f711ff775fcce547dee82d59cac33fedb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/illuminate/collections/zipball/deb291b109b6f7fd776a3550a120771137b3c5d1", - "reference": "deb291b109b6f7fd776a3550a120771137b3c5d1", + "url": "https://api.github.com/repos/illuminate/collections/zipball/1cf6115f711ff775fcce547dee82d59cac33fedb", + "reference": "1cf6115f711ff775fcce547dee82d59cac33fedb", "shasum": "" }, "require": { @@ -1072,11 +1066,11 @@ "issues": "https://github.com/laravel/framework/issues", "source": "https://github.com/laravel/framework" }, - "time": "2025-10-30T12:22:05+00:00" + "time": "2025-11-29T13:55:43+00:00" }, { "name": "illuminate/conditionable", - "version": "v12.37.0", + "version": "v12.41.1", "source": { "type": "git", "url": "https://github.com/illuminate/conditionable.git", @@ -1122,16 +1116,16 @@ }, { "name": "illuminate/container", - "version": "v12.37.0", + "version": "v12.41.1", "source": { "type": "git", "url": "https://github.com/illuminate/container.git", - "reference": "d6eaa8afd48dbe16b6b3c412a87479cad67eeb12" + "reference": "0489bc396de1537537750babec3a03ee7b753d1f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/illuminate/container/zipball/d6eaa8afd48dbe16b6b3c412a87479cad67eeb12", - "reference": "d6eaa8afd48dbe16b6b3c412a87479cad67eeb12", + "url": "https://api.github.com/repos/illuminate/container/zipball/0489bc396de1537537750babec3a03ee7b753d1f", + "reference": "0489bc396de1537537750babec3a03ee7b753d1f", "shasum": "" }, "require": { @@ -1179,20 +1173,20 @@ "issues": "https://github.com/laravel/framework/issues", "source": "https://github.com/laravel/framework" }, - "time": "2025-09-12T14:35:11+00:00" + "time": "2025-11-28T18:51:53+00:00" }, { "name": "illuminate/contracts", - "version": "v12.37.0", + "version": "v12.41.1", "source": { "type": "git", "url": "https://github.com/illuminate/contracts.git", - "reference": "5ab717c8f0dd4e84be703796bbb415ccff8de57a" + "reference": "19e8938edb73047017cfbd443b96844b86da4a59" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/illuminate/contracts/zipball/5ab717c8f0dd4e84be703796bbb415ccff8de57a", - "reference": "5ab717c8f0dd4e84be703796bbb415ccff8de57a", + "url": "https://api.github.com/repos/illuminate/contracts/zipball/19e8938edb73047017cfbd443b96844b86da4a59", + "reference": "19e8938edb73047017cfbd443b96844b86da4a59", "shasum": "" }, "require": { @@ -1227,20 +1221,20 @@ "issues": "https://github.com/laravel/framework/issues", "source": "https://github.com/laravel/framework" }, - "time": "2025-10-07T19:59:08+00:00" + "time": "2025-11-26T21:36:01+00:00" }, { "name": "illuminate/database", - "version": "v12.37.0", + "version": "v12.41.1", "source": { "type": "git", "url": "https://github.com/illuminate/database.git", - "reference": "e42bec6ad999bcc78ce9f3789f5ef937ee2e5982" + "reference": "199e9200a08c1841a31932e0a36a4094de84b72d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/illuminate/database/zipball/e42bec6ad999bcc78ce9f3789f5ef937ee2e5982", - "reference": "e42bec6ad999bcc78ce9f3789f5ef937ee2e5982", + "url": "https://api.github.com/repos/illuminate/database/zipball/199e9200a08c1841a31932e0a36a4094de84b72d", + "reference": "199e9200a08c1841a31932e0a36a4094de84b72d", "shasum": "" }, "require": { @@ -1298,20 +1292,20 @@ "issues": "https://github.com/laravel/framework/issues", "source": "https://github.com/laravel/framework" }, - "time": "2025-11-03T14:22:34+00:00" + "time": "2025-12-01T15:01:30+00:00" }, { "name": "illuminate/events", - "version": "v12.37.0", + "version": "v12.41.1", "source": { "type": "git", "url": "https://github.com/illuminate/events.git", - "reference": "e0de667c68040d59a6ffc09e914536a1186870f0" + "reference": "5fbf9a127cb649699071c2fe98ac1d39e4991da3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/illuminate/events/zipball/e0de667c68040d59a6ffc09e914536a1186870f0", - "reference": "e0de667c68040d59a6ffc09e914536a1186870f0", + "url": "https://api.github.com/repos/illuminate/events/zipball/5fbf9a127cb649699071c2fe98ac1d39e4991da3", + "reference": "5fbf9a127cb649699071c2fe98ac1d39e4991da3", "shasum": "" }, "require": { @@ -1353,11 +1347,11 @@ "issues": "https://github.com/laravel/framework/issues", "source": "https://github.com/laravel/framework" }, - "time": "2025-10-21T15:10:34+00:00" + "time": "2025-12-01T15:02:02+00:00" }, { "name": "illuminate/filesystem", - "version": "v12.37.0", + "version": "v12.41.1", "source": { "type": "git", "url": "https://github.com/illuminate/filesystem.git", @@ -1424,16 +1418,16 @@ }, { "name": "illuminate/http", - "version": "v12.37.0", + "version": "v12.41.1", "source": { "type": "git", "url": "https://github.com/illuminate/http.git", - "reference": "5c864f562b2fded08622ee19cd53260aef3db9a0" + "reference": "cdabd462fa50ab057da4807b980c861326c5a345" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/illuminate/http/zipball/5c864f562b2fded08622ee19cd53260aef3db9a0", - "reference": "5c864f562b2fded08622ee19cd53260aef3db9a0", + "url": "https://api.github.com/repos/illuminate/http/zipball/cdabd462fa50ab057da4807b980c861326c5a345", + "reference": "cdabd462fa50ab057da4807b980c861326c5a345", "shasum": "" }, "require": { @@ -1482,11 +1476,11 @@ "issues": "https://github.com/laravel/framework/issues", "source": "https://github.com/laravel/framework" }, - "time": "2025-10-28T14:38:15+00:00" + "time": "2025-11-28T18:00:52+00:00" }, { "name": "illuminate/macroable", - "version": "v12.37.0", + "version": "v12.41.1", "source": { "type": "git", "url": "https://github.com/illuminate/macroable.git", @@ -1532,16 +1526,16 @@ }, { "name": "illuminate/pagination", - "version": "v12.37.0", + "version": "v12.41.1", "source": { "type": "git", "url": "https://github.com/illuminate/pagination.git", - "reference": "1d95e70671177108b202e6ceb61bc7bc9924bdbf" + "reference": "87e7e3e7b02d6809b1bcd41782e1ca2c6d2a413b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/illuminate/pagination/zipball/1d95e70671177108b202e6ceb61bc7bc9924bdbf", - "reference": "1d95e70671177108b202e6ceb61bc7bc9924bdbf", + "url": "https://api.github.com/repos/illuminate/pagination/zipball/87e7e3e7b02d6809b1bcd41782e1ca2c6d2a413b", + "reference": "87e7e3e7b02d6809b1bcd41782e1ca2c6d2a413b", "shasum": "" }, "require": { @@ -1578,11 +1572,11 @@ "issues": "https://github.com/laravel/framework/issues", "source": "https://github.com/laravel/framework" }, - "time": "2025-09-02T15:31:06+00:00" + "time": "2025-11-16T14:36:17+00:00" }, { "name": "illuminate/pipeline", - "version": "v12.37.0", + "version": "v12.41.1", "source": { "type": "git", "url": "https://github.com/illuminate/pipeline.git", @@ -1634,16 +1628,16 @@ }, { "name": "illuminate/redis", - "version": "v12.37.0", + "version": "v12.41.1", "source": { "type": "git", "url": "https://github.com/illuminate/redis.git", - "reference": "8a397f00036974404d5d4d94eacfe8050af8e7f2" + "reference": "8dc6682638100db9feceaac3db206de752dd0f9a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/illuminate/redis/zipball/8a397f00036974404d5d4d94eacfe8050af8e7f2", - "reference": "8a397f00036974404d5d4d94eacfe8050af8e7f2", + "url": "https://api.github.com/repos/illuminate/redis/zipball/8dc6682638100db9feceaac3db206de752dd0f9a", + "reference": "8dc6682638100db9feceaac3db206de752dd0f9a", "shasum": "" }, "require": { @@ -1684,11 +1678,11 @@ "issues": "https://github.com/laravel/framework/issues", "source": "https://github.com/laravel/framework" }, - "time": "2025-10-26T17:11:19+00:00" + "time": "2025-11-20T20:50:40+00:00" }, { "name": "illuminate/session", - "version": "v12.37.0", + "version": "v12.41.1", "source": { "type": "git", "url": "https://github.com/illuminate/session.git", @@ -1745,16 +1739,16 @@ }, { "name": "illuminate/support", - "version": "v12.37.0", + "version": "v12.41.1", "source": { "type": "git", "url": "https://github.com/illuminate/support.git", - "reference": "480df979c8ffe700d6adb6e6e64abe04b32e02e3" + "reference": "c65715d8a41863ee3f2e22dc796b75c62143fca2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/illuminate/support/zipball/480df979c8ffe700d6adb6e6e64abe04b32e02e3", - "reference": "480df979c8ffe700d6adb6e6e64abe04b32e02e3", + "url": "https://api.github.com/repos/illuminate/support/zipball/c65715d8a41863ee3f2e22dc796b75c62143fca2", + "reference": "c65715d8a41863ee3f2e22dc796b75c62143fca2", "shasum": "" }, "require": { @@ -1820,7 +1814,7 @@ "issues": "https://github.com/laravel/framework/issues", "source": "https://github.com/laravel/framework" }, - "time": "2025-10-31T16:20:10+00:00" + "time": "2025-12-03T01:01:36+00:00" }, { "name": "jaeger/g-http", @@ -1979,16 +1973,16 @@ }, { "name": "laravel/serializable-closure", - "version": "v2.0.6", + "version": "v2.0.7", "source": { "type": "git", "url": "https://github.com/laravel/serializable-closure.git", - "reference": "038ce42edee619599a1debb7e81d7b3759492819" + "reference": "cb291e4c998ac50637c7eeb58189c14f5de5b9dd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/serializable-closure/zipball/038ce42edee619599a1debb7e81d7b3759492819", - "reference": "038ce42edee619599a1debb7e81d7b3759492819", + "url": "https://api.github.com/repos/laravel/serializable-closure/zipball/cb291e4c998ac50637c7eeb58189c14f5de5b9dd", + "reference": "cb291e4c998ac50637c7eeb58189c14f5de5b9dd", "shasum": "" }, "require": { @@ -1997,7 +1991,7 @@ "require-dev": { "illuminate/support": "^10.0|^11.0|^12.0", "nesbot/carbon": "^2.67|^3.0", - "pestphp/pest": "^2.36|^3.0", + "pestphp/pest": "^2.36|^3.0|^4.0", "phpstan/phpstan": "^2.0", "symfony/var-dumper": "^6.2.0|^7.0.0" }, @@ -2036,7 +2030,7 @@ "issues": "https://github.com/laravel/serializable-closure/issues", "source": "https://github.com/laravel/serializable-closure" }, - "time": "2025-10-09T13:42:30+00:00" + "time": "2025-11-21T20:52:36+00:00" }, { "name": "league/flysystem", @@ -2515,16 +2509,16 @@ }, { "name": "mpdf/mpdf", - "version": "v8.2.6", + "version": "v8.2.7", "source": { "type": "git", "url": "https://github.com/mpdf/mpdf.git", - "reference": "dd30e3b01061cf8dfe65e7041ab4cc46d8ebdd44" + "reference": "b59670a09498689c33ce639bac8f5ba26721dab3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/mpdf/mpdf/zipball/dd30e3b01061cf8dfe65e7041ab4cc46d8ebdd44", - "reference": "dd30e3b01061cf8dfe65e7041ab4cc46d8ebdd44", + "url": "https://api.github.com/repos/mpdf/mpdf/zipball/b59670a09498689c33ce639bac8f5ba26721dab3", + "reference": "b59670a09498689c33ce639bac8f5ba26721dab3", "shasum": "" }, "require": { @@ -2592,7 +2586,7 @@ "type": "custom" } ], - "time": "2025-08-18T08:51:51+00:00" + "time": "2025-12-01T10:18:02+00:00" }, { "name": "mpdf/psr-http-message-shim", @@ -2748,16 +2742,16 @@ }, { "name": "nesbot/carbon", - "version": "3.10.3", + "version": "3.11.0", "source": { "type": "git", "url": "https://github.com/CarbonPHP/carbon.git", - "reference": "8e3643dcd149ae0fe1d2ff4f2c8e4bbfad7c165f" + "reference": "bdb375400dcd162624531666db4799b36b64e4a1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/CarbonPHP/carbon/zipball/8e3643dcd149ae0fe1d2ff4f2c8e4bbfad7c165f", - "reference": "8e3643dcd149ae0fe1d2ff4f2c8e4bbfad7c165f", + "url": "https://api.github.com/repos/CarbonPHP/carbon/zipball/bdb375400dcd162624531666db4799b36b64e4a1", + "reference": "bdb375400dcd162624531666db4799b36b64e4a1", "shasum": "" }, "require": { @@ -2765,9 +2759,9 @@ "ext-json": "*", "php": "^8.1", "psr/clock": "^1.0", - "symfony/clock": "^6.3.12 || ^7.0", + "symfony/clock": "^6.3.12 || ^7.0 || ^8.0", "symfony/polyfill-mbstring": "^1.0", - "symfony/translation": "^4.4.18 || ^5.2.1 || ^6.0 || ^7.0" + "symfony/translation": "^4.4.18 || ^5.2.1 || ^6.0 || ^7.0 || ^8.0" }, "provide": { "psr/clock-implementation": "1.0" @@ -2849,7 +2843,7 @@ "type": "tidelift" } ], - "time": "2025-09-06T13:39:36+00:00" + "time": "2025-12-02T21:04:28+00:00" }, { "name": "nikic/fast-route", @@ -3898,16 +3892,16 @@ }, { "name": "symfony/cache", - "version": "v7.3.6", + "version": "v7.4.0", "source": { "type": "git", "url": "https://github.com/symfony/cache.git", - "reference": "1277a1ec61c8d93ea61b2a59738f1deb9bfb6701" + "reference": "a7a1325a5de2e54ddb45fda002ff528162e48293" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/cache/zipball/1277a1ec61c8d93ea61b2a59738f1deb9bfb6701", - "reference": "1277a1ec61c8d93ea61b2a59738f1deb9bfb6701", + "url": "https://api.github.com/repos/symfony/cache/zipball/a7a1325a5de2e54ddb45fda002ff528162e48293", + "reference": "a7a1325a5de2e54ddb45fda002ff528162e48293", "shasum": "" }, "require": { @@ -3915,12 +3909,14 @@ "psr/cache": "^2.0|^3.0", "psr/log": "^1.1|^2|^3", "symfony/cache-contracts": "^3.6", - "symfony/deprecation-contracts": "^2.5|^3.0", + "symfony/deprecation-contracts": "^2.5|^3", "symfony/service-contracts": "^2.5|^3", - "symfony/var-exporter": "^6.4|^7.0" + "symfony/var-exporter": "^6.4|^7.0|^8.0" }, "conflict": { "doctrine/dbal": "<3.6", + "ext-redis": "<6.1", + "ext-relay": "<0.12.1", "symfony/dependency-injection": "<6.4", "symfony/http-kernel": "<6.4", "symfony/var-dumper": "<6.4" @@ -3935,13 +3931,13 @@ "doctrine/dbal": "^3.6|^4", "predis/predis": "^1.1|^2.0", "psr/simple-cache": "^1.0|^2.0|^3.0", - "symfony/clock": "^6.4|^7.0", - "symfony/config": "^6.4|^7.0", - "symfony/dependency-injection": "^6.4|^7.0", - "symfony/filesystem": "^6.4|^7.0", - "symfony/http-kernel": "^6.4|^7.0", - "symfony/messenger": "^6.4|^7.0", - "symfony/var-dumper": "^6.4|^7.0" + "symfony/clock": "^6.4|^7.0|^8.0", + "symfony/config": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/filesystem": "^6.4|^7.0|^8.0", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/messenger": "^6.4|^7.0|^8.0", + "symfony/var-dumper": "^6.4|^7.0|^8.0" }, "type": "library", "autoload": { @@ -3976,7 +3972,7 @@ "psr6" ], "support": { - "source": "https://github.com/symfony/cache/tree/v7.3.6" + "source": "https://github.com/symfony/cache/tree/v7.4.0" }, "funding": [ { @@ -3996,7 +3992,7 @@ "type": "tidelift" } ], - "time": "2025-10-30T13:22:58+00:00" + "time": "2025-11-16T10:14:42+00:00" }, { "name": "symfony/cache-contracts", @@ -4076,16 +4072,16 @@ }, { "name": "symfony/clock", - "version": "v7.3.0", + "version": "v7.4.0", "source": { "type": "git", "url": "https://github.com/symfony/clock.git", - "reference": "b81435fbd6648ea425d1ee96a2d8e68f4ceacd24" + "reference": "9169f24776edde469914c1e7a1442a50f7a4e110" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/clock/zipball/b81435fbd6648ea425d1ee96a2d8e68f4ceacd24", - "reference": "b81435fbd6648ea425d1ee96a2d8e68f4ceacd24", + "url": "https://api.github.com/repos/symfony/clock/zipball/9169f24776edde469914c1e7a1442a50f7a4e110", + "reference": "9169f24776edde469914c1e7a1442a50f7a4e110", "shasum": "" }, "require": { @@ -4130,7 +4126,7 @@ "time" ], "support": { - "source": "https://github.com/symfony/clock/tree/v7.3.0" + "source": "https://github.com/symfony/clock/tree/v7.4.0" }, "funding": [ { @@ -4141,25 +4137,29 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2024-09-25T14:21:43+00:00" + "time": "2025-11-12T15:39:26+00:00" }, { "name": "symfony/console", - "version": "v7.3.6", + "version": "v7.4.0", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "c28ad91448f86c5f6d9d2c70f0cf68bf135f252a" + "reference": "0bc0f45254b99c58d45a8fbf9fb955d46cbd1bb8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/c28ad91448f86c5f6d9d2c70f0cf68bf135f252a", - "reference": "c28ad91448f86c5f6d9d2c70f0cf68bf135f252a", + "url": "https://api.github.com/repos/symfony/console/zipball/0bc0f45254b99c58d45a8fbf9fb955d46cbd1bb8", + "reference": "0bc0f45254b99c58d45a8fbf9fb955d46cbd1bb8", "shasum": "" }, "require": { @@ -4167,7 +4167,7 @@ "symfony/deprecation-contracts": "^2.5|^3", "symfony/polyfill-mbstring": "~1.0", "symfony/service-contracts": "^2.5|^3", - "symfony/string": "^7.2" + "symfony/string": "^7.2|^8.0" }, "conflict": { "symfony/dependency-injection": "<6.4", @@ -4181,16 +4181,16 @@ }, "require-dev": { "psr/log": "^1|^2|^3", - "symfony/config": "^6.4|^7.0", - "symfony/dependency-injection": "^6.4|^7.0", - "symfony/event-dispatcher": "^6.4|^7.0", - "symfony/http-foundation": "^6.4|^7.0", - "symfony/http-kernel": "^6.4|^7.0", - "symfony/lock": "^6.4|^7.0", - "symfony/messenger": "^6.4|^7.0", - "symfony/process": "^6.4|^7.0", - "symfony/stopwatch": "^6.4|^7.0", - "symfony/var-dumper": "^6.4|^7.0" + "symfony/config": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/event-dispatcher": "^6.4|^7.0|^8.0", + "symfony/http-foundation": "^6.4|^7.0|^8.0", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/lock": "^6.4|^7.0|^8.0", + "symfony/messenger": "^6.4|^7.0|^8.0", + "symfony/process": "^6.4|^7.0|^8.0", + "symfony/stopwatch": "^6.4|^7.0|^8.0", + "symfony/var-dumper": "^6.4|^7.0|^8.0" }, "type": "library", "autoload": { @@ -4224,7 +4224,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v7.3.6" + "source": "https://github.com/symfony/console/tree/v7.4.0" }, "funding": [ { @@ -4244,7 +4244,7 @@ "type": "tidelift" } ], - "time": "2025-11-04T01:21:42+00:00" + "time": "2025-11-27T13:27:24+00:00" }, { "name": "symfony/deprecation-contracts", @@ -4315,32 +4315,33 @@ }, { "name": "symfony/error-handler", - "version": "v7.3.6", + "version": "v7.4.0", "source": { "type": "git", "url": "https://github.com/symfony/error-handler.git", - "reference": "bbe40bfab84323d99dab491b716ff142410a92a8" + "reference": "48be2b0653594eea32dcef130cca1c811dcf25c2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/error-handler/zipball/bbe40bfab84323d99dab491b716ff142410a92a8", - "reference": "bbe40bfab84323d99dab491b716ff142410a92a8", + "url": "https://api.github.com/repos/symfony/error-handler/zipball/48be2b0653594eea32dcef130cca1c811dcf25c2", + "reference": "48be2b0653594eea32dcef130cca1c811dcf25c2", "shasum": "" }, "require": { "php": ">=8.2", "psr/log": "^1|^2|^3", - "symfony/var-dumper": "^6.4|^7.0" + "symfony/polyfill-php85": "^1.32", + "symfony/var-dumper": "^6.4|^7.0|^8.0" }, "conflict": { "symfony/deprecation-contracts": "<2.5", "symfony/http-kernel": "<6.4" }, "require-dev": { - "symfony/console": "^6.4|^7.0", + "symfony/console": "^6.4|^7.0|^8.0", "symfony/deprecation-contracts": "^2.5|^3", - "symfony/http-kernel": "^6.4|^7.0", - "symfony/serializer": "^6.4|^7.0", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/serializer": "^6.4|^7.0|^8.0", "symfony/webpack-encore-bundle": "^1.0|^2.0" }, "bin": [ @@ -4372,7 +4373,7 @@ "description": "Provides tools to manage errors and ease debugging PHP code", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/error-handler/tree/v7.3.6" + "source": "https://github.com/symfony/error-handler/tree/v7.4.0" }, "funding": [ { @@ -4392,20 +4393,20 @@ "type": "tidelift" } ], - "time": "2025-10-31T19:12:50+00:00" + "time": "2025-11-05T14:29:59+00:00" }, { "name": "symfony/event-dispatcher", - "version": "v7.3.3", + "version": "v7.4.0", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher.git", - "reference": "b7dc69e71de420ac04bc9ab830cf3ffebba48191" + "reference": "9dddcddff1ef974ad87b3708e4b442dc38b2261d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/b7dc69e71de420ac04bc9ab830cf3ffebba48191", - "reference": "b7dc69e71de420ac04bc9ab830cf3ffebba48191", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/9dddcddff1ef974ad87b3708e4b442dc38b2261d", + "reference": "9dddcddff1ef974ad87b3708e4b442dc38b2261d", "shasum": "" }, "require": { @@ -4422,13 +4423,14 @@ }, "require-dev": { "psr/log": "^1|^2|^3", - "symfony/config": "^6.4|^7.0", - "symfony/dependency-injection": "^6.4|^7.0", - "symfony/error-handler": "^6.4|^7.0", - "symfony/expression-language": "^6.4|^7.0", - "symfony/http-foundation": "^6.4|^7.0", + "symfony/config": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/error-handler": "^6.4|^7.0|^8.0", + "symfony/expression-language": "^6.4|^7.0|^8.0", + "symfony/framework-bundle": "^6.4|^7.0|^8.0", + "symfony/http-foundation": "^6.4|^7.0|^8.0", "symfony/service-contracts": "^2.5|^3", - "symfony/stopwatch": "^6.4|^7.0" + "symfony/stopwatch": "^6.4|^7.0|^8.0" }, "type": "library", "autoload": { @@ -4456,7 +4458,7 @@ "description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/event-dispatcher/tree/v7.3.3" + "source": "https://github.com/symfony/event-dispatcher/tree/v7.4.0" }, "funding": [ { @@ -4476,7 +4478,7 @@ "type": "tidelift" } ], - "time": "2025-08-13T11:49:31+00:00" + "time": "2025-10-28T09:38:46+00:00" }, { "name": "symfony/event-dispatcher-contracts", @@ -4556,16 +4558,16 @@ }, { "name": "symfony/filesystem", - "version": "v7.3.6", + "version": "v7.4.0", "source": { "type": "git", "url": "https://github.com/symfony/filesystem.git", - "reference": "e9bcfd7837928ab656276fe00464092cc9e1826a" + "reference": "d551b38811096d0be9c4691d406991b47c0c630a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/filesystem/zipball/e9bcfd7837928ab656276fe00464092cc9e1826a", - "reference": "e9bcfd7837928ab656276fe00464092cc9e1826a", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/d551b38811096d0be9c4691d406991b47c0c630a", + "reference": "d551b38811096d0be9c4691d406991b47c0c630a", "shasum": "" }, "require": { @@ -4574,7 +4576,7 @@ "symfony/polyfill-mbstring": "~1.8" }, "require-dev": { - "symfony/process": "^6.4|^7.0" + "symfony/process": "^6.4|^7.0|^8.0" }, "type": "library", "autoload": { @@ -4602,7 +4604,7 @@ "description": "Provides basic utilities for the filesystem", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/filesystem/tree/v7.3.6" + "source": "https://github.com/symfony/filesystem/tree/v7.4.0" }, "funding": [ { @@ -4622,27 +4624,27 @@ "type": "tidelift" } ], - "time": "2025-11-05T09:52:27+00:00" + "time": "2025-11-27T13:27:24+00:00" }, { "name": "symfony/finder", - "version": "v7.3.5", + "version": "v7.4.0", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "9f696d2f1e340484b4683f7853b273abff94421f" + "reference": "340b9ed7320570f319028a2cbec46d40535e94bd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/9f696d2f1e340484b4683f7853b273abff94421f", - "reference": "9f696d2f1e340484b4683f7853b273abff94421f", + "url": "https://api.github.com/repos/symfony/finder/zipball/340b9ed7320570f319028a2cbec46d40535e94bd", + "reference": "340b9ed7320570f319028a2cbec46d40535e94bd", "shasum": "" }, "require": { "php": ">=8.2" }, "require-dev": { - "symfony/filesystem": "^6.4|^7.0" + "symfony/filesystem": "^6.4|^7.0|^8.0" }, "type": "library", "autoload": { @@ -4670,7 +4672,7 @@ "description": "Finds files and directories via an intuitive fluent interface", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/finder/tree/v7.3.5" + "source": "https://github.com/symfony/finder/tree/v7.4.0" }, "funding": [ { @@ -4690,27 +4692,26 @@ "type": "tidelift" } ], - "time": "2025-10-15T18:45:57+00:00" + "time": "2025-11-05T05:42:40+00:00" }, { "name": "symfony/http-foundation", - "version": "v7.3.6", + "version": "v7.4.0", "source": { "type": "git", "url": "https://github.com/symfony/http-foundation.git", - "reference": "6379e490d6ecfc5c4224ff3a754b90495ecd135c" + "reference": "769c1720b68e964b13b58529c17d4a385c62167b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-foundation/zipball/6379e490d6ecfc5c4224ff3a754b90495ecd135c", - "reference": "6379e490d6ecfc5c4224ff3a754b90495ecd135c", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/769c1720b68e964b13b58529c17d4a385c62167b", + "reference": "769c1720b68e964b13b58529c17d4a385c62167b", "shasum": "" }, "require": { "php": ">=8.2", - "symfony/deprecation-contracts": "^2.5|^3.0", - "symfony/polyfill-mbstring": "~1.1", - "symfony/polyfill-php83": "^1.27" + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-mbstring": "^1.1" }, "conflict": { "doctrine/dbal": "<3.6", @@ -4719,13 +4720,13 @@ "require-dev": { "doctrine/dbal": "^3.6|^4", "predis/predis": "^1.1|^2.0", - "symfony/cache": "^6.4.12|^7.1.5", - "symfony/clock": "^6.4|^7.0", - "symfony/dependency-injection": "^6.4|^7.0", - "symfony/expression-language": "^6.4|^7.0", - "symfony/http-kernel": "^6.4|^7.0", - "symfony/mime": "^6.4|^7.0", - "symfony/rate-limiter": "^6.4|^7.0" + "symfony/cache": "^6.4.12|^7.1.5|^8.0", + "symfony/clock": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/expression-language": "^6.4|^7.0|^8.0", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/mime": "^6.4|^7.0|^8.0", + "symfony/rate-limiter": "^6.4|^7.0|^8.0" }, "type": "library", "autoload": { @@ -4753,7 +4754,7 @@ "description": "Defines an object-oriented layer for the HTTP specification", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/http-foundation/tree/v7.3.6" + "source": "https://github.com/symfony/http-foundation/tree/v7.4.0" }, "funding": [ { @@ -4773,29 +4774,29 @@ "type": "tidelift" } ], - "time": "2025-11-06T11:05:57+00:00" + "time": "2025-11-13T08:49:24+00:00" }, { "name": "symfony/http-kernel", - "version": "v7.3.6", + "version": "v7.4.0", "source": { "type": "git", "url": "https://github.com/symfony/http-kernel.git", - "reference": "f9a34dc0196677250e3609c2fac9de9e1551a262" + "reference": "7348193cd384495a755554382e4526f27c456085" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-kernel/zipball/f9a34dc0196677250e3609c2fac9de9e1551a262", - "reference": "f9a34dc0196677250e3609c2fac9de9e1551a262", + "url": "https://api.github.com/repos/symfony/http-kernel/zipball/7348193cd384495a755554382e4526f27c456085", + "reference": "7348193cd384495a755554382e4526f27c456085", "shasum": "" }, "require": { "php": ">=8.2", "psr/log": "^1|^2|^3", "symfony/deprecation-contracts": "^2.5|^3", - "symfony/error-handler": "^6.4|^7.0", - "symfony/event-dispatcher": "^7.3", - "symfony/http-foundation": "^7.3", + "symfony/error-handler": "^6.4|^7.0|^8.0", + "symfony/event-dispatcher": "^7.3|^8.0", + "symfony/http-foundation": "^7.4|^8.0", "symfony/polyfill-ctype": "^1.8" }, "conflict": { @@ -4805,6 +4806,7 @@ "symfony/console": "<6.4", "symfony/dependency-injection": "<6.4", "symfony/doctrine-bridge": "<6.4", + "symfony/flex": "<2.10", "symfony/form": "<6.4", "symfony/http-client": "<6.4", "symfony/http-client-contracts": "<2.5", @@ -4822,27 +4824,27 @@ }, "require-dev": { "psr/cache": "^1.0|^2.0|^3.0", - "symfony/browser-kit": "^6.4|^7.0", - "symfony/clock": "^6.4|^7.0", - "symfony/config": "^6.4|^7.0", - "symfony/console": "^6.4|^7.0", - "symfony/css-selector": "^6.4|^7.0", - "symfony/dependency-injection": "^6.4|^7.0", - "symfony/dom-crawler": "^6.4|^7.0", - "symfony/expression-language": "^6.4|^7.0", - "symfony/finder": "^6.4|^7.0", + "symfony/browser-kit": "^6.4|^7.0|^8.0", + "symfony/clock": "^6.4|^7.0|^8.0", + "symfony/config": "^6.4|^7.0|^8.0", + "symfony/console": "^6.4|^7.0|^8.0", + "symfony/css-selector": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/dom-crawler": "^6.4|^7.0|^8.0", + "symfony/expression-language": "^6.4|^7.0|^8.0", + "symfony/finder": "^6.4|^7.0|^8.0", "symfony/http-client-contracts": "^2.5|^3", - "symfony/process": "^6.4|^7.0", - "symfony/property-access": "^7.1", - "symfony/routing": "^6.4|^7.0", - "symfony/serializer": "^7.1", - "symfony/stopwatch": "^6.4|^7.0", - "symfony/translation": "^6.4|^7.0", + "symfony/process": "^6.4|^7.0|^8.0", + "symfony/property-access": "^7.1|^8.0", + "symfony/routing": "^6.4|^7.0|^8.0", + "symfony/serializer": "^7.1|^8.0", + "symfony/stopwatch": "^6.4|^7.0|^8.0", + "symfony/translation": "^6.4|^7.0|^8.0", "symfony/translation-contracts": "^2.5|^3", - "symfony/uid": "^6.4|^7.0", - "symfony/validator": "^6.4|^7.0", - "symfony/var-dumper": "^6.4|^7.0", - "symfony/var-exporter": "^6.4|^7.0", + "symfony/uid": "^6.4|^7.0|^8.0", + "symfony/validator": "^6.4|^7.0|^8.0", + "symfony/var-dumper": "^6.4|^7.0|^8.0", + "symfony/var-exporter": "^6.4|^7.0|^8.0", "twig/twig": "^3.12" }, "type": "library", @@ -4871,7 +4873,7 @@ "description": "Provides a structured process for converting a Request into a Response", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/http-kernel/tree/v7.3.6" + "source": "https://github.com/symfony/http-kernel/tree/v7.4.0" }, "funding": [ { @@ -4891,24 +4893,25 @@ "type": "tidelift" } ], - "time": "2025-11-06T20:58:12+00:00" + "time": "2025-11-27T13:38:24+00:00" }, { "name": "symfony/mime", - "version": "v7.3.4", + "version": "v7.4.0", "source": { "type": "git", "url": "https://github.com/symfony/mime.git", - "reference": "b1b828f69cbaf887fa835a091869e55df91d0e35" + "reference": "bdb02729471be5d047a3ac4a69068748f1a6be7a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/mime/zipball/b1b828f69cbaf887fa835a091869e55df91d0e35", - "reference": "b1b828f69cbaf887fa835a091869e55df91d0e35", + "url": "https://api.github.com/repos/symfony/mime/zipball/bdb02729471be5d047a3ac4a69068748f1a6be7a", + "reference": "bdb02729471be5d047a3ac4a69068748f1a6be7a", "shasum": "" }, "require": { "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", "symfony/polyfill-intl-idn": "^1.10", "symfony/polyfill-mbstring": "^1.0" }, @@ -4923,11 +4926,11 @@ "egulias/email-validator": "^2.1.10|^3.1|^4", "league/html-to-markdown": "^5.0", "phpdocumentor/reflection-docblock": "^3.0|^4.0|^5.0", - "symfony/dependency-injection": "^6.4|^7.0", - "symfony/process": "^6.4|^7.0", - "symfony/property-access": "^6.4|^7.0", - "symfony/property-info": "^6.4|^7.0", - "symfony/serializer": "^6.4.3|^7.0.3" + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/process": "^6.4|^7.0|^8.0", + "symfony/property-access": "^6.4|^7.0|^8.0", + "symfony/property-info": "^6.4|^7.0|^8.0", + "symfony/serializer": "^6.4.3|^7.0.3|^8.0" }, "type": "library", "autoload": { @@ -4959,7 +4962,7 @@ "mime-type" ], "support": { - "source": "https://github.com/symfony/mime/tree/v7.3.4" + "source": "https://github.com/symfony/mime/tree/v7.4.0" }, "funding": [ { @@ -4979,7 +4982,7 @@ "type": "tidelift" } ], - "time": "2025-09-16T08:38:17+00:00" + "time": "2025-11-16T10:14:42+00:00" }, { "name": "symfony/polyfill-ctype", @@ -5816,22 +5819,23 @@ }, { "name": "symfony/string", - "version": "v7.3.4", + "version": "v7.4.0", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "f96476035142921000338bad71e5247fbc138872" + "reference": "d50e862cb0a0e0886f73ca1f31b865efbb795003" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/f96476035142921000338bad71e5247fbc138872", - "reference": "f96476035142921000338bad71e5247fbc138872", + "url": "https://api.github.com/repos/symfony/string/zipball/d50e862cb0a0e0886f73ca1f31b865efbb795003", + "reference": "d50e862cb0a0e0886f73ca1f31b865efbb795003", "shasum": "" }, "require": { "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3.0", "symfony/polyfill-ctype": "~1.8", - "symfony/polyfill-intl-grapheme": "~1.0", + "symfony/polyfill-intl-grapheme": "~1.33", "symfony/polyfill-intl-normalizer": "~1.0", "symfony/polyfill-mbstring": "~1.0" }, @@ -5839,11 +5843,11 @@ "symfony/translation-contracts": "<2.5" }, "require-dev": { - "symfony/emoji": "^7.1", - "symfony/http-client": "^6.4|^7.0", - "symfony/intl": "^6.4|^7.0", + "symfony/emoji": "^7.1|^8.0", + "symfony/http-client": "^6.4|^7.0|^8.0", + "symfony/intl": "^6.4|^7.0|^8.0", "symfony/translation-contracts": "^2.5|^3.0", - "symfony/var-exporter": "^6.4|^7.0" + "symfony/var-exporter": "^6.4|^7.0|^8.0" }, "type": "library", "autoload": { @@ -5882,7 +5886,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v7.3.4" + "source": "https://github.com/symfony/string/tree/v7.4.0" }, "funding": [ { @@ -5902,27 +5906,27 @@ "type": "tidelift" } ], - "time": "2025-09-11T14:36:48+00:00" + "time": "2025-11-27T13:27:24+00:00" }, { "name": "symfony/translation", - "version": "v7.3.4", + "version": "v7.4.0", "source": { "type": "git", "url": "https://github.com/symfony/translation.git", - "reference": "ec25870502d0c7072d086e8ffba1420c85965174" + "reference": "2d01ca0da3f092f91eeedb46f24aa30d2fca8f68" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/translation/zipball/ec25870502d0c7072d086e8ffba1420c85965174", - "reference": "ec25870502d0c7072d086e8ffba1420c85965174", + "url": "https://api.github.com/repos/symfony/translation/zipball/2d01ca0da3f092f91eeedb46f24aa30d2fca8f68", + "reference": "2d01ca0da3f092f91eeedb46f24aa30d2fca8f68", "shasum": "" }, "require": { "php": ">=8.2", "symfony/deprecation-contracts": "^2.5|^3", "symfony/polyfill-mbstring": "~1.0", - "symfony/translation-contracts": "^2.5|^3.0" + "symfony/translation-contracts": "^2.5.3|^3.3" }, "conflict": { "nikic/php-parser": "<5.0", @@ -5941,17 +5945,17 @@ "require-dev": { "nikic/php-parser": "^5.0", "psr/log": "^1|^2|^3", - "symfony/config": "^6.4|^7.0", - "symfony/console": "^6.4|^7.0", - "symfony/dependency-injection": "^6.4|^7.0", - "symfony/finder": "^6.4|^7.0", + "symfony/config": "^6.4|^7.0|^8.0", + "symfony/console": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/finder": "^6.4|^7.0|^8.0", "symfony/http-client-contracts": "^2.5|^3.0", - "symfony/http-kernel": "^6.4|^7.0", - "symfony/intl": "^6.4|^7.0", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/intl": "^6.4|^7.0|^8.0", "symfony/polyfill-intl-icu": "^1.21", - "symfony/routing": "^6.4|^7.0", + "symfony/routing": "^6.4|^7.0|^8.0", "symfony/service-contracts": "^2.5|^3", - "symfony/yaml": "^6.4|^7.0" + "symfony/yaml": "^6.4|^7.0|^8.0" }, "type": "library", "autoload": { @@ -5982,7 +5986,7 @@ "description": "Provides tools to internationalize your application", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/translation/tree/v7.3.4" + "source": "https://github.com/symfony/translation/tree/v7.4.0" }, "funding": [ { @@ -6002,7 +6006,7 @@ "type": "tidelift" } ], - "time": "2025-09-07T11:39:36+00:00" + "time": "2025-11-27T13:27:24+00:00" }, { "name": "symfony/translation-contracts", @@ -6088,16 +6092,16 @@ }, { "name": "symfony/var-dumper", - "version": "v7.3.5", + "version": "v7.4.0", "source": { "type": "git", "url": "https://github.com/symfony/var-dumper.git", - "reference": "476c4ae17f43a9a36650c69879dcf5b1e6ae724d" + "reference": "41fd6c4ae28c38b294b42af6db61446594a0dece" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-dumper/zipball/476c4ae17f43a9a36650c69879dcf5b1e6ae724d", - "reference": "476c4ae17f43a9a36650c69879dcf5b1e6ae724d", + "url": "https://api.github.com/repos/symfony/var-dumper/zipball/41fd6c4ae28c38b294b42af6db61446594a0dece", + "reference": "41fd6c4ae28c38b294b42af6db61446594a0dece", "shasum": "" }, "require": { @@ -6109,10 +6113,10 @@ "symfony/console": "<6.4" }, "require-dev": { - "symfony/console": "^6.4|^7.0", - "symfony/http-kernel": "^6.4|^7.0", - "symfony/process": "^6.4|^7.0", - "symfony/uid": "^6.4|^7.0", + "symfony/console": "^6.4|^7.0|^8.0", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/process": "^6.4|^7.0|^8.0", + "symfony/uid": "^6.4|^7.0|^8.0", "twig/twig": "^3.12" }, "bin": [ @@ -6151,7 +6155,7 @@ "dump" ], "support": { - "source": "https://github.com/symfony/var-dumper/tree/v7.3.5" + "source": "https://github.com/symfony/var-dumper/tree/v7.4.0" }, "funding": [ { @@ -6171,20 +6175,20 @@ "type": "tidelift" } ], - "time": "2025-09-27T09:00:46+00:00" + "time": "2025-10-27T20:36:44+00:00" }, { "name": "symfony/var-exporter", - "version": "v7.3.4", + "version": "v7.4.0", "source": { "type": "git", "url": "https://github.com/symfony/var-exporter.git", - "reference": "0f020b544a30a7fe8ba972e53ee48a74c0bc87f4" + "reference": "03a60f169c79a28513a78c967316fbc8bf17816f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-exporter/zipball/0f020b544a30a7fe8ba972e53ee48a74c0bc87f4", - "reference": "0f020b544a30a7fe8ba972e53ee48a74c0bc87f4", + "url": "https://api.github.com/repos/symfony/var-exporter/zipball/03a60f169c79a28513a78c967316fbc8bf17816f", + "reference": "03a60f169c79a28513a78c967316fbc8bf17816f", "shasum": "" }, "require": { @@ -6192,9 +6196,9 @@ "symfony/deprecation-contracts": "^2.5|^3" }, "require-dev": { - "symfony/property-access": "^6.4|^7.0", - "symfony/serializer": "^6.4|^7.0", - "symfony/var-dumper": "^6.4|^7.0" + "symfony/property-access": "^6.4|^7.0|^8.0", + "symfony/serializer": "^6.4|^7.0|^8.0", + "symfony/var-dumper": "^6.4|^7.0|^8.0" }, "type": "library", "autoload": { @@ -6232,7 +6236,7 @@ "serialize" ], "support": { - "source": "https://github.com/symfony/var-exporter/tree/v7.3.4" + "source": "https://github.com/symfony/var-exporter/tree/v7.4.0" }, "funding": [ { @@ -6252,7 +6256,7 @@ "type": "tidelift" } ], - "time": "2025-09-11T10:12:26+00:00" + "time": "2025-09-11T10:15:23+00:00" }, { "name": "topthink/think-container", @@ -6641,16 +6645,16 @@ }, { "name": "webman/database", - "version": "v2.1.6", + "version": "v2.1.7", "source": { "type": "git", "url": "https://github.com/webman-php/database.git", - "reference": "943c5a0f92d8959d9160c2ee3925112b15c308c8" + "reference": "81a8a834cc8db5a5dbb46fe0073a9d5ac73848f6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/webman-php/database/zipball/943c5a0f92d8959d9160c2ee3925112b15c308c8", - "reference": "943c5a0f92d8959d9160c2ee3925112b15c308c8", + "url": "https://api.github.com/repos/webman-php/database/zipball/81a8a834cc8db5a5dbb46fe0073a9d5ac73848f6", + "reference": "81a8a834cc8db5a5dbb46fe0073a9d5ac73848f6", "shasum": "" }, "require": { @@ -6673,9 +6677,9 @@ "description": "Webman database", "support": { "issues": "https://github.com/webman-php/database/issues", - "source": "https://github.com/webman-php/database/tree/v2.1.6" + "source": "https://github.com/webman-php/database/tree/v2.1.7" }, - "time": "2025-04-04T13:23:25+00:00" + "time": "2025-11-27T08:09:25+00:00" }, { "name": "webman/gateway-worker", @@ -6710,6 +6714,40 @@ }, "time": "2024-04-30T01:05:42+00:00" }, + { + "name": "webman/log", + "version": "v2.1.3", + "source": { + "type": "git", + "url": "https://github.com/webman-php/log.git", + "reference": "14e3f3e64e87783952417a68cf5205c1293aba69" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/webman-php/log/zipball/14e3f3e64e87783952417a68cf5205c1293aba69", + "reference": "14e3f3e64e87783952417a68cf5205c1293aba69", + "shasum": "" + }, + "require": { + "workerman/webman-framework": "^2.1" + }, + "type": "library", + "autoload": { + "psr-4": { + "Webman\\Log\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Webman plugin webman/log", + "support": { + "issues": "https://github.com/webman-php/log/issues", + "source": "https://github.com/webman-php/log/tree/v2.1.3" + }, + "time": "2025-02-26T11:43:36+00:00" + }, { "name": "webman/redis", "version": "v2.1.3", @@ -6748,16 +6786,16 @@ }, { "name": "webman/redis-queue", - "version": "v2.1.0", + "version": "v2.1.1", "source": { "type": "git", "url": "https://github.com/webman-php/redis-queue.git", - "reference": "12e1c9915ec64c91c0c9c255606bd0c7de27334c" + "reference": "ff4791e21f3c324a47e21da7b6f2dae5a7311dcb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/webman-php/redis-queue/zipball/12e1c9915ec64c91c0c9c255606bd0c7de27334c", - "reference": "12e1c9915ec64c91c0c9c255606bd0c7de27334c", + "url": "https://api.github.com/repos/webman-php/redis-queue/zipball/ff4791e21f3c324a47e21da7b6f2dae5a7311dcb", + "reference": "ff4791e21f3c324a47e21da7b6f2dae5a7311dcb", "shasum": "" }, "require": { @@ -6776,9 +6814,9 @@ "description": "Redis message queue plugin for webman.", "support": { "issues": "https://github.com/webman-php/redis-queue/issues", - "source": "https://github.com/webman-php/redis-queue/tree/v2.1.0" + "source": "https://github.com/webman-php/redis-queue/tree/v2.1.1" }, - "time": "2025-01-24T12:56:01+00:00" + "time": "2025-11-14T07:12:52+00:00" }, { "name": "workerman/coroutine", @@ -7135,28 +7173,31 @@ "type": "patreon" } ], - "time": "2025-06-12T13:34:04+00:00" + "time": "2025-11-12T01:36:20+00:00" }, { "name": "zoujingli/ip2region", - "version": "v3.0.0", + "version": "v3.0.8", "source": { "type": "git", "url": "https://github.com/zoujingli/ip2region.git", - "reference": "fd31d7a023f28f4e2f3e63f46f1fd51f8b2672f6" + "reference": "4ab4283f4b4ea4222da6cb97fbf7f7167f1ebb53" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/zoujingli/ip2region/zipball/fd31d7a023f28f4e2f3e63f46f1fd51f8b2672f6", - "reference": "fd31d7a023f28f4e2f3e63f46f1fd51f8b2672f6", + "url": "https://api.github.com/repos/zoujingli/ip2region/zipball/4ab4283f4b4ea4222da6cb97fbf7f7167f1ebb53", + "reference": "4ab4283f4b4ea4222da6cb97fbf7f7167f1ebb53", "shasum": "" }, "require": { - "php": ">=5.4" + "php": ">=7.1.0" }, "suggest": { "ext-opcache": "启用 OPcache 以获得更好的性能" }, + "bin": [ + "bin/ip2down" + ], "type": "library", "extra": { "branch-alias": { @@ -7165,10 +7206,13 @@ }, "autoload": { "files": [ - "ip2region.php" + "src/common.php" ], + "psr-4": { + "ip2region\\": "src/ip2region/" + }, "classmap": [ - "src/" + "src/Ip2Region.php" ] }, "notification-url": "https://packagist.org/downloads/", @@ -7182,30 +7226,28 @@ "homepage": "https://thinkadmin.top" } ], - "description": "Ip2Region v3.0 for PHP - 企业级 IP 地理位置查询库,支持 IPv4/IPv6,分片文件管理,智能缓存,零依赖", + "description": "ip2region v3.0 for PHP - 企业级 IP 地理位置查询库,支持 IPv4 和 IPv6,多种缓存策略,零依赖,开箱即用", "homepage": "https://github.com/zoujingli/Ip2Region", "keywords": [ "IP", - "IP定位", "Ip2Region", - "geoip", + "composer", "geolocation", "ipv4", "ipv6", "location", + "php", "企业级", - "分片数据库", - "压缩", - "地理位置", - "地理位置查询", - "缓存", + "智能加载", + "缓存策略", + "零依赖", "高性能" ], "support": { "issues": "https://github.com/zoujingli/ip2region/issues", - "source": "https://github.com/zoujingli/ip2region/tree/v3.0.0" + "source": "https://github.com/zoujingli/ip2region/tree/v3.0.8" }, - "time": "2025-09-15T12:27:22+00:00" + "time": "2025-11-20T11:02:33+00:00" } ], "packages-dev": [], diff --git a/config/plugin/webman/log/app.php b/config/plugin/webman/log/app.php new file mode 100644 index 0000000..6217553 --- /dev/null +++ b/config/plugin/webman/log/app.php @@ -0,0 +1,19 @@ + true, + 'exception' => [ + // 是否记录异常到日志 + 'enable' => true, + // 不会记录到日志的异常类 + 'dontReport' => [ + support\exception\BusinessException::class + ] + ], + 'dontReport' => [ + 'app' => [], + 'controller' => [], + 'action' => [], + 'path' => [] + ], + 'channel' => 'default' // 日志通道(在config/log.php里配置,默认是default) +]; diff --git a/config/plugin/webman/log/middleware.php b/config/plugin/webman/log/middleware.php new file mode 100644 index 0000000..7c7b0a9 --- /dev/null +++ b/config/plugin/webman/log/middleware.php @@ -0,0 +1,21 @@ + + * @copyright walkor + * @link http://www.workerman.net/ + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ + +use Webman\Log\Middleware; + +return [ + '@' => [ + Middleware::class + ] +]; diff --git a/config/route.php b/config/route.php index 7b092f3..54f6cce 100644 --- a/config/route.php +++ b/config/route.php @@ -505,6 +505,16 @@ Route::group('/admin', function () { Route::any('/destroy', [app\admin\controller\FinanceReconDubaiFilesController::class, 'destroy']); }); }); + + + // 物品领用 + Route::group('/inv', function () { + Route::any('/item/list', [app\admin\controller\InvItemController::class, 'index']); + Route::any('/item/store', [app\admin\controller\InvItemController::class, 'store']); + Route::any('/item/update', [app\admin\controller\InvItemController::class, 'update']); + Route::any('/item/show', [app\admin\controller\InvItemController::class, 'show']); + Route::any('/item/destroy', [app\admin\controller\InvItemController::class, 'destroy']); + }); }); @@ -624,4 +634,17 @@ Route::group('/crm', function () { Route::any('/update', [app\common\controller\VehModificationController::class, 'update']); Route::any('/destroy', [app\common\controller\VehModificationController::class, 'destroy']); }); +}); + + + +// 移动端 +Route::group('/mobile', function () { + // 物品领用 + Route::group('/inv', function () { + Route::any('/requisition/apply', [app\mobile\controller\InvRequisitionController::class, 'apply']); + Route::any('/requisition/update', [app\mobile\controller\InvRequisitionController::class, 'update']); + Route::any('/requisition/item/return', [app\mobile\controller\InvRequisitionItemReturnController::class, 'store']); + Route::any('/requisition/item/return/update', [app\mobile\controller\InvRequisitionItemReturnController::class, 'update']); + }); }); \ No newline at end of file diff --git a/public/db_log/物品领用.sql b/public/db_log/物品领用.sql new file mode 100644 index 0000000..570a17e --- /dev/null +++ b/public/db_log/物品领用.sql @@ -0,0 +1,174 @@ +SET NAMES utf8mb4; +SET FOREIGN_KEY_CHECKS = 0; + +-- 1. 物品主数据表:增加 company_id +DROP TABLE IF EXISTS inv_item; +CREATE TABLE inv_item ( + id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键ID', + company_id BIGINT(20) UNSIGNED DEFAULT NULL COMMENT '物品所属公司ID', + item_code VARCHAR(64) NOT NULL COMMENT '物品编号(公司内部编码)', + item_name VARCHAR(128) NOT NULL COMMENT '物品名称', + category_id BIGINT(20) UNSIGNED DEFAULT NULL COMMENT '物品分类ID(可选,指向分类表)', + spec_model VARCHAR(128) DEFAULT NULL COMMENT '规格型号', + unit VARCHAR(32) DEFAULT '件' COMMENT '计量单位,如 件/盒/包', + is_consumable TINYINT(1) DEFAULT 1 COMMENT '是否耗材:1耗材(不需归还),0非耗材(需归还)', + stock_qty INT(11) DEFAULT 0 COMMENT '当前库存数量', + manage_user_id BIGINT(20) UNSIGNED DEFAULT NULL COMMENT '物品管理员用户ID', + location VARCHAR(255) DEFAULT NULL COMMENT '存放位置说明', + remark VARCHAR(255) DEFAULT NULL COMMENT '备注', + created_at INT(11) NOT NULL DEFAULT 0 COMMENT '创建时间时间戳', + updated_at INT(11) NOT NULL DEFAULT 0 COMMENT '更新时间时间戳', + PRIMARY KEY (id), + UNIQUE KEY uk_item_code (item_code), + KEY idx_company (company_id), + KEY idx_category (category_id), + KEY idx_manage_user (manage_user_id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='物品主数据表'; + + +-- 2. 物品领用/借用申请单(主表) +DROP TABLE IF EXISTS inv_requisition; +CREATE TABLE inv_requisition ( + id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键ID', + order_no VARCHAR(64) NOT NULL COMMENT '申请单号', + + applicant_company_id BIGINT(20) UNSIGNED DEFAULT NULL COMMENT '申请人所在公司ID(可选)', + dept_id BIGINT(20) UNSIGNED DEFAULT NULL COMMENT '申请人部门ID', + applicant_id BIGINT(20) UNSIGNED NOT NULL COMMENT '申请人用户ID', + applicant_name VARCHAR(64) DEFAULT NULL COMMENT '申请人姓名快照', + leader_id BIGINT(20) UNSIGNED DEFAULT NULL COMMENT '直属上级用户ID', + manager_id BIGINT(20) UNSIGNED DEFAULT NULL COMMENT '物品管理员用户ID(主负责人)', + + borrow_reason VARCHAR(255) NOT NULL COMMENT '领用/借用事由', + use_start_date DATE NOT NULL COMMENT '借用/预计使用开始日期', + use_end_date DATE DEFAULT NULL COMMENT '预计归还日期(耗材可为空)', + + -- 0草稿 10待直属上级审批 20直属上级通过待物品管理员审批 + -- 30全部通过 40已驳回 50已取消 + status TINYINT(3) NOT NULL DEFAULT 10 COMMENT '审批状态', + + is_returned TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否已全部归还(针对需要归还的物品):0否 1是', + + remark VARCHAR(255) DEFAULT NULL COMMENT '备注', + created_at INT(11) NOT NULL DEFAULT 0 COMMENT '创建时间时间戳', + updated_at INT(11) NOT NULL DEFAULT 0 COMMENT '更新时间时间戳', + + PRIMARY KEY (id), + UNIQUE KEY uk_order_no (order_no), + KEY idx_applicant (applicant_id), + KEY idx_status (status) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='物品领用/借用申请单'; + + +-- 3. 申请单明细表:支持部分、多次归还 +DROP TABLE IF EXISTS inv_requisition_item; +CREATE TABLE inv_requisition_item ( + id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键ID', + requisition_id BIGINT(20) UNSIGNED NOT NULL COMMENT '申请单ID', + item_id BIGINT(20) UNSIGNED NOT NULL COMMENT '物品ID', + + item_name_snap VARCHAR(128) DEFAULT NULL COMMENT '物品名称快照', + spec_model_snap VARCHAR(128) DEFAULT NULL COMMENT '规格型号快照', + unit_snap VARCHAR(32) DEFAULT NULL COMMENT '单位快照', + is_consumable_snap TINYINT(1) DEFAULT NULL COMMENT '是否耗材快照:1耗材 0非耗材', + + request_qty INT(11) NOT NULL DEFAULT 1 COMMENT '申请数量', + approved_qty INT(11) DEFAULT NULL COMMENT '实批数量(可与申请数量不同)', + returned_qty INT(11) NOT NULL DEFAULT 0 COMMENT '已归还数量(累加)', + + -- 对于非耗材:returned_qty >= approved_qty 时置为1 + is_finished TINYINT(1) NOT NULL DEFAULT 0 COMMENT '该物品是否已完成归还:0否 1是', + + remark VARCHAR(255) DEFAULT NULL COMMENT '行备注', + created_at INT(11) NOT NULL DEFAULT 0 COMMENT '创建时间时间戳', + updated_at INT(11) NOT NULL DEFAULT 0 COMMENT '更新时间时间戳', + + PRIMARY KEY (id), + KEY idx_req (requisition_id), + KEY idx_item (item_id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='物品领用申请明细表'; + + +-- 4. 审批记录表(同上一版,略微无改动) +DROP TABLE IF EXISTS inv_requisition_approve; +CREATE TABLE inv_requisition_approve ( + id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键ID', + requisition_id BIGINT(20) UNSIGNED NOT NULL COMMENT '申请单ID', + node_type VARCHAR(32) NOT NULL COMMENT '审批节点类型:leader=直属上级,manager=物品管理员,other=其他', + approver_id BIGINT(20) UNSIGNED NOT NULL COMMENT '审批人用户ID', + approver_name VARCHAR(64) DEFAULT NULL COMMENT '审批人姓名快照', + action TINYINT(3) NOT NULL COMMENT '审批动作:1通过 2驳回', + comment VARCHAR(255) DEFAULT NULL COMMENT '审批意见', + action_at INT(11) NOT NULL DEFAULT 0 COMMENT '审批时间时间戳', + + created_at INT(11) NOT NULL DEFAULT 0 COMMENT '创建时间时间戳', + updated_at INT(11) NOT NULL DEFAULT 0 COMMENT '更新时间时间戳', + + PRIMARY KEY (id), + KEY idx_req (requisition_id), + KEY idx_node (node_type), + KEY idx_approver (approver_id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='物品领用申请审批记录表'; + + +-- 5. 申请单附件表(申请时上传) +DROP TABLE IF EXISTS inv_requisition_attachment; +CREATE TABLE inv_requisition_attachment ( + id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键ID', + requisition_id BIGINT(20) UNSIGNED NOT NULL COMMENT '申请单ID', + file_name VARCHAR(255) NOT NULL COMMENT '文件原始名称', + file_path VARCHAR(255) NOT NULL COMMENT '文件存储路径/URL', + file_size INT(11) DEFAULT NULL COMMENT '文件大小(字节)', + created_at INT(11) NOT NULL DEFAULT 0 COMMENT '创建时间时间戳', + updated_at INT(11) NOT NULL DEFAULT 0 COMMENT '更新时间时间戳', + + PRIMARY KEY (id), + KEY idx_req (requisition_id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='物品领用申请附件表'; + + +/* ================= 归 还 相 关 表 ================= */ + +-- 6. 归还记录表:一次归还动作(可归还一个明细的一部分) +DROP TABLE IF EXISTS inv_requisition_item_return; +CREATE TABLE inv_requisition_item_return ( + id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键ID', + requisition_id BIGINT(20) UNSIGNED NOT NULL COMMENT '申请单ID', + requisition_item_id BIGINT(20) UNSIGNED NOT NULL COMMENT '申请单明细ID', + item_id BIGINT(20) UNSIGNED NOT NULL COMMENT '物品ID', + + return_qty INT(11) NOT NULL DEFAULT 0 COMMENT '本次归还数量', + handler_id BIGINT(20) UNSIGNED NOT NULL COMMENT '办理归还的人(通常是物品管理员)', + handler_name VARCHAR(64) DEFAULT NULL COMMENT '办理人姓名快照', + + return_status TINYINT(3) NOT NULL DEFAULT 1 COMMENT '归还状态:1正常归还 2损坏 3其他', + remark VARCHAR(255) DEFAULT NULL COMMENT '归还备注', + + return_at INT(11) NOT NULL DEFAULT 0 COMMENT '归还时间时间戳', + created_at INT(11) NOT NULL DEFAULT 0 COMMENT '创建时间时间戳', + updated_at INT(11) NOT NULL DEFAULT 0 COMMENT '更新时间时间戳', + + PRIMARY KEY (id), + KEY idx_req (requisition_id), + KEY idx_req_item (requisition_item_id), + KEY idx_item (item_id), + KEY idx_handler (handler_id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='物品归还记录表(支持多次、部分归还)'; + + +-- 7. 归还附件表:归还时拍照留底 +DROP TABLE IF EXISTS inv_requisition_item_return_attachment; +CREATE TABLE inv_requisition_item_return_attachment ( + id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键ID', + return_id BIGINT(20) UNSIGNED NOT NULL COMMENT '归还记录ID', + file_name VARCHAR(255) NOT NULL COMMENT '文件原始名称', + file_path VARCHAR(255) NOT NULL COMMENT '文件存储路径/URL(通常为照片)', + file_size INT(11) DEFAULT NULL COMMENT '文件大小(字节)', + created_at INT(11) NOT NULL DEFAULT 0 COMMENT '创建时间时间戳', + updated_at INT(11) NOT NULL DEFAULT 0 COMMENT '更新时间时间戳', + + PRIMARY KEY (id), + KEY idx_return (return_id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='物品归还记录附件表(归还拍照留底)'; + +SET FOREIGN_KEY_CHECKS = 1; From 725710abcb7ec9bf8ad6f42805c13d8d5bd0b608 Mon Sep 17 00:00:00 2001 From: mzl Date: Tue, 9 Dec 2025 07:33:04 +0000 Subject: [PATCH 092/110] ts --- app/common/service/InvItemService.php | 73 + .../InvRequisitionItemReturnService.php | 186 +- app/common/service/InvRequisitionService.php | 8 +- .../approval/service/ApprovalFlowService.php | 1951 +--------------- .../service/ApprovalFlowService_20251208.php | 1953 +++++++++++++++++ .../traits/ApprovalDiscussionTrait.php | 151 ++ .../approval/traits/ApprovalEngineTrait.php | 592 +++++ .../traits/ApprovalNotificationTrait.php | 155 ++ .../approval/traits/ApprovalQueryTrait.php | 475 ++++ .../approval/traits/ApprovalResolverTrait.php | 406 ++++ .../approval/traits/ApprovalTransferTrait.php | 130 ++ .../InvRequisitionItemReturnController.php | 2 +- config/route.php | 1 + public/db_log/审批.sql | 94 + 14 files changed, 4213 insertions(+), 1964 deletions(-) create mode 100644 app/common/utils/approval/service/ApprovalFlowService_20251208.php create mode 100644 app/common/utils/approval/traits/ApprovalDiscussionTrait.php create mode 100644 app/common/utils/approval/traits/ApprovalEngineTrait.php create mode 100644 app/common/utils/approval/traits/ApprovalNotificationTrait.php create mode 100644 app/common/utils/approval/traits/ApprovalQueryTrait.php create mode 100644 app/common/utils/approval/traits/ApprovalResolverTrait.php create mode 100644 app/common/utils/approval/traits/ApprovalTransferTrait.php create mode 100644 public/db_log/审批.sql diff --git a/app/common/service/InvItemService.php b/app/common/service/InvItemService.php index 05bbe84..43a1f44 100644 --- a/app/common/service/InvItemService.php +++ b/app/common/service/InvItemService.php @@ -6,6 +6,7 @@ use app\common\validate\InvItemValidate; use app\common\traits\crud\CrudServiceTrait; use app\common\model\InvItem; use support\Request; +use support\exception\BusinessException; class InvItemService { @@ -62,4 +63,76 @@ class InvItemService $data['location'] = ''; return $data; } + + + /** + * 变动物品库存数量 + * + * @param int $itemId 物品ID + * @param int $changeQty 变动数量:>0 入库,<0 出库 + * @param bool $allowNegative 是否允许库存变成负数,默认不允许 + * + * @return InvItem 最新的物品记录 + * @throws BusinessException + */ + public function changeStock(int $itemId, int $changeQty, bool $allowNegative = false): InvItem + { + if ($changeQty === 0) { + throw new BusinessException('库存变动数量不能为 0'); + } + + $now = time(); + + // 构建查询 + $query = InvItem::query()->where('id', $itemId); + + // 如果不允许负数,并且是出库场景(变动为负) + if (! $allowNegative && $changeQty < 0) { + // 要保证:当前 stock_qty >= 出库数量 + $query->where('stock_qty', '>=', -$changeQty); + } + + // 使用 increment 实现:UPDATE inv_item SET stock_qty = stock_qty + ?, updated_at = ? WHERE ... + $affected = $query->increment('stock_qty', $changeQty, [ + 'updated_at' => $now, + ]); + + if ($affected === 0) { + // 更新不到行: + // 1) 物品不存在 + // 2) 或者库存不足(不允许负数时) + $exists = InvItem::query()->where('id', $itemId)->exists(); + if (! $exists) { + throw new BusinessException('物品不存在'); + } + throw new BusinessException('库存不足,无法完成库存变动'); + } + + // 再查一次最新数据返回(在外层事务中也会保持一致) + /** @var InvItem $item */ + $item = InvItem::query()->find($itemId); + return $item; + } + + /** + * 出库(减库存,默认不允许负数) + */ + public function stockOut(int $itemId, int $qty, bool $allowNegative = false): InvItem + { + if ($qty <= 0) { + throw new BusinessException('出库数量必须大于 0'); + } + return $this->changeStock($itemId, -$qty, $allowNegative); + } + + /** + * 入库(加库存) + */ + public function stockIn(int $itemId, int $qty): InvItem + { + if ($qty <= 0) { + throw new BusinessException('入库数量必须大于 0'); + } + return $this->changeStock($itemId, $qty, true); + } } \ No newline at end of file diff --git a/app/common/service/InvRequisitionItemReturnService.php b/app/common/service/InvRequisitionItemReturnService.php index 5955a77..a90f199 100644 --- a/app/common/service/InvRequisitionItemReturnService.php +++ b/app/common/service/InvRequisitionItemReturnService.php @@ -2,6 +2,7 @@ namespace app\common\service; +use app\common\model\InvRequisition; use app\common\model\InvRequisitionItem; use app\common\validate\InvRequisitionItemReturnValidate; use app\common\traits\crud\CrudServiceTrait; @@ -268,30 +269,181 @@ class InvRequisitionItemReturnService InvRequisitionItemReturnAttachment::where('return_id', $returnId)->delete(); } - - - public function returnConfirm(Request $request, array $data, string $scene = '') + /** + * 物品领用归还确认 + * + * @param Request $request + * @param array $data + * @param string $scene + * @return array|bool + * @throws BusinessException + */ + public function serviceReturnConfirm(Request $request, array $data, string $scene = 'returnConfirm') { try { - - // 关联表增加事务处理 - return Db::transaction(function () use ($request, $data) { - $data = $this->preProcess('Update', $data, $request); - $config = $this->buildQueryConfig($request, 'update'); + // 归还确认是业务顶层方法,这里统一使用事务 + return Db::transaction(function () use ($request, $data, $scene) { - $attachment = $data['attachment']; - unset($data['attachment']); + // 1. 当前操作人信息 + $userInfo = (new UserService())->getUserInfo($request->user->uid, ''); + if (empty($userInfo['user']['id']) || empty($userInfo['user']['name'])) { + throw new BusinessException('无法获取当前用户信息'); + } - $this->logicUpdateBy($config, $data); - // 获取更新后的完整记录 - $updated = $this->logicShow($config); - // 处理附件更新 - $this->processRequisitionItemReturnAttachment($updated, $attachment, true); - return $this->postProcess('Update', $updated); + $handlerId = (int)$userInfo['user']['id']; + $handlerName = (string)$userInfo['user']['name']; + + $data['handler_id'] = $handlerId; + $data['handler_name'] = $handlerName; + + // 确认状态:1=通过,2=拒绝 (按你现在用法) + $status = isset($data['status']) ? (int)$data['status'] : null; + if (!in_array($status, [1, 2], true)) { + throw new BusinessException('非法的归还处理状态'); + } + + // 2. 构建查询配置,定位到这条归还记录 + $config = $this->buildQueryConfig($request, $scene); + + // 更新时不允许前端覆盖主键 + unset($data['requisition_item_return_id']); + + // 3. 获取现有记录 + /** @var \app\model\InvRequisitionItemReturn|null $existingRecord */ + $existingRecord = $this->logicShow($config); + + if (!$existingRecord) { + throw new BusinessException('未找到对应的归还申请记录'); + } + + // 已处理校验:只要归还记录的 status 已经是「通过或拒绝」,就不能再处理 + $existingStatus = (int)($existingRecord->status ?? 0); + if (in_array($existingStatus, [1, 2], true) || !empty($existingRecord->handler_id)) { + throw new BusinessException('该归还申请已经被处理过了'); + } + + // 4. 更新归还记录(状态 + 审核人 + 备注等) + $updateResult = $this->logicUpdateBy($config, $data); + if ($updateResult === 0) { + throw new BusinessException('更新归还申请失败,可能记录不存在或已被处理'); + } + + // 5. 若是拒绝,直接返回(不改库存、不改已归还数量) + if ($status === 2) { + // 拒绝:这里你返回 true 或者返回更新后的记录都可以 + return true; + } + + // 6. 审核通过:继续处理领用明细 & 库存 & 领用单状态 + // 注意:requisition_item_id、item_id、return_qty 等字段在本次确认流程中不会被修改, + // 所以可以直接使用 $existingRecord 中的值(都是只读字段) + $requisitionItemId = (int)$existingRecord->requisition_item_id; + $itemId = (int)$existingRecord->item_id; + $returnQty = (int)$existingRecord->return_qty; + $requisitionId = (int)$existingRecord->requisition_id; + + if ($requisitionItemId <= 0 || $itemId <= 0 || $returnQty <= 0 || $requisitionId <= 0) { + throw new BusinessException('归还申请记录缺少必要字段,无法处理'); + } + + // 6.1 处理领用明细 + /** @var \app\model\InvRequisitionItem|null $item */ + $item = InvRequisitionItem::find($requisitionItemId); + if (!$item) { + throw new BusinessException('未找到对应的领用明细记录'); + } + + $approvedQty = (int)($item->approved_qty ?? 0); + $returnedQty = (int)($item->returned_qty ?? 0); + + if ($approvedQty <= 0) { + throw new BusinessException('该物品尚未审批出库,无法归还'); + } + + if ($returnedQty + $returnQty > $approvedQty) { + throw new BusinessException('归还数量超过已借数量,无法确认'); + } + + $item->returned_qty = $returnedQty + $returnQty; + if ($item->returned_qty >= $approvedQty) { + $item->is_finished = 1; + } + $item->updated_at = time(); + + if ($item->save() === false) { + throw new BusinessException('更新领用明细失败'); + } + + // 6.2 处理库存(入库) + (new InvItemService())->stockIn($itemId, $returnQty); + + // 6.3 检查整张单是否已全部归还(只看非耗材) + $notFinishedCount = InvRequisitionItem::query() + ->where('requisition_id', $requisitionId) + ->where('is_consumable_snap', 0) + ->where('is_finished', 0) + ->count(); + + if ($notFinishedCount === 0) { + // 全部非耗材都已归还 + $affected = InvRequisition::where('id', $requisitionId)->update([ + 'is_returned' => 1, + 'updated_at' => time(), + ]); + + // Eloquent::update 返回的是影响行数,0 表示未更新到任何行 + if ($affected === 0) { + throw new BusinessException('更新领用单状态失败,可能记录不存在'); + } + } + + // 7. 返回处理结果(保持你原来的 postProcess 习惯) + return $this->postProcess(ucfirst($scene), $existingRecord); }); - + + } catch (BusinessException $e) { + throw $e; } catch (\Throwable $e) { throw new BusinessException("确认归还物品失败:" . $e->getMessage()); } } + + /** + * 构建查询配置 + * + * @param Request $request + * @param string $scene + * @return array + */ + protected function buildQueryConfig(Request $request, string $scene = 'default'): array + { + $p = $request->all(); + + $config = []; + $config['model'] = $this->modelClass; + switch ($scene) { + case 'returnConfirm': + $config['where']['id'] = $p['requisition_item_return_id']; + break; + case 'index': + $config['where']['status'] = 1; + $config['order']['sort'] = 'desc'; + $config['order']['id'] = 'desc'; + break; + case 'show': + $config['where']['id'] = $p['id']; + $config['where']['status'] = 1; + break; + case 'store': + break; + case 'update': + $config['where']['id'] = $p['id']; + break; + case 'destroy': + $config['where']['id'] = $p['id']; + break; + } + + return $config; + } } \ No newline at end of file diff --git a/app/common/service/InvRequisitionService.php b/app/common/service/InvRequisitionService.php index dfff880..6c4e8e2 100644 --- a/app/common/service/InvRequisitionService.php +++ b/app/common/service/InvRequisitionService.php @@ -47,9 +47,13 @@ class InvRequisitionService { $user = (new UserService())->getUserInfo($request->user->uid, ''); + if (empty($user['user']['id']) || empty($user['user']['name'])) { + throw new BusinessException('无法获取当前用户信息'); + } + $data['order_no'] = $this->generateOrderNo(); - $data['applicant_id'] = $user['user']['id']; - $data['applicant_name'] = $user['user']['name']; + $data['applicant_id'] = (int)$user['user']['id']; + $data['applicant_name'] = (string)$user['user']['name']; $data['leader_id'] = $user['user']['report_to']; $data['status'] = InvRequisition::STATUS_LEADER; diff --git a/app/common/utils/approval/service/ApprovalFlowService.php b/app/common/utils/approval/service/ApprovalFlowService.php index a26b79a..6b39023 100644 --- a/app/common/utils/approval/service/ApprovalFlowService.php +++ b/app/common/utils/approval/service/ApprovalFlowService.php @@ -7,1947 +7,10 @@ use support\Log; class ApprovalFlowService { - /** 启动审批流程 */ - public function start(string $flowType, string $bizType, string $bizId, array $formData, int $initiatorId): array - { - return Db::transaction(function () use ($flowType, $bizType, $bizId, $formData, $initiatorId) { - $flow = Db::table('approval_flows')->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, - 'round' => 1, - 'deadline_at' => $deadlineAt, - 'created_at' => $now, - 'updated_at' => $now, - ]); - - $instance = (object)[ - 'id' => $instanceId, - 'flow_type' => $flowType, - 'form_data' => json_encode($formData, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), - 'initiator_id' => $initiatorId, - 'current_step' => $firstIndex, - 'status' => 'processing', - 'flow_type' => $flowType, - 'biz_type' => $bizType, - 'biz_id' => $bizId, - 'round' => 1, - ]; - - // 解析首步审批人(支持 role_id 或 roles.name) - $approverIds = $this->resolveApprovers($firstStep, $instance, true); - foreach ($approverIds as $uid) { - $this->insertApprovalRecord($instance, $firstIndex, $uid); - } - - $extra = []; - $submitNote = $formData['submit_note'] ?? ''; - if (is_string($submitNote) && $submitNote !== '') { - $extra['note'] = $submitNote; - } - $this->sendNotification($approverIds, $instance, 'pending', false, $extra); - - // $this->notifyCCs($firstStep, $instance, 'pending'); - - $this->pushTodoChanged($approverIds); - - 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', '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; - $round = (int)$instance->round; - $step = $steps[$idx] ?? null; - if (!$step) throw new BusinessException("当前步骤配置错误"); - - // 找到该用户在本步的 pending 记录(加锁) - $pendingRecord = Db::table('approval_records') - ->where('instance_id', $instanceId) - ->where('round', $round) - ->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('round', $round) - ->where('approver_id', $approverId) - ->where('step_index', $idx) - ->first(); - if ($anyRecord && $anyRecord->status !== 'pending') { - return ['status' => 'noop', 'msg' => '已处理']; - } - // 步骤已推进 - if ((int)$instance->current_step !== $idx) { - return ['status' => 'noop', 'msg' => '非当前审批']; - } - 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, - ]); - - // 异步审批回调 9.23 add - $approvalcallback = [ - 'biz_id' => $instance->biz_id, - 'flow_type' => $instance->flow_type, - 'instance_id' => $instance->id, - 'initiator_id' => $instance->initiator_id, - 'status' => $action - ]; - \Webman\RedisQueue\Redis::send('approval-callback', $approvalcallback); - - // 2) 聚合本步结论(any / all) - $typeRaw = strtolower($step['type'] ?? 'all'); - $type = in_array($typeRaw, ['any','all']) ? $typeRaw : 'all'; - $outcome = $this->getStepOutcome($instanceId, $round, $idx, $type); // 'pending' | 'approve' | 'reject' - - if ($outcome === 'pending') { - $this->pushTodoChanged([$approverId]); - return ['status' => 'step_pending', 'msg' => '审批待定']; - } - - $otherPendingUids = $this->getPendingApproverIds($instanceId, $round, $idx); - - // 3) 有结论:关闭本步其余 pending - $this->closeOtherPending($instanceId, $round, $idx, [$approverId]); - - // 这些人 pending→skipped,待办数减少 - $this->pushTodoChanged($otherPendingUids); - - // 4) 拒绝 => 结束 - if ($outcome === 'reject') { - Db::table('approval_instances')->where('id', $instanceId)->update([ - 'status' => 'rejected', - 'finished_at' => $now, - 'updated_at' => $now, - ]); - $this->onFlowFinalized($instance, 'rejected'); - $this->sendNotification([$instance->initiator_id], $instance, 'reject', false); - $this->pushTodoChanged([$approverId]); - return ['status' => 'completed', 'msg' => '拒绝']; - } - - // 5) 通过 => 进入下一步或结束 - $formData = json_decode($instance->form_data, true) ?: []; - $nextIndex = $this->getNextStepIndex($steps, $formData, $idx); - - if ($nextIndex === null) { - $finalStatus = $this->normalizeFinalStatus((string)$instance->flow_type, 'approved'); - $notifyAction = ($finalStatus === 'voided') ? 'voided' : 'approve'; - - Db::table('approval_instances')->where('id', $instanceId)->update([ - 'status' => $finalStatus, - 'finished_at' => $now, - 'updated_at' => $now, - ]); - $this->onFlowFinalized($instance, $finalStatus); - - // 新增:在流程最终完成时才发本步骤的抄送 - if (!empty($step['cc'])) { - $this->notifyCCs($step, $instance, $notifyAction); - } - - $this->sendNotification([$instance->initiator_id], $instance, $notifyAction, false); - $this->pushTodoChanged([$approverId]); - return ['status' => 'completed', 'msg' => ($finalStatus === 'voided' ? '已作废' : '同意')]; - } - - // 乐观推进 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', 'msg' => '并发进展']; - } - - // 生成下一步待办 + 通知 - $nextStep = $steps[$nextIndex]; - $nextApprovers = $this->resolveApprovers($nextStep, $instance, true); - foreach ($nextApprovers as $uid) { - $this->insertApprovalRecord($instance, $nextIndex, $uid); - } - $this->sendNotification($nextApprovers, $instance, 'pending', false); - // $this->notifyCCs($nextStep, $instance, 'pending'); - $this->pushTodoChanged(array_merge([$approverId], $nextApprovers)); - return ['status' => 'next_step', 'msg' => '进行下一步:'.$nextStep['name'] ?? ('Step '.$nextIndex)]; - }); - } - - // 流程完结后的业务回调(approved|rejected|revoked|timeout?) - private function onFlowFinalized(object $instance, string $final): void - { - try { - switch ($instance->flow_type) { - case 'stock_orders': { - $order = \app\common\model\StockOrders::find($instance->biz_id); - if (!$order) { - throw new BusinessException('订单不存在'); - } - if ($final === 'approved') { - $order->approval_status = 2; - $order->save(); - } elseif ($final === 'rejected') { - $order->approval_status = 3; - $order->save(); - } elseif ($final === 'revoked') { - $order->approval_status = 0; - $order->save(); - } - break; - } - case 'stock_orders_nullify': - if ($final === 'approved') { - $final = 'voided'; - Db::table('approval_instances')->where('id', $instance->id)->update([ - 'status' => 'voided', - ]); - } - if (in_array($final, ['voided','approved'], true)) { - $order = \app\common\model\StockOrders::find($instance->biz_id); - if ($order && $order->approval_status !== 4) { - $order->approval_status = 4; - $order->save(); - } - } - break; - default: - // 非作废流程,无需处理 - break; - } - } catch (\Throwable $e) { - Log::error('onFlowFinalized error: '.$e->getMessage()); - } - } - - - /** 撤回(仅发起人在无已处理记录前可撤回) */ - 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, - ]); - - $this->onFlowFinalized($instance, 'revoked'); - - $pendingUids = Db::table('approval_records') - ->where('instance_id', $instanceId) - ->where('status', 'pending') - ->pluck('approver_id')->toArray(); - - 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); - $this->pushTodoChanged($pendingUids); - return ['status' => 'completed', 'result' => 'revoked']; - }); - } - - /** 流程图数据(把 skipped/timeout 视作已完结的中性状态) */ - public function getFlowDiagramData(int $instanceId): array - { - $instance = Db::table('approval_instances')->find($instanceId); - if (!$instance) return []; - - $flow = Db::table('approval_flows')->where('type', $instance->flow_type)->first(); - if (!$flow) return []; - - $steps = json_decode($flow->steps, true) ?: []; - $records = Db::table('approval_records') - ->where('instance_id', $instanceId) - ->where('round', (int)$instance->round) - ->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'; - $final = $this->normalizeFinalStatus((string)$instance->flow_type, $final); - $finalAction = ($final === 'voided') ? 'voided' : (($final === 'approved') ? 'approve' : 'reject'); - - Db::table('approval_instances')->where('id', $instance->id)->update([ - 'status' => $final, - 'finished_at' => $now, - 'updated_at' => $now, - ]); - - $this->onFlowFinalized((object)$instance, $final); - - $pendingUids = Db::table('approval_records') - ->where('instance_id', $instance->id) - ->where('round', (int)$instance->round) - ->where('step_index', $instance->current_step) - ->where('status', 'pending') - ->pluck('approver_id')->toArray(); - - Db::table('approval_records') - ->where('instance_id', $instance->id) - ->where('round', (int)$instance->round) - ->where('step_index', $instance->current_step) - ->where('status', 'pending') - ->update(['status' => 'timeout', 'approved_at' => $now, 'updated_at' => $now]); - - $this->notifyInitiator($instance, $finalAction); - - $this->pushTodoChanged($pendingUids); - }); - } - } - - // ---------- 内部工具 ---------- - - /** 解析审批人(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; - - case 'group_members': { - // $gid = (int)($cfg['group_id'] ?? 0); - $gid = (int)($cfg['value'] ?? 0); - if ($gid <= 0) throw new BusinessException('group_members 需要 group_id'); - $ids = $this->getGroupMemberIds($gid); // A 方案 - break; - } - - case 'scenario_leader_of_initiator': { - $formData = json_decode($instance->form_data ?? '{}', true) ?: []; - // $scenario = $cfg['scenario'] ?? ($formData[$cfg['scenario_field']] ?? null); - $scenario = $val ?? ($formData[$cfg['scenario_field']] ?? null); - if (!$scenario) throw new BusinessException('缺少业务场景(scenario 或 scenario_field)'); - - $mustBelong = array_key_exists('must_belong', $cfg) ? (bool)$cfg['must_belong'] : true; - $preferPrimary = array_key_exists('prefer_primary', $cfg) ? (bool)$cfg['prefer_primary'] : true; - - $ids = $this->getLeadersByScenarioForInitiator( - (int)$instance->initiator_id, - (string)$scenario, - $mustBelong, - $preferPrimary - ); // B 方案(按场景) - 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; - case 'group_members': { - $gid = (int)($val ?? 0); - if ($gid <= 0) throw new BusinessException('group_members 需要 group_id'); - $uids = $this->getGroupMemberIds($gid); // A 方案 - break; - } - case 'scenario_leader_of_initiator': { - $formData = json_decode($instance->form_data ?? '{}', true) ?: []; - $scenario = $val ?? ($formData[$cc['scenario_field']] ?? null); - if (!$scenario) throw new BusinessException('缺少业务场景(scenario 或 scenario_field)'); - - $mustBelong = array_key_exists('must_belong', $cc) ? (bool)$cc['must_belong'] : true; - $preferPrimary = array_key_exists('prefer_primary', $cc) ? (bool)$cc['prefer_primary'] : true; - - $uids = $this->getLeadersByScenarioForInitiator( - (int)$instance->initiator_id, - (string)$scenario, - $mustBelong, - $preferPrimary - ); // B 方案(按场景) - 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; - } - - /** - * 聚合本步骤结论(按类型) - */ - private function getStepOutcome(int $instanceId, int $round, int $stepIndex, string $type): string - { - $records = Db::table('approval_records') - ->where('instance_id', $instanceId) - ->where('round', $round) // - ->where('step_index', $stepIndex) - ->get(); - - $pending = $approved = $rejected = $timeout = $skipped = 0; - foreach ($records as $r) { - switch ($r->status) { - case 'pending': $pending++; break; - case 'approved': $approved++; break; - case 'rejected': $rejected++; break; - case 'timeout': $timeout++; break; - case 'skipped': $skipped++; break; - } - } - - $type = strtolower($type); - if ($type === 'any') { - if ($rejected > 0) return 'reject'; - if ($approved > 0) return 'approve'; - // 全为 skipped/timeout 且无 pending/rejected ⇒ 通过 - if ($pending === 0 && $rejected === 0 && $approved === 0) return 'approve'; - return 'pending'; - } - if ($rejected > 0) return 'reject'; - if ($pending > 0) return 'pending'; - return 'approve'; - } - - /** 关闭本步其余待办为 skipped */ - private function closeOtherPending(int $instanceId, int $round, int $stepIndex, array $keepApproverIds = []): void - { - $now = date('Y-m-d H:i:s'); - $q = Db::table('approval_records') - ->where('instance_id', $instanceId) - ->where('round', $round) // - ->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(object $instance, int $stepIndex, int $approverId): void - { - $round = (int)($instance->round ?? 1); - $now = date('Y-m-d H:i:s'); - - $exists = Db::table('approval_records') - ->where('instance_id', (int)$instance->id) - ->where('round', $round) // 限当前轮 - ->where('step_index', $stepIndex) - ->where('approver_id', $approverId) - ->where('status', 'pending') - ->exists(); - - if ($exists) return; - - Db::table('approval_records')->insert([ - 'instance_id' => (int)$instance->id, - 'approver_id' => $approverId, - 'step_index' => $stepIndex, - 'round' => $round, // - '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, array $extra = []): void - { - $map = [ - 'pending' => '待审批', - 'approve' => '已通过', - 'reject' => '已拒绝', - 'revoked' => '已撤回', - 'voided' => '已作废', - ]; - $prefix = $isCC ? '【抄送】' : '【审批】'; - $text = $map[$action] ?? $action; - - // 业务标题/内容 - $approvalType = array_flip(\app\common\service\SysDictService::pluckKeyValueByCode('approval_type')); - - $flowTxt = $approvalType[$instance->flow_type] ?? (string)$instance->flow_type; - $title = $prefix.'审批通知'; - $content = "{$prefix}你有一条审批 {$text}:类型 {$flowTxt},业务标识 {$instance->biz_id}"; - - switch ($instance->flow_type) { - case 'stock_orders': - // 备货单 - $stockOrder = \app\common\model\StockOrders::find($instance->biz_id); - if ($stockOrder) { - $title = $prefix."备货单:{$stockOrder->order_no}"; - $flowTxt = $approvalType[$instance->flow_type] ?? $instance->flow_type; - $content = "{$prefix}你有一条审批 {$text}:类型 {$flowTxt},业务单号 {$stockOrder->order_no}"; - } - break; - case 'stock_orders_nullify': - // 备货单作废 - $stockOrder = \app\common\model\StockOrders::find($instance->biz_id); - if ($stockOrder) { - $title = $prefix."备货单作废:{$stockOrder->order_no}"; - $flowTxt = $approvalType[$instance->flow_type] ?? '备货单作废'; - $content = "{$prefix}你有一条审批 {$text}:类型 {$flowTxt},业务单号 {$stockOrder->order_no}"; - // 把作废原因附带展示更清晰(也可依赖 $extra['note']) - $fd = json_decode($instance->form_data ?? '{}', true) ?: []; - if (!empty($fd['_nullify_reason'])) { - $content .= ';原因:'.(function_exists('mb_strimwidth') ? mb_strimwidth($fd['_nullify_reason'], 0, 120, '...', 'UTF-8') : $fd['_nullify_reason']); - } - } - break; - case 'purchase_orders': - // 采购单 - $purchaseOrder = \app\common\model\PurchaseOrders::find($instance->biz_id); - if ($purchaseOrder) { - $title = $prefix."采购单:{$purchaseOrder->order_no}"; - $flowTxt = $approvalType[$instance->flow_type] ?? $instance->flow_type; - $content = "{$prefix}你有一条审批 {$text}:类型 {$flowTxt},业务单号 {$purchaseOrder->order_no}"; - } - break; - case 'purchase_order': - // 采购单 - break; - } - - // 附加说明(提交说明/申诉/讨论摘要) - $append = $extra['appeal'] ?? ($extra['note'] ?? null); - if ($append) { - if (function_exists('mb_strimwidth')) { - $append = mb_strimwidth($append, 0, 120, '...', 'UTF-8'); - } - $content .= ";说明:{$append}"; - } - - noticePush([ - 'title' => $title, - 'content' => $content, - 'recipient' => $userIds, - 'email' => 1, - ]); - } - - - private function notifyInitiator(object $instance, string $result): void - { - $this->sendNotification([(int)$instance->initiator_id], $instance, $result, false); - } - - // ---------- 数据访问(roles / user_roles 结构) ---------- - - /** - * 从角色取用户ID - * @param mixed $roleIdOrName 数字 => role_id;字符串 => roles.name - * @param int|null $systemId 限定 roles.system_id;null 不限定 - * @param bool $onlyActive 仅取 status=1 角色,默认 true - * @param bool $includeGlobal 是否包含全局角色(system_id=0),默认 true - */ - private function getUserIdsByRole($roleIdOrName, ?int $systemId = null, bool $onlyActive = true, bool $includeGlobal = true): array - { - if (is_int($roleIdOrName) || (is_string($roleIdOrName) && ctype_digit($roleIdOrName))) { - $roleId = (int)$roleIdOrName; - $needJoin = $onlyActive || !is_null($systemId) || $includeGlobal === false; - - if (!$needJoin) { - $rows = Db::table('user_roles')->where('role_id', $roleId)->select('user_id')->get(); - } else { - $q = Db::table('user_roles as ur') - ->join('roles as r', 'ur.role_id', '=', 'r.id') - ->where('ur.role_id', $roleId) - ->select('ur.user_id'); - if ($onlyActive) $q->where('r.status', 1); - if (!is_null($systemId)) { - $includeGlobal ? $q->whereIn('r.system_id', [0, $systemId]) : $q->where('r.system_id', $systemId); - } - $rows = $q->get(); - } - } else { - $name = (string)$roleIdOrName; - $q = Db::table('user_roles as ur') - ->join('roles as r', 'ur.role_id', '=', 'r.id') - ->where('r.name', $name) - ->select('ur.user_id'); - if ($onlyActive) $q->where('r.status', 1); - if (!is_null($systemId)) { - $includeGlobal ? $q->whereIn('r.system_id', [0, $systemId]) : $q->where('r.system_id', $systemId); - } - $rows = $q->get(); - } - - $ids = []; - foreach ($rows as $row) $ids[] = (int)$row->user_id; - return array_values(array_unique($ids)); - } - - private function getLeaderIdByUserId(int $userId): ?int - { - $id = Db::table('user')->where('id', $userId)->value('report_to'); - return $id ? (int)$id : null; - } - - private function getDeptHeadId(int $deptId): ?int - { - $id = Db::table('departments')->where('id', $deptId)->value('head_user_id'); - return $id ? (int)$id : null; - } - - - /** - * 查询某个审批实例的审批记录(附带步骤名/类型,可选带出审批人姓名) - * - * @param int $instanceId - * @param string|null $userNameColumn 可选:如 'name' 或 'nickname'。传 null 则不 join 用户表 - * @return array{ - * instance: array, - * steps: array, - * records: array, - * summary: array{approved:int,rejected:int,pending:int,skipped:int,timeout:int,total:int} - * } - */ - public function getInstanceRecords(int $instanceId, ?string $userNameColumn = null): array - { - // 1) 实例 + 流程步骤 - $instance = Db::table('approval_instances')->where('id', $instanceId)->first(); - if (!$instance) { - throw new BusinessException('审批实例未找到'); - } - $flow = Db::table('approval_flows')->where('type', $instance->flow_type)->first(); - if (!$flow) { - throw new BusinessException('审批流程定义未找到'); - } - $steps = json_decode($flow->steps, true) ?: []; - - // 2) 记录查询(可选带出审批人姓名) - if ($userNameColumn) { - // 注意:确保 user 表存在该列 - $records = Db::table('approval_records as ar') - ->leftJoin('user as u', 'ar.approver_id', '=', 'u.id') - ->where('ar.instance_id', $instanceId) - ->orderBy('ar.step_index') - ->orderByRaw('COALESCE(ar.approved_at, ar.created_at) asc') - ->select( - 'ar.*', - Db::raw("u.`{$userNameColumn}` as approver_name") - ) - ->get() - ->toArray(); - } else { - $records = Db::table('approval_records as ar') - ->where('ar.instance_id', $instanceId) - ->orderBy('ar.step_index') - ->orderByRaw('COALESCE(ar.approved_at, ar.created_at) asc') - ->select('ar.*') - ->get() - ->toArray(); - } - - // 3) 附加步骤名/类型;统计汇总 - $summary = ['approved'=>0,'rejected'=>0,'pending'=>0,'skipped'=>0,'timeout'=>0,'total'=>0]; - foreach ($records as &$r) { - $idx = (int)$r->step_index; - $meta = $steps[$idx] ?? []; - $r->step_name = $meta['name'] ?? ('Step '.$idx); - $r->step_type = $meta['type'] ?? 'all'; - - if (isset($summary[$r->status])) { - $summary[$r->status]++; - } - $summary['total']++; - } - unset($r); - - return [ - 'instance' => [ - 'id' => (int)$instance->id, - 'flow_type' => $instance->flow_type, - 'biz_type' => $instance->biz_type, - 'biz_id' => $instance->biz_id, - 'status' => $instance->status, - 'current_step' => (int)$instance->current_step, - 'created_at' => $instance->created_at, - 'updated_at' => $instance->updated_at, - 'finished_at' => $instance->finished_at, - ], - 'steps' => array_map(function($s){ - return [ - 'name' => $s['name'] ?? '', - 'type' => $s['type'] ?? 'all', - 'condition' => $s['condition'] ?? null, - ]; - }, $steps), - 'records' => array_map(function($r){ - // 转成数组(Illuminate 返回的是 stdClass) - return [ - 'id' => (int)$r->id, - 'instance_id' => (int)$r->instance_id, - 'approver_id' => (int)$r->approver_id, - 'step_index' => (int)$r->step_index, - 'action' => $r->action, - 'status' => $r->status, - 'comment' => $r->comment, - 'approved_at' => $r->approved_at, - 'created_at' => $r->created_at, - 'updated_at' => $r->updated_at, - 'approver_name'=> property_exists($r, 'approver_name') ? $r->approver_name : null, - 'step_name' => $r->step_name, - 'step_type' => $r->step_type, - ]; - }, $records), - 'summary' => $summary, - ]; - } - - - 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; - $round = (int)$instance->round; - - // 申请人 - $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) - ->where('ar.round', $round) - ->orderBy('ar.step_index') - ->orderByRaw('COALESCE(ar.approved_at, ar.created_at) asc') - ->select('ar.*') - ->get() - ->toArray(); - - // 一次性拉取姓名,避免 N+1 - $nameMap = []; - if ($userNameColumn && !empty($records)) { - $uids = []; - foreach ($records as $r) $uids[] = (int)$r->approver_id; - $uids = array_values(array_unique($uids)); - if (!empty($uids)) { - $pairs = Db::table('user')->whereIn('id', $uids)->pluck($userNameColumn, 'id')->toArray(); - foreach ($pairs as $uid => $name) $nameMap[(int)$uid] = $name; - } - } - - // 记录分组 - $byStep = []; - foreach ($records as $r) { - $idx = (int)$r->step_index; - $byStep[$idx][] = $r; - } - - $timeline = []; - - // 可选:发起申请块 - 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; - - // 每一步的“审批意见”(只展示有内容的 comment) - $opinions = []; // [{approver_id, approver_name?, action, status, comment, time}...] - - 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) { - // 收集“审批意见”(非空 comment 都算) - $cmt = trim((string)($rr->comment ?? '')); - if ($cmt !== '') { - $opinions[] = [ - 'approver_id' => (int)$rr->approver_id, - 'approver_name' => $userNameColumn ? ($nameMap[(int)$rr->approver_id] ?? null) : null, - 'action' => $rr->action, // approve | reject | null - 'status' => $rr->status, // approved|rejected|pending|skipped|timeout - 'comment' => $cmt, - 'time' => $rr->approved_at ?: $rr->created_at, - ]; - } - - 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]++; - } - - // 时间升序排序 opinions - if (!empty($opinions)) { - usort($opinions, function($a, $b) { - return strcmp($a['time'] ?? '', $b['time'] ?? ''); - }); - } - - $startedAt = $minCreated; - - // 总结果(注意:这里按你现在的签名调用,含 round) - $outcome = $this->getStepOutcome($instanceId, $round, $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, - 'opinions' => $opinions, // 这就是每个审批意见 - ]; - - // 把申请人信息并到第一步(可选) - 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; - $round = (int)$instance->round; - $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.round', $round) - ->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('round', $round) - ->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('round', $round) - ->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; - } - - // ====== ====== ====== ====== ====== ====== 重提操作 && 讨论 ====== ====== ====== ====== ====== ====== - - /** - * 重提交(仅对 status=rejected 的实例;支持申诉说明) - * - 合并 $patchData 到 form_data,并设置 resubmitted=1 - * - 若 $appealText 非空:form_data.appeal_last = $appealText(可用于前端展示) - * - 重新计算首个命中的步骤并创建待办;通知审批人且带上申诉说明 - */ - public function resubmit(int $instanceId, int $initiatorId, array $patchData = [], string $appealText = ''): array - { - return Db::transaction(function () use ($instanceId, $initiatorId, $patchData, $appealText) { - $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 !== 'rejected') 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) ?: []; - - // 合并改动 & 打上重提标记 - $formData = array_merge($formData, $patchData); - $formData['resubmitted'] = 1; - - $appealText = trim((string)$appealText); - if ($appealText !== '') { - $formData['appeal_last'] = $appealText; - } - - // 计算重提后首个命中的步骤 - $firstIndex = $this->getNextStepIndex($steps, $formData, -1); - if ($firstIndex === null) throw new BusinessException('重提后未找到可执行的步骤'); - - $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); - } - - $oldPendingUids = Db::table('approval_records') - ->where('instance_id', $instanceId) - ->where('status', 'pending') - ->pluck('approver_id')->toArray(); - - // 清理可能遗留的 pending - Db::table('approval_records') - ->where('instance_id', $instanceId) - ->where('status', 'pending') - ->update(['status'=>'skipped','approved_at'=>$now,'updated_at'=>$now]); - - // 轮次 +1 - $newRound = ((int)$instance->round) + 1; - - // 重新开启实例 - Db::table('approval_instances')->where('id', $instanceId)->update([ - 'status' => 'processing', - 'current_step' => $firstIndex, - 'round' => $newRound, - 'form_data' => json_encode($formData, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), - 'finished_at' => null, - 'deadline_at' => $deadlineAt, - 'updated_at' => $now, - ]); - - // 创建待办 + 通知 - $firstStep = $steps[$firstIndex]; - $instanceLite = (object)[ - 'id' => (int)$instanceId, - 'flow_type' => (string)$instance->flow_type, - 'form_data' => json_encode($formData, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), - 'initiator_id' => (int)$instance->initiator_id, - 'biz_type' => (string)$instance->biz_type, - 'biz_id' => (string)$instance->biz_id, - 'round' => $newRound, - ]; - $approvers = $this->resolveApprovers($firstStep, $instanceLite, true); - if (empty($approvers)) throw new BusinessException('重提后首步未解析到审批人'); - - foreach ($approvers as $uid) { - $this->insertApprovalRecord($instanceLite, $firstIndex, (int)$uid); - } - - $extra = $appealText !== '' ? ['appeal' => $appealText] : []; - $this->sendNotification($approvers, $instanceLite, 'pending', false, $extra); - $this->notifyCCs($firstStep, $instanceLite, 'pending'); - $this->pushTodoChanged(array_merge($oldPendingUids, $approvers)); - - return [ - 'status' => 'resubmitted', - 'instance_id' => (int)$instanceId, - 'current_step' => $firstIndex, - 'step_name' => $firstStep['name'] ?? ('Step '.$firstIndex), - ]; - }); - } - - - /** 是否该实例参与者(发起人/任一步审批人/抄送人) */ - private function isParticipantOfInstance(int $instanceId, int $userId): bool - { - $inst = Db::table('approval_instances')->where('id', $instanceId)->first(); - if (!$inst) return false; - if ((int)$inst->initiator_id === $userId) return true; - - $inRecords = Db::table('approval_records') - ->where('instance_id', $instanceId)->where('approver_id', $userId)->exists(); - if ($inRecords) return true; - - $inCc = Db::table('approval_ccs') - ->where('instance_id', $instanceId)->where('user_id', $userId)->exists(); - return $inCc; - } - - /** 是否该实例任一步的审批人 */ - private function isApproverOfInstance(int $instanceId, int $userId): bool - { - return Db::table('approval_records') - ->where('instance_id', $instanceId) - ->where('approver_id', $userId) - ->exists(); - } - - /** 获取讨论受众:公开=全体参与者;内部=仅审批人(排除自己) */ - private function getDiscussionAudienceUserIds(int $instanceId, bool $approversOnly, int $excludeUserId): array - { - $uids = []; - if (!$approversOnly) { - $inst = Db::table('approval_instances')->where('id', $instanceId)->first(); - if ($inst) $uids[] = (int)$inst->initiator_id; - $ccs = Db::table('approval_ccs')->where('instance_id', $instanceId)->pluck('user_id')->toArray(); - foreach ($ccs as $u) $uids[] = (int)$u; - } - $approvers = Db::table('approval_records') - ->where('instance_id', $instanceId) - ->distinct()->pluck('approver_id')->toArray(); - foreach ($approvers as $u) $uids[] = (int)$u; - - $uids = array_values(array_unique(array_filter($uids, fn($u) => $u !== $excludeUserId))); - return $uids; - } - - /** - * 发表讨论 - * @param int $instanceId - * @param int $userId - * @param string $content - * @param bool $isInternal true=仅审批人可见;false=所有参与者可见 - * @param array $attachments 可选附件数组 - * @param string|null $userNameColumn 通知里带发言人姓名(如 'name'),不需要传 null - */ - public function postDiscussion( - int $instanceId, - int $userId, - string $content, - bool $isInternal = false, - array $attachments = [], - ?string $userNameColumn = null - ): array { - $content = trim($content); - if ($content === '') throw new BusinessException('讨论内容不能为空'); - if (!$this->isParticipantOfInstance($instanceId, $userId)) { - throw new BusinessException('无权参与该实例的讨论'); - } - if ($isInternal && !$this->isApproverOfInstance($instanceId, $userId)) { - throw new BusinessException('仅审批人可发布内部讨论'); - } - - $now = date('Y-m-d H:i:s'); - $id = Db::table('approval_discussions')->insertGetId([ - 'instance_id' => $instanceId, - 'user_id' => $userId, - 'is_internal' => $isInternal ? 1 : 0, - 'content' => $content, - 'attachments' => empty($attachments) ? null : json_encode($attachments, JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES), - 'created_at' => $now, - 'updated_at' => $now, - ]); - - // 通知其他人 - $inst = Db::table('approval_instances')->where('id', $instanceId)->first(); - if ($inst) { - $audience = $this->getDiscussionAudienceUserIds($instanceId, $isInternal, $userId); - // 发言人姓名(可选) - $speaker = (string)$userId; - if ($userNameColumn) { - $name = Db::table('user')->where('id', $userId)->value($userNameColumn); - if ($name) $speaker = $name; - } - $snippet = function_exists('mb_strimwidth') ? mb_strimwidth($content, 0, 80, '...', 'UTF-8') : $content; - $extra = ['note' => "【讨论】{$speaker}:{$snippet}"]; - - $this->sendNotification( - $audience, - (object)[ - 'id' => (int)$inst->id, - 'flow_type' => (string)$inst->flow_type, - 'initiator_id' => (int)$inst->initiator_id, - 'form_data' => $inst->form_data, - 'biz_type' => (string)$inst->biz_type, - 'biz_id' => (string)$inst->biz_id, - ], - 'pending', - false, - $extra - ); - } - - return [ - 'id' => (int)$id, - 'instance_id' => $instanceId, - 'user_id' => $userId, - 'is_internal' => (bool)$isInternal, - 'content' => $content, - 'attachments' => $attachments, - 'created_at' => $now, - ]; - } - - /** - * 获取讨论列表 - * @param int $instanceId - * @param int|null $afterId 翻页:取 id > afterId 的新消息 - * @param int $limit - * @param string|null $userNameColumn 带出用户名(如 'name'),不需要可传 null - */ - public function listDiscussions(int $instanceId, ?int $afterId = null, int $limit = 50, ?string $userNameColumn = null): array - { - $q = Db::table('approval_discussions as d')->where('d.instance_id', $instanceId); - if ($afterId) $q->where('d.id', '>', $afterId); - $q->orderBy('d.id', 'asc')->limit(max(1, min(200, $limit))); - - if ($userNameColumn) { - $rows = $q->leftJoin('user as u', 'd.user_id', '=', 'u.id') - ->select('d.*', Db::raw("u.`{$userNameColumn}` as user_name")) - ->get()->toArray(); - } else { - $rows = $q->select('d.*')->get()->toArray(); - } - - return array_map(function($r){ - return [ - 'id' => (int)$r->id, - 'instance_id' => (int)$r->instance_id, - 'user_id' => (int)$r->user_id, - 'is_internal' => (bool)$r->is_internal, - 'content' => $r->content, - 'attachments' => $r->attachments ? json_decode($r->attachments, true) : [], - 'created_at' => $r->created_at, - 'updated_at' => $r->updated_at, - 'user_name' => property_exists($r, 'user_name') ? $r->user_name : null, - ]; - }, $rows); - } - - // ====== ====== ====== ====== ====== ====== 转交操作 ====== ====== ====== ====== ====== ====== - - /** - * 查看某个实例的转交流水(按时间升序) - */ - public function listTransfers(int $instanceId): array - { - $rows = Db::table('approval_transfers') - ->where('instance_id', $instanceId) - ->orderBy('id', 'asc') - ->get() - ->toArray(); - - return array_map(function($r){ - return [ - 'id' => (int)$r->id, - 'instance_id' => (int)$r->instance_id, - 'step_index' => (int)$r->step_index, - 'record_id' => (int)$r->record_id, - 'from_user_id' => (int)$r->from_user_id, - 'to_user_id' => (int)$r->to_user_id, - 'reason' => $r->reason, - 'created_at' => $r->created_at, - ]; - }, $rows); - } - - - /** - * 转交当前待办给他人(原地替换 approver_id)并在通知里追加“来源/原因” - */ - public function transferApproval( - int $instanceId, - int $stepIndex, - int $fromUserId, - int $toUserId, - string $reason = '', - ?string $userNameColumn = null // 可选:传 'name' 或 'nickname' 来展示姓名 - ): array { - if ($fromUserId === $toUserId) { - return ['status' => 'noop', 'msg' => '目标与来源相同,无需转交']; - } - - return \support\Db::transaction(function () use ($instanceId, $stepIndex, $fromUserId, $toUserId, $reason, $userNameColumn) { - // 锁实例,避免并发推进 - $instance = Db::table('approval_instances')->where('id', $instanceId)->lockForUpdate()->first(); - if (!$instance) throw new BusinessException('审批实例未找到'); - if ($instance->status !== 'processing') throw new BusinessException('流程非进行中,无法转交'); - - // 锁定来源人的 pending 记录 - $record = Db::table('approval_records') - ->where('instance_id', $instanceId) - ->where('round', (int)$instance->round) - ->where('step_index', $stepIndex) - ->where('approver_id', $fromUserId) - ->where('status', 'pending') - ->lockForUpdate() - ->first(); - if (!$record) throw new BusinessException('未找到可转交的待办或已处理'); - - $now = date('Y-m-d H:i:s'); - - // 若目标已有 pending:合并(本记录置为 skipped) - $target = Db::table('approval_records') - ->where('instance_id', $instanceId) - ->where('round', (int)$instance->round) - ->where('step_index', $stepIndex) - ->where('approver_id', $toUserId) - ->where('status', 'pending') - ->lockForUpdate() - ->first(); - - if ($target) { - Db::table('approval_records')->where('id', $record->id)->update([ - 'status' => 'skipped', - 'approved_at' => $now, - 'updated_at' => $now, - 'comment' => trim(($record->comment ?? '')."\n[转交给 {$toUserId},目标已有待办,已合并]"), - ]); - $effectiveRecordId = (int)$target->id; - } else { - // 原地替换 approver_id - Db::table('approval_records')->where('id', $record->id)->update([ - 'approver_id' => $toUserId, - 'updated_at' => $now, - 'comment' => trim(($record->comment ?? '')."\n[自 {$fromUserId} 转交]"), - ]); - $effectiveRecordId = (int)$record->id; - } - - // 组织“转交来源/原因”文案(优先姓名) - $fromLabel = (string)$fromUserId; - $toLabel = (string)$toUserId; - if ($userNameColumn) { - $fromName = Db::table('user')->where('id', $fromUserId)->value($userNameColumn); - $toName = Db::table('user')->where('id', $toUserId)->value($userNameColumn); - if ($fromName) $fromLabel = $fromName; - if ($toName) $toLabel = $toName; - } - $note = "【转交】{$fromLabel} → {$toLabel}"; - $r = trim($reason); - if ($r !== '') $note .= ",原因:{$r}"; - - // 通知被转交人(在内容里追加 note) - $this->sendNotification([$toUserId], (object)[ - 'id' => (int)$instance->id, - 'flow_type' => (string)$instance->flow_type, - 'biz_type' => (string)$instance->biz_type, - 'biz_id' => (string)$instance->biz_id, - 'initiator_id' => (int)$instance->initiator_id, - 'form_data' => $instance->form_data, - ], 'pending', false, ['note' => $note]); - - // 记转交流水 - Db::table('approval_transfers')->insert([ - 'instance_id' => (int)$instanceId, - 'step_index' => (int)$stepIndex, - 'record_id' => $effectiveRecordId, - 'from_user_id' => (int)$fromUserId, - 'to_user_id' => (int)$toUserId, - 'reason' => $reason, - 'created_at' => $now, - ]); - - $this->pushTodoChanged([$fromUserId, $toUserId]); - return ['status' => 'ok', 'record_id' => $effectiveRecordId]; - }); - } - - - - /** 新增:发起“作废审批” */ - public function startNullify( - string $voidFlowType, // 例如 'stock_orders_nullify' - string $bizType, // 原业务类型,如 'stock_orders' - string $bizId, // 原业务ID - array $formData, // 可包含 ['nullify_reason'=>...] - int $initiatorId - ): array { - return Db::transaction(function () use ($voidFlowType, $bizType, $bizId, $formData, $initiatorId) { - // 1) 流程定义校验 - $flow = Db::table('approval_flows')->where('type', $voidFlowType)->where('is_active', 1)->first(); - if (!$flow) throw new BusinessException("作废流程未找到或已停用: {$voidFlowType}"); - $steps = json_decode($flow->steps, true); - if (!is_array($steps) || empty($steps)) throw new BusinessException("作废流程步骤配置错误"); - - // 2) 必须存在“已通过”的最新有效实例(非作废流程) - $latestApproved = Db::table('approval_instances') - ->where('biz_type', $bizType) - ->where('biz_id', $bizId) - ->where('flow_type', '!=', $voidFlowType) - ->where('status', 'approved') - ->orderByDesc('id') - ->first(); - - if (!$latestApproved) { - throw new BusinessException('仅已通过的单据可发起作废'); - } - - // 3) 不允许并行的作废流程 - $voidProcessing = Db::table('approval_instances') - ->where('flow_type', $voidFlowType) - ->where('biz_id', $bizId) - ->where('status', 'processing') - ->exists(); - if ($voidProcessing) { - throw new BusinessException('已存在进行中的作废审批'); - } - - // A. 基于历史“作废流程已通过”的实例拦截 - $voidApproved = Db::table('approval_instances') - ->where('flow_type', $voidFlowType) - ->where('biz_id', $bizId) - ->whereIn('status', ['approved','voided']) - ->exists(); - if ($voidApproved) { - throw new BusinessException('该单据已作废,无需重复作废'); - } - - // B.(可选)直接查业务表状态拦截(若你用 approval_status=4 表示已作废) - /* - $orderVoided = \app\common\model\StockOrders::where('id', $bizId) - ->where('approval_status', 4) - ->exists(); - if ($orderVoided) { - throw new BusinessException('该单据已作废,无需重复作废'); - } - */ - - // 4) 计算首步 - // 注:作废流程可以有独立条件;若不需要条件,直接第一步 - $formData = array_merge([ - '_op' => 'nullify', - '_nullify_reason' => $formData['nullify_reason'] ?? ($formData['reason'] ?? ''), - '_nullify_of_instance_id' => (int)$latestApproved->id, - ], $formData); - - $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); - } - - // 5) 新建作废实例(round 固定从 1 开始) - $instanceId = Db::table('approval_instances')->insertGetId([ - 'flow_type' => $voidFlowType, - '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, - 'round' => 1, // - 'deadline_at' => $deadlineAt, - 'created_at' => $now, - 'updated_at' => $now, - ]); - - $instance = (object)[ - 'id' => $instanceId, - 'flow_type' => $voidFlowType, - 'biz_type' => $bizType, - 'biz_id' => $bizId, - 'form_data' => json_encode($formData, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), - 'initiator_id' => $initiatorId, - 'current_step' => $firstIndex, - 'status' => 'processing', - 'round' => 1, // - ]; - - // 6) 生成首步待办 + 通知(带上作废原因) - $approverIds = $this->resolveApprovers($firstStep, $instance, true); - foreach ($approverIds as $uid) { - $this->insertApprovalRecord($instance, $firstIndex, $uid); - } - $extra = []; - if (!empty($formData['_nullify_reason'])) { - $extra['note'] = '【作废原因】'.$formData['_nullify_reason']; - } - $this->sendNotification($approverIds, $instance, 'pending', false, $extra); - $this->notifyCCs($firstStep, $instance, 'pending'); - $this->pushTodoChanged($approverIds); - return ['instance_id' => $instanceId]; - }); - } - - - // A) 取组内全部成员 - private function getGroupMemberIds(int $groupId): array - { - $rows = Db::table('user_group_members') - ->where('group_id', $groupId) - ->where('status', 1) - ->pluck('user_id')->toArray(); - - return array_values(array_unique(array_map('intval', $rows))); - } - - // B) 按“场景”在发起人所在的组里找负责人(支持主归属/排序兜底) - private function getLeadersByScenarioForInitiator( - int $initiatorId, - string $scenarioCode, - bool $mustBelong = true, - bool $preferPrimary = true - ): array { - // 找出“该场景下发起人所在的所有组” - $groups = Db::table('user_group as g') - ->join('user_group_members as m', 'm.group_id', '=', 'g.id') - ->where('g.scenario_code', $scenarioCode) - ->where('g.status', 1) - ->where('m.user_id', $initiatorId) - ->where('m.status', 1) - ->orderByDesc('m.is_primary') // 主归属优先 - ->orderByDesc('g.sort') // 其次按组排序 - ->select('g.id', 'm.is_primary') - ->get()->toArray(); - - if (empty($groups)) { - if ($mustBelong) { - throw new BusinessException('发起人在该业务场景下未加入任何分组'); - } - // 不强制属于:允许只按“该场景下任一组”的负责人(不推荐) - $groups = Db::table('user_group as g') - ->where('g.scenario_code', $scenarioCode) - ->where('g.status', 1) - ->orderByDesc('g.sort') - ->select('g.id', Db::raw('0 as is_primary')) - ->get()->toArray(); - if (empty($groups)) { - throw new BusinessException('该业务场景下未配置任何分组'); - } - } - - // 选择一个“目标组” - $targetGroupId = null; - if ($preferPrimary) { - foreach ($groups as $g) { - if ((int)$g->is_primary === 1) { $targetGroupId = (int)$g->id; break; } - } - } - if (!$targetGroupId) { - $targetGroupId = (int)$groups[0]->id; // 用排序第一的组 - } - - // 取负责人(可支持多负责人) - $leaders = Db::table('user_group_members') - ->where('group_id', $targetGroupId) - ->where('is_leader', 1) - ->where('status', 1) - ->pluck('user_id')->toArray(); - - $ids = array_values(array_unique(array_map('intval', $leaders))); - if (empty($ids)) throw new BusinessException('目标分组未配置负责人'); - - return $ids; - } - - // - private function isNullifyFlow(string $flowType): bool - { - $t = strtolower(trim($flowType)); - return str_ends_with($t, '_nullify') || in_array($t, [ - // 所有“作废”类流程的类型 - 'stock_orders_nullify', - ], true); - } - - // 放到工具方法区 - private function normalizeFinalStatus(string $flowType, string $final): string - { - if ($this->isNullifyFlow($flowType) && $final === 'approved') { - return 'voided'; - } - return $final; - } - - /** WebSocket:把一组用户的“待办数量”推送给前端 */ - private function pushTodoChanged22(array $userIds): void - { - $userIds = array_values(array_unique(array_map('intval', $userIds))); - if (empty($userIds)) return; - - // 统计这些用户当前的待办数量(以 approval_records.status = 'pending' 为准) - $rows = Db::table('approval_records') - ->whereIn('approver_id', $userIds) - ->where('status', 'pending') - ->groupBy('approver_id') - ->select('approver_id', Db::raw('COUNT(*) as cnt')) - ->get(); - - // 默认都置 0,再填充真实值 - $payload = array_fill_keys($userIds, 0); - foreach ($rows as $r) { - $payload[(int)$r->approver_id] = (int)$r->cnt; - } - - try { - foreach ($payload as $uid => $cnt) { - $data = [ - 'event' => 'approval.todo.counts', - 'user_id' => $uid, - 'num' => $cnt, - ]; - \Webman\RedisQueue\Redis::send('ws-msg', $data); - } - } catch (\Throwable $e) { - Log::error('ws todo push error: '.$e->getMessage()); - } - } - - /** 取“某实例、某轮次、某步骤”里当前 pending 的审批人(用于推送受影响的用户) */ - private function getPendingApproverIds(int $instanceId, int $round, int $stepIndex): array - { - $ids = Db::table('approval_records') - ->where('instance_id', $instanceId) - ->where('round', $round) - ->where('step_index', $stepIndex) - ->where('status', 'pending') - ->pluck('approver_id')->toArray(); - - return array_values(array_unique(array_map('intval', $ids))); - } - - - /** 登录/首屏初始化:获取当前用户的待办总数(仅统计当前步骤、进行中的实例) */ - public function getInitTodoCount(int $userId): int - { - $userId = (int)$userId; - if ($userId <= 0) { - throw new BusinessException('无效的用户ID'); - } - - $row = Db::table('approval_records as ar') - ->join('approval_instances as ai', 'ai.id', '=', 'ar.instance_id') - ->where('ar.approver_id', $userId) - ->where('ar.status', 'pending') - ->where('ai.status', 'processing') - // 只统计实例“当前轮次、当前步骤”的待办,避免历史残留干扰 - ->whereRaw('ar.round = ai.round') - ->whereRaw('ar.step_index = ai.current_step') - ->selectRaw('COUNT(*) as c') - ->first(); - - return (int)($row->c ?? 0); - } - - /** WebSocket:把一组用户的“待办数量”推送给前端(与 getInitTodoCount 统一口径) */ - private function pushTodoChanged(array $userIds): void - { - $userIds = array_values(array_unique(array_map('intval', $userIds))); - if (empty($userIds)) return; - - // 只统计:实例进行中 + 当前轮次 + 当前步骤 的 pending - $rows = Db::table('approval_records as ar') - ->join('approval_instances as ai', 'ai.id', '=', 'ar.instance_id') - ->whereIn('ar.approver_id', $userIds) - ->where('ar.status', 'pending') - ->where('ai.status', 'processing') - ->whereRaw('ar.round = ai.round') - ->whereRaw('ar.step_index = ai.current_step') - ->groupBy('ar.approver_id') - ->select('ar.approver_id', Db::raw('COUNT(*) as cnt')) - ->get(); - - // 默认置 0,再填充真实值 - $payload = array_fill_keys($userIds, 0); - foreach ($rows as $r) { - $payload[(int)$r->approver_id] = (int)$r->cnt; - } - - try { - foreach ($payload as $uid => $cnt) { - (new \app\common\utils\WsEvents())->todoCounts($uid, $cnt); - } - } catch (\Throwable $e) { - Log::error('ws todo push error: '.$e->getMessage()); - } - } -} \ No newline at end of file + use traits\ApprovalEngineTrait; + use traits\ApprovalQueryTrait; + use traits\ApprovalResolverTrait; + use traits\ApprovalNotificationTrait; + use traits\ApprovalDiscussionTrait; + use traits\ApprovalTransferTrait; +} diff --git a/app/common/utils/approval/service/ApprovalFlowService_20251208.php b/app/common/utils/approval/service/ApprovalFlowService_20251208.php new file mode 100644 index 0000000..989bf7c --- /dev/null +++ b/app/common/utils/approval/service/ApprovalFlowService_20251208.php @@ -0,0 +1,1953 @@ +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, + 'round' => 1, + 'deadline_at' => $deadlineAt, + 'created_at' => $now, + 'updated_at' => $now, + ]); + + $instance = (object)[ + 'id' => $instanceId, + 'flow_type' => $flowType, + 'form_data' => json_encode($formData, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), + 'initiator_id' => $initiatorId, + 'current_step' => $firstIndex, + 'status' => 'processing', + 'flow_type' => $flowType, + 'biz_type' => $bizType, + 'biz_id' => $bizId, + 'round' => 1, + ]; + + // 解析首步审批人(支持 role_id 或 roles.name) + $approverIds = $this->resolveApprovers($firstStep, $instance, true); + foreach ($approverIds as $uid) { + $this->insertApprovalRecord($instance, $firstIndex, $uid); + } + + $extra = []; + $submitNote = $formData['submit_note'] ?? ''; + if (is_string($submitNote) && $submitNote !== '') { + $extra['note'] = $submitNote; + } + $this->sendNotification($approverIds, $instance, 'pending', false, $extra); + + // $this->notifyCCs($firstStep, $instance, 'pending'); + + $this->pushTodoChanged($approverIds); + + 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', '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; + $round = (int)$instance->round; + $step = $steps[$idx] ?? null; + if (!$step) throw new BusinessException("当前步骤配置错误"); + + // 找到该用户在本步的 pending 记录(加锁) + $pendingRecord = Db::table('approval_records') + ->where('instance_id', $instanceId) + ->where('round', $round) + ->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('round', $round) + ->where('approver_id', $approverId) + ->where('step_index', $idx) + ->first(); + if ($anyRecord && $anyRecord->status !== 'pending') { + return ['status' => 'noop', 'msg' => '已处理']; + } + // 步骤已推进 + if ((int)$instance->current_step !== $idx) { + return ['status' => 'noop', 'msg' => '非当前审批']; + } + 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, + ]); + + // 异步审批回调 9.23 add + $approvalcallback = [ + 'biz_id' => $instance->biz_id, + 'flow_type' => $instance->flow_type, + 'instance_id' => $instance->id, + 'initiator_id' => $instance->initiator_id, + 'status' => $action + ]; + \Webman\RedisQueue\Redis::send('approval-callback', $approvalcallback); + + // 2) 聚合本步结论(any / all) + $typeRaw = strtolower($step['type'] ?? 'all'); + $type = in_array($typeRaw, ['any','all']) ? $typeRaw : 'all'; + $outcome = $this->getStepOutcome($instanceId, $round, $idx, $type); // 'pending' | 'approve' | 'reject' + + if ($outcome === 'pending') { + $this->pushTodoChanged([$approverId]); + return ['status' => 'step_pending', 'msg' => '审批待定']; + } + + $otherPendingUids = $this->getPendingApproverIds($instanceId, $round, $idx); + + // 3) 有结论:关闭本步其余 pending + $this->closeOtherPending($instanceId, $round, $idx, [$approverId]); + + // 这些人 pending→skipped,待办数减少 + $this->pushTodoChanged($otherPendingUids); + + // 4) 拒绝 => 结束 + if ($outcome === 'reject') { + Db::table('approval_instances')->where('id', $instanceId)->update([ + 'status' => 'rejected', + 'finished_at' => $now, + 'updated_at' => $now, + ]); + $this->onFlowFinalized($instance, 'rejected'); + $this->sendNotification([$instance->initiator_id], $instance, 'reject', false); + $this->pushTodoChanged([$approverId]); + return ['status' => 'completed', 'msg' => '拒绝']; + } + + // 5) 通过 => 进入下一步或结束 + $formData = json_decode($instance->form_data, true) ?: []; + $nextIndex = $this->getNextStepIndex($steps, $formData, $idx); + + if ($nextIndex === null) { + $finalStatus = $this->normalizeFinalStatus((string)$instance->flow_type, 'approved'); + $notifyAction = ($finalStatus === 'voided') ? 'voided' : 'approve'; + + Db::table('approval_instances')->where('id', $instanceId)->update([ + 'status' => $finalStatus, + 'finished_at' => $now, + 'updated_at' => $now, + ]); + $this->onFlowFinalized($instance, $finalStatus); + + // 新增:在流程最终完成时才发本步骤的抄送 + if (!empty($step['cc'])) { + $this->notifyCCs($step, $instance, $notifyAction); + } + + $this->sendNotification([$instance->initiator_id], $instance, $notifyAction, false); + $this->pushTodoChanged([$approverId]); + return ['status' => 'completed', 'msg' => ($finalStatus === 'voided' ? '已作废' : '同意')]; + } + + // 乐观推进 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', 'msg' => '并发进展']; + } + + // 生成下一步待办 + 通知 + $nextStep = $steps[$nextIndex]; + $nextApprovers = $this->resolveApprovers($nextStep, $instance, true); + foreach ($nextApprovers as $uid) { + $this->insertApprovalRecord($instance, $nextIndex, $uid); + } + $this->sendNotification($nextApprovers, $instance, 'pending', false); + // $this->notifyCCs($nextStep, $instance, 'pending'); + $this->pushTodoChanged(array_merge([$approverId], $nextApprovers)); + return ['status' => 'next_step', 'msg' => '进行下一步:'.$nextStep['name'] ?? ('Step '.$nextIndex)]; + }); + } + + // 流程完结后的业务回调(approved|rejected|revoked|timeout?) + private function onFlowFinalized(object $instance, string $final): void + { + try { + switch ($instance->flow_type) { + case 'stock_orders': { + $order = \app\common\model\StockOrders::find($instance->biz_id); + if (!$order) { + throw new BusinessException('订单不存在'); + } + if ($final === 'approved') { + $order->approval_status = 2; + $order->save(); + } elseif ($final === 'rejected') { + $order->approval_status = 3; + $order->save(); + } elseif ($final === 'revoked') { + $order->approval_status = 0; + $order->save(); + } + break; + } + case 'stock_orders_nullify': + if ($final === 'approved') { + $final = 'voided'; + Db::table('approval_instances')->where('id', $instance->id)->update([ + 'status' => 'voided', + ]); + } + if (in_array($final, ['voided','approved'], true)) { + $order = \app\common\model\StockOrders::find($instance->biz_id); + if ($order && $order->approval_status !== 4) { + $order->approval_status = 4; + $order->save(); + } + } + break; + default: + // 非作废流程,无需处理 + break; + } + } catch (\Throwable $e) { + Log::error('onFlowFinalized error: '.$e->getMessage()); + } + } + + + /** 撤回(仅发起人在无已处理记录前可撤回) */ + 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, + ]); + + $this->onFlowFinalized($instance, 'revoked'); + + $pendingUids = Db::table('approval_records') + ->where('instance_id', $instanceId) + ->where('status', 'pending') + ->pluck('approver_id')->toArray(); + + 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); + $this->pushTodoChanged($pendingUids); + return ['status' => 'completed', 'result' => 'revoked']; + }); + } + + /** 流程图数据(把 skipped/timeout 视作已完结的中性状态) */ + public function getFlowDiagramData(int $instanceId): array + { + $instance = Db::table('approval_instances')->find($instanceId); + if (!$instance) return []; + + $flow = Db::table('approval_flows')->where('type', $instance->flow_type)->first(); + if (!$flow) return []; + + $steps = json_decode($flow->steps, true) ?: []; + $records = Db::table('approval_records') + ->where('instance_id', $instanceId) + ->where('round', (int)$instance->round) + ->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'; + $final = $this->normalizeFinalStatus((string)$instance->flow_type, $final); + $finalAction = ($final === 'voided') ? 'voided' : (($final === 'approved') ? 'approve' : 'reject'); + + Db::table('approval_instances')->where('id', $instance->id)->update([ + 'status' => $final, + 'finished_at' => $now, + 'updated_at' => $now, + ]); + + $this->onFlowFinalized((object)$instance, $final); + + $pendingUids = Db::table('approval_records') + ->where('instance_id', $instance->id) + ->where('round', (int)$instance->round) + ->where('step_index', $instance->current_step) + ->where('status', 'pending') + ->pluck('approver_id')->toArray(); + + Db::table('approval_records') + ->where('instance_id', $instance->id) + ->where('round', (int)$instance->round) + ->where('step_index', $instance->current_step) + ->where('status', 'pending') + ->update(['status' => 'timeout', 'approved_at' => $now, 'updated_at' => $now]); + + $this->notifyInitiator($instance, $finalAction); + + $this->pushTodoChanged($pendingUids); + }); + } + } + + // ---------- 内部工具 ---------- + + /** 解析审批人(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; + + case 'group_members': { + // $gid = (int)($cfg['group_id'] ?? 0); + $gid = (int)($cfg['value'] ?? 0); + if ($gid <= 0) throw new BusinessException('group_members 需要 group_id'); + $ids = $this->getGroupMemberIds($gid); // A 方案 + break; + } + + case 'scenario_leader_of_initiator': { + $formData = json_decode($instance->form_data ?? '{}', true) ?: []; + // $scenario = $cfg['scenario'] ?? ($formData[$cfg['scenario_field']] ?? null); + $scenario = $val ?? ($formData[$cfg['scenario_field']] ?? null); + if (!$scenario) throw new BusinessException('缺少业务场景(scenario 或 scenario_field)'); + + $mustBelong = array_key_exists('must_belong', $cfg) ? (bool)$cfg['must_belong'] : true; + $preferPrimary = array_key_exists('prefer_primary', $cfg) ? (bool)$cfg['prefer_primary'] : true; + + $ids = $this->getLeadersByScenarioForInitiator( + (int)$instance->initiator_id, + (string)$scenario, + $mustBelong, + $preferPrimary + ); // B 方案(按场景) + 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; + case 'group_members': { + $gid = (int)($val ?? 0); + if ($gid <= 0) throw new BusinessException('group_members 需要 group_id'); + $uids = $this->getGroupMemberIds($gid); // A 方案 + break; + } + case 'scenario_leader_of_initiator': { + $formData = json_decode($instance->form_data ?? '{}', true) ?: []; + $scenario = $val ?? ($formData[$cc['scenario_field']] ?? null); + if (!$scenario) throw new BusinessException('缺少业务场景(scenario 或 scenario_field)'); + + $mustBelong = array_key_exists('must_belong', $cc) ? (bool)$cc['must_belong'] : true; + $preferPrimary = array_key_exists('prefer_primary', $cc) ? (bool)$cc['prefer_primary'] : true; + + $uids = $this->getLeadersByScenarioForInitiator( + (int)$instance->initiator_id, + (string)$scenario, + $mustBelong, + $preferPrimary + ); // B 方案(按场景) + 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; + } + + /** + * 聚合本步骤结论(按类型) + */ + private function getStepOutcome(int $instanceId, int $round, int $stepIndex, string $type): string + { + $records = Db::table('approval_records') + ->where('instance_id', $instanceId) + ->where('round', $round) // + ->where('step_index', $stepIndex) + ->get(); + + $pending = $approved = $rejected = $timeout = $skipped = 0; + foreach ($records as $r) { + switch ($r->status) { + case 'pending': $pending++; break; + case 'approved': $approved++; break; + case 'rejected': $rejected++; break; + case 'timeout': $timeout++; break; + case 'skipped': $skipped++; break; + } + } + + $type = strtolower($type); + if ($type === 'any') { + if ($rejected > 0) return 'reject'; + if ($approved > 0) return 'approve'; + // 全为 skipped/timeout 且无 pending/rejected ⇒ 通过 + if ($pending === 0 && $rejected === 0 && $approved === 0) return 'approve'; + return 'pending'; + } + if ($rejected > 0) return 'reject'; + if ($pending > 0) return 'pending'; + return 'approve'; + } + + /** 关闭本步其余待办为 skipped */ + private function closeOtherPending(int $instanceId, int $round, int $stepIndex, array $keepApproverIds = []): void + { + $now = date('Y-m-d H:i:s'); + $q = Db::table('approval_records') + ->where('instance_id', $instanceId) + ->where('round', $round) // + ->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(object $instance, int $stepIndex, int $approverId): void + { + $round = (int)($instance->round ?? 1); + $now = date('Y-m-d H:i:s'); + + $exists = Db::table('approval_records') + ->where('instance_id', (int)$instance->id) + ->where('round', $round) // 限当前轮 + ->where('step_index', $stepIndex) + ->where('approver_id', $approverId) + ->where('status', 'pending') + ->exists(); + + if ($exists) return; + + Db::table('approval_records')->insert([ + 'instance_id' => (int)$instance->id, + 'approver_id' => $approverId, + 'step_index' => $stepIndex, + 'round' => $round, // + '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, array $extra = []): void + { + $map = [ + 'pending' => '待审批', + 'approve' => '已通过', + 'reject' => '已拒绝', + 'revoked' => '已撤回', + 'voided' => '已作废', + ]; + $prefix = $isCC ? '【抄送】' : '【审批】'; + $text = $map[$action] ?? $action; + + // 业务标题/内容 + $approvalType = array_flip(\app\common\service\SysDictService::pluckKeyValueByCode('approval_type')); + + $flowTxt = $approvalType[$instance->flow_type] ?? (string)$instance->flow_type; + $title = $prefix.'审批通知'; + $content = "{$prefix}你有一条审批 {$text}:类型 {$flowTxt},业务标识 {$instance->biz_id}"; + + switch ($instance->flow_type) { + case 'stock_orders': + // 备货单 + $stockOrder = \app\common\model\StockOrders::find($instance->biz_id); + if ($stockOrder) { + $title = $prefix."备货单:{$stockOrder->order_no}"; + $flowTxt = $approvalType[$instance->flow_type] ?? $instance->flow_type; + $content = "{$prefix}你有一条审批 {$text}:类型 {$flowTxt},业务单号 {$stockOrder->order_no}"; + } + break; + case 'stock_orders_nullify': + // 备货单作废 + $stockOrder = \app\common\model\StockOrders::find($instance->biz_id); + if ($stockOrder) { + $title = $prefix."备货单作废:{$stockOrder->order_no}"; + $flowTxt = $approvalType[$instance->flow_type] ?? '备货单作废'; + $content = "{$prefix}你有一条审批 {$text}:类型 {$flowTxt},业务单号 {$stockOrder->order_no}"; + // 把作废原因附带展示更清晰(也可依赖 $extra['note']) + $fd = json_decode($instance->form_data ?? '{}', true) ?: []; + if (!empty($fd['_nullify_reason'])) { + $content .= ';原因:'.(function_exists('mb_strimwidth') ? mb_strimwidth($fd['_nullify_reason'], 0, 120, '...', 'UTF-8') : $fd['_nullify_reason']); + } + } + break; + case 'purchase_orders': + // 采购单 + $purchaseOrder = \app\common\model\PurchaseOrders::find($instance->biz_id); + if ($purchaseOrder) { + $title = $prefix."采购单:{$purchaseOrder->order_no}"; + $flowTxt = $approvalType[$instance->flow_type] ?? $instance->flow_type; + $content = "{$prefix}你有一条审批 {$text}:类型 {$flowTxt},业务单号 {$purchaseOrder->order_no}"; + } + break; + case 'purchase_order': + // 采购单 + break; + } + + // 附加说明(提交说明/申诉/讨论摘要) + $append = $extra['appeal'] ?? ($extra['note'] ?? null); + if ($append) { + if (function_exists('mb_strimwidth')) { + $append = mb_strimwidth($append, 0, 120, '...', 'UTF-8'); + } + $content .= ";说明:{$append}"; + } + + noticePush([ + 'title' => $title, + 'content' => $content, + 'recipient' => $userIds, + 'email' => 1, + ]); + } + + + private function notifyInitiator(object $instance, string $result): void + { + $this->sendNotification([(int)$instance->initiator_id], $instance, $result, false); + } + + // ---------- 数据访问(roles / user_roles 结构) ---------- + + /** + * 从角色取用户ID + * @param mixed $roleIdOrName 数字 => role_id;字符串 => roles.name + * @param int|null $systemId 限定 roles.system_id;null 不限定 + * @param bool $onlyActive 仅取 status=1 角色,默认 true + * @param bool $includeGlobal 是否包含全局角色(system_id=0),默认 true + */ + private function getUserIdsByRole($roleIdOrName, ?int $systemId = null, bool $onlyActive = true, bool $includeGlobal = true): array + { + if (is_int($roleIdOrName) || (is_string($roleIdOrName) && ctype_digit($roleIdOrName))) { + $roleId = (int)$roleIdOrName; + $needJoin = $onlyActive || !is_null($systemId) || $includeGlobal === false; + + if (!$needJoin) { + $rows = Db::table('user_roles')->where('role_id', $roleId)->select('user_id')->get(); + } else { + $q = Db::table('user_roles as ur') + ->join('roles as r', 'ur.role_id', '=', 'r.id') + ->where('ur.role_id', $roleId) + ->select('ur.user_id'); + if ($onlyActive) $q->where('r.status', 1); + if (!is_null($systemId)) { + $includeGlobal ? $q->whereIn('r.system_id', [0, $systemId]) : $q->where('r.system_id', $systemId); + } + $rows = $q->get(); + } + } else { + $name = (string)$roleIdOrName; + $q = Db::table('user_roles as ur') + ->join('roles as r', 'ur.role_id', '=', 'r.id') + ->where('r.name', $name) + ->select('ur.user_id'); + if ($onlyActive) $q->where('r.status', 1); + if (!is_null($systemId)) { + $includeGlobal ? $q->whereIn('r.system_id', [0, $systemId]) : $q->where('r.system_id', $systemId); + } + $rows = $q->get(); + } + + $ids = []; + foreach ($rows as $row) $ids[] = (int)$row->user_id; + return array_values(array_unique($ids)); + } + + private function getLeaderIdByUserId(int $userId): ?int + { + $id = Db::table('user')->where('id', $userId)->value('report_to'); + return $id ? (int)$id : null; + } + + private function getDeptHeadId(int $deptId): ?int + { + $id = Db::table('departments')->where('id', $deptId)->value('head_user_id'); + return $id ? (int)$id : null; + } + + + /** + * 查询某个审批实例的审批记录(附带步骤名/类型,可选带出审批人姓名) + * + * @param int $instanceId + * @param string|null $userNameColumn 可选:如 'name' 或 'nickname'。传 null 则不 join 用户表 + * @return array{ + * instance: array, + * steps: array, + * records: array, + * summary: array{approved:int,rejected:int,pending:int,skipped:int,timeout:int,total:int} + * } + */ + public function getInstanceRecords(int $instanceId, ?string $userNameColumn = null): array + { + // 1) 实例 + 流程步骤 + $instance = Db::table('approval_instances')->where('id', $instanceId)->first(); + if (!$instance) { + throw new BusinessException('审批实例未找到'); + } + $flow = Db::table('approval_flows')->where('type', $instance->flow_type)->first(); + if (!$flow) { + throw new BusinessException('审批流程定义未找到'); + } + $steps = json_decode($flow->steps, true) ?: []; + + // 2) 记录查询(可选带出审批人姓名) + if ($userNameColumn) { + // 注意:确保 user 表存在该列 + $records = Db::table('approval_records as ar') + ->leftJoin('user as u', 'ar.approver_id', '=', 'u.id') + ->where('ar.instance_id', $instanceId) + ->orderBy('ar.step_index') + ->orderByRaw('COALESCE(ar.approved_at, ar.created_at) asc') + ->select( + 'ar.*', + Db::raw("u.`{$userNameColumn}` as approver_name") + ) + ->get() + ->toArray(); + } else { + $records = Db::table('approval_records as ar') + ->where('ar.instance_id', $instanceId) + ->orderBy('ar.step_index') + ->orderByRaw('COALESCE(ar.approved_at, ar.created_at) asc') + ->select('ar.*') + ->get() + ->toArray(); + } + + // 3) 附加步骤名/类型;统计汇总 + $summary = ['approved'=>0,'rejected'=>0,'pending'=>0,'skipped'=>0,'timeout'=>0,'total'=>0]; + foreach ($records as &$r) { + $idx = (int)$r->step_index; + $meta = $steps[$idx] ?? []; + $r->step_name = $meta['name'] ?? ('Step '.$idx); + $r->step_type = $meta['type'] ?? 'all'; + + if (isset($summary[$r->status])) { + $summary[$r->status]++; + } + $summary['total']++; + } + unset($r); + + return [ + 'instance' => [ + 'id' => (int)$instance->id, + 'flow_type' => $instance->flow_type, + 'biz_type' => $instance->biz_type, + 'biz_id' => $instance->biz_id, + 'status' => $instance->status, + 'current_step' => (int)$instance->current_step, + 'created_at' => $instance->created_at, + 'updated_at' => $instance->updated_at, + 'finished_at' => $instance->finished_at, + ], + 'steps' => array_map(function($s){ + return [ + 'name' => $s['name'] ?? '', + 'type' => $s['type'] ?? 'all', + 'condition' => $s['condition'] ?? null, + ]; + }, $steps), + 'records' => array_map(function($r){ + // 转成数组(Illuminate 返回的是 stdClass) + return [ + 'id' => (int)$r->id, + 'instance_id' => (int)$r->instance_id, + 'approver_id' => (int)$r->approver_id, + 'step_index' => (int)$r->step_index, + 'action' => $r->action, + 'status' => $r->status, + 'comment' => $r->comment, + 'approved_at' => $r->approved_at, + 'created_at' => $r->created_at, + 'updated_at' => $r->updated_at, + 'approver_name'=> property_exists($r, 'approver_name') ? $r->approver_name : null, + 'step_name' => $r->step_name, + 'step_type' => $r->step_type, + ]; + }, $records), + 'summary' => $summary, + ]; + } + + + 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; + $round = (int)$instance->round; + + // 申请人 + $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) + ->where('ar.round', $round) + ->orderBy('ar.step_index') + ->orderByRaw('COALESCE(ar.approved_at, ar.created_at) asc') + ->select('ar.*') + ->get() + ->toArray(); + + // 一次性拉取姓名,避免 N+1 + $nameMap = []; + if ($userNameColumn && !empty($records)) { + $uids = []; + foreach ($records as $r) $uids[] = (int)$r->approver_id; + $uids = array_values(array_unique($uids)); + if (!empty($uids)) { + $pairs = Db::table('user')->whereIn('id', $uids)->pluck($userNameColumn, 'id')->toArray(); + foreach ($pairs as $uid => $name) $nameMap[(int)$uid] = $name; + } + } + + // 记录分组 + $byStep = []; + foreach ($records as $r) { + $idx = (int)$r->step_index; + $byStep[$idx][] = $r; + } + + $timeline = []; + + // 可选:发起申请块 + 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; + + // 每一步的“审批意见”(只展示有内容的 comment) + $opinions = []; // [{approver_id, approver_name?, action, status, comment, time}...] + + 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) { + // 收集“审批意见”(非空 comment 都算) + $cmt = trim((string)($rr->comment ?? '')); + if ($cmt !== '') { + $opinions[] = [ + 'approver_id' => (int)$rr->approver_id, + 'approver_name' => $userNameColumn ? ($nameMap[(int)$rr->approver_id] ?? null) : null, + 'action' => $rr->action, // approve | reject | null + 'status' => $rr->status, // approved|rejected|pending|skipped|timeout + 'comment' => $cmt, + 'time' => $rr->approved_at ?: $rr->created_at, + ]; + } + + 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]++; + } + + // 时间升序排序 opinions + if (!empty($opinions)) { + usort($opinions, function($a, $b) { + return strcmp($a['time'] ?? '', $b['time'] ?? ''); + }); + } + + $startedAt = $minCreated; + + // 总结果(注意:这里按你现在的签名调用,含 round) + $outcome = $this->getStepOutcome($instanceId, $round, $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, + 'opinions' => $opinions, // 这就是每个审批意见 + ]; + + // 把申请人信息并到第一步(可选) + 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; + $round = (int)$instance->round; + $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.round', $round) + ->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('round', $round) + ->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('round', $round) + ->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; + } + + // ====== ====== ====== ====== ====== ====== 重提操作 && 讨论 ====== ====== ====== ====== ====== ====== + + /** + * 重提交(仅对 status=rejected 的实例;支持申诉说明) + * - 合并 $patchData 到 form_data,并设置 resubmitted=1 + * - 若 $appealText 非空:form_data.appeal_last = $appealText(可用于前端展示) + * - 重新计算首个命中的步骤并创建待办;通知审批人且带上申诉说明 + */ + public function resubmit(int $instanceId, int $initiatorId, array $patchData = [], string $appealText = ''): array + { + return Db::transaction(function () use ($instanceId, $initiatorId, $patchData, $appealText) { + $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 !== 'rejected') 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) ?: []; + + // 合并改动 & 打上重提标记 + $formData = array_merge($formData, $patchData); + $formData['resubmitted'] = 1; + + $appealText = trim((string)$appealText); + if ($appealText !== '') { + $formData['appeal_last'] = $appealText; + } + + // 计算重提后首个命中的步骤 + $firstIndex = $this->getNextStepIndex($steps, $formData, -1); + if ($firstIndex === null) throw new BusinessException('重提后未找到可执行的步骤'); + + $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); + } + + $oldPendingUids = Db::table('approval_records') + ->where('instance_id', $instanceId) + ->where('status', 'pending') + ->pluck('approver_id')->toArray(); + + // 清理可能遗留的 pending + Db::table('approval_records') + ->where('instance_id', $instanceId) + ->where('status', 'pending') + ->update(['status'=>'skipped','approved_at'=>$now,'updated_at'=>$now]); + + // 轮次 +1 + $newRound = ((int)$instance->round) + 1; + + // 重新开启实例 + Db::table('approval_instances')->where('id', $instanceId)->update([ + 'status' => 'processing', + 'current_step' => $firstIndex, + 'round' => $newRound, + 'form_data' => json_encode($formData, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), + 'finished_at' => null, + 'deadline_at' => $deadlineAt, + 'updated_at' => $now, + ]); + + // 创建待办 + 通知 + $firstStep = $steps[$firstIndex]; + $instanceLite = (object)[ + 'id' => (int)$instanceId, + 'flow_type' => (string)$instance->flow_type, + 'form_data' => json_encode($formData, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), + 'initiator_id' => (int)$instance->initiator_id, + 'biz_type' => (string)$instance->biz_type, + 'biz_id' => (string)$instance->biz_id, + 'round' => $newRound, + ]; + $approvers = $this->resolveApprovers($firstStep, $instanceLite, true); + if (empty($approvers)) throw new BusinessException('重提后首步未解析到审批人'); + + foreach ($approvers as $uid) { + $this->insertApprovalRecord($instanceLite, $firstIndex, (int)$uid); + } + + $extra = $appealText !== '' ? ['appeal' => $appealText] : []; + $this->sendNotification($approvers, $instanceLite, 'pending', false, $extra); + $this->notifyCCs($firstStep, $instanceLite, 'pending'); + $this->pushTodoChanged(array_merge($oldPendingUids, $approvers)); + + return [ + 'status' => 'resubmitted', + 'instance_id' => (int)$instanceId, + 'current_step' => $firstIndex, + 'step_name' => $firstStep['name'] ?? ('Step '.$firstIndex), + ]; + }); + } + + + /** 是否该实例参与者(发起人/任一步审批人/抄送人) */ + private function isParticipantOfInstance(int $instanceId, int $userId): bool + { + $inst = Db::table('approval_instances')->where('id', $instanceId)->first(); + if (!$inst) return false; + if ((int)$inst->initiator_id === $userId) return true; + + $inRecords = Db::table('approval_records') + ->where('instance_id', $instanceId)->where('approver_id', $userId)->exists(); + if ($inRecords) return true; + + $inCc = Db::table('approval_ccs') + ->where('instance_id', $instanceId)->where('user_id', $userId)->exists(); + return $inCc; + } + + /** 是否该实例任一步的审批人 */ + private function isApproverOfInstance(int $instanceId, int $userId): bool + { + return Db::table('approval_records') + ->where('instance_id', $instanceId) + ->where('approver_id', $userId) + ->exists(); + } + + /** 获取讨论受众:公开=全体参与者;内部=仅审批人(排除自己) */ + private function getDiscussionAudienceUserIds(int $instanceId, bool $approversOnly, int $excludeUserId): array + { + $uids = []; + if (!$approversOnly) { + $inst = Db::table('approval_instances')->where('id', $instanceId)->first(); + if ($inst) $uids[] = (int)$inst->initiator_id; + $ccs = Db::table('approval_ccs')->where('instance_id', $instanceId)->pluck('user_id')->toArray(); + foreach ($ccs as $u) $uids[] = (int)$u; + } + $approvers = Db::table('approval_records') + ->where('instance_id', $instanceId) + ->distinct()->pluck('approver_id')->toArray(); + foreach ($approvers as $u) $uids[] = (int)$u; + + $uids = array_values(array_unique(array_filter($uids, fn($u) => $u !== $excludeUserId))); + return $uids; + } + + /** + * 发表讨论 + * @param int $instanceId + * @param int $userId + * @param string $content + * @param bool $isInternal true=仅审批人可见;false=所有参与者可见 + * @param array $attachments 可选附件数组 + * @param string|null $userNameColumn 通知里带发言人姓名(如 'name'),不需要传 null + */ + public function postDiscussion( + int $instanceId, + int $userId, + string $content, + bool $isInternal = false, + array $attachments = [], + ?string $userNameColumn = null + ): array { + $content = trim($content); + if ($content === '') throw new BusinessException('讨论内容不能为空'); + if (!$this->isParticipantOfInstance($instanceId, $userId)) { + throw new BusinessException('无权参与该实例的讨论'); + } + if ($isInternal && !$this->isApproverOfInstance($instanceId, $userId)) { + throw new BusinessException('仅审批人可发布内部讨论'); + } + + $now = date('Y-m-d H:i:s'); + $id = Db::table('approval_discussions')->insertGetId([ + 'instance_id' => $instanceId, + 'user_id' => $userId, + 'is_internal' => $isInternal ? 1 : 0, + 'content' => $content, + 'attachments' => empty($attachments) ? null : json_encode($attachments, JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES), + 'created_at' => $now, + 'updated_at' => $now, + ]); + + // 通知其他人 + $inst = Db::table('approval_instances')->where('id', $instanceId)->first(); + if ($inst) { + $audience = $this->getDiscussionAudienceUserIds($instanceId, $isInternal, $userId); + // 发言人姓名(可选) + $speaker = (string)$userId; + if ($userNameColumn) { + $name = Db::table('user')->where('id', $userId)->value($userNameColumn); + if ($name) $speaker = $name; + } + $snippet = function_exists('mb_strimwidth') ? mb_strimwidth($content, 0, 80, '...', 'UTF-8') : $content; + $extra = ['note' => "【讨论】{$speaker}:{$snippet}"]; + + $this->sendNotification( + $audience, + (object)[ + 'id' => (int)$inst->id, + 'flow_type' => (string)$inst->flow_type, + 'initiator_id' => (int)$inst->initiator_id, + 'form_data' => $inst->form_data, + 'biz_type' => (string)$inst->biz_type, + 'biz_id' => (string)$inst->biz_id, + ], + 'pending', + false, + $extra + ); + } + + return [ + 'id' => (int)$id, + 'instance_id' => $instanceId, + 'user_id' => $userId, + 'is_internal' => (bool)$isInternal, + 'content' => $content, + 'attachments' => $attachments, + 'created_at' => $now, + ]; + } + + /** + * 获取讨论列表 + * @param int $instanceId + * @param int|null $afterId 翻页:取 id > afterId 的新消息 + * @param int $limit + * @param string|null $userNameColumn 带出用户名(如 'name'),不需要可传 null + */ + public function listDiscussions(int $instanceId, ?int $afterId = null, int $limit = 50, ?string $userNameColumn = null): array + { + $q = Db::table('approval_discussions as d')->where('d.instance_id', $instanceId); + if ($afterId) $q->where('d.id', '>', $afterId); + $q->orderBy('d.id', 'asc')->limit(max(1, min(200, $limit))); + + if ($userNameColumn) { + $rows = $q->leftJoin('user as u', 'd.user_id', '=', 'u.id') + ->select('d.*', Db::raw("u.`{$userNameColumn}` as user_name")) + ->get()->toArray(); + } else { + $rows = $q->select('d.*')->get()->toArray(); + } + + return array_map(function($r){ + return [ + 'id' => (int)$r->id, + 'instance_id' => (int)$r->instance_id, + 'user_id' => (int)$r->user_id, + 'is_internal' => (bool)$r->is_internal, + 'content' => $r->content, + 'attachments' => $r->attachments ? json_decode($r->attachments, true) : [], + 'created_at' => $r->created_at, + 'updated_at' => $r->updated_at, + 'user_name' => property_exists($r, 'user_name') ? $r->user_name : null, + ]; + }, $rows); + } + + // ====== ====== ====== ====== ====== ====== 转交操作 ====== ====== ====== ====== ====== ====== + + /** + * 查看某个实例的转交流水(按时间升序) + */ + public function listTransfers(int $instanceId): array + { + $rows = Db::table('approval_transfers') + ->where('instance_id', $instanceId) + ->orderBy('id', 'asc') + ->get() + ->toArray(); + + return array_map(function($r){ + return [ + 'id' => (int)$r->id, + 'instance_id' => (int)$r->instance_id, + 'step_index' => (int)$r->step_index, + 'record_id' => (int)$r->record_id, + 'from_user_id' => (int)$r->from_user_id, + 'to_user_id' => (int)$r->to_user_id, + 'reason' => $r->reason, + 'created_at' => $r->created_at, + ]; + }, $rows); + } + + + /** + * 转交当前待办给他人(原地替换 approver_id)并在通知里追加“来源/原因” + */ + public function transferApproval( + int $instanceId, + int $stepIndex, + int $fromUserId, + int $toUserId, + string $reason = '', + ?string $userNameColumn = null // 可选:传 'name' 或 'nickname' 来展示姓名 + ): array { + if ($fromUserId === $toUserId) { + return ['status' => 'noop', 'msg' => '目标与来源相同,无需转交']; + } + + return \support\Db::transaction(function () use ($instanceId, $stepIndex, $fromUserId, $toUserId, $reason, $userNameColumn) { + // 锁实例,避免并发推进 + $instance = Db::table('approval_instances')->where('id', $instanceId)->lockForUpdate()->first(); + if (!$instance) throw new BusinessException('审批实例未找到'); + if ($instance->status !== 'processing') throw new BusinessException('流程非进行中,无法转交'); + + // 锁定来源人的 pending 记录 + $record = Db::table('approval_records') + ->where('instance_id', $instanceId) + ->where('round', (int)$instance->round) + ->where('step_index', $stepIndex) + ->where('approver_id', $fromUserId) + ->where('status', 'pending') + ->lockForUpdate() + ->first(); + if (!$record) throw new BusinessException('未找到可转交的待办或已处理'); + + $now = date('Y-m-d H:i:s'); + + // 若目标已有 pending:合并(本记录置为 skipped) + $target = Db::table('approval_records') + ->where('instance_id', $instanceId) + ->where('round', (int)$instance->round) + ->where('step_index', $stepIndex) + ->where('approver_id', $toUserId) + ->where('status', 'pending') + ->lockForUpdate() + ->first(); + + if ($target) { + Db::table('approval_records')->where('id', $record->id)->update([ + 'status' => 'skipped', + 'approved_at' => $now, + 'updated_at' => $now, + 'comment' => trim(($record->comment ?? '')."\n[转交给 {$toUserId},目标已有待办,已合并]"), + ]); + $effectiveRecordId = (int)$target->id; + } else { + // 原地替换 approver_id + Db::table('approval_records')->where('id', $record->id)->update([ + 'approver_id' => $toUserId, + 'updated_at' => $now, + 'comment' => trim(($record->comment ?? '')."\n[自 {$fromUserId} 转交]"), + ]); + $effectiveRecordId = (int)$record->id; + } + + // 组织“转交来源/原因”文案(优先姓名) + $fromLabel = (string)$fromUserId; + $toLabel = (string)$toUserId; + if ($userNameColumn) { + $fromName = Db::table('user')->where('id', $fromUserId)->value($userNameColumn); + $toName = Db::table('user')->where('id', $toUserId)->value($userNameColumn); + if ($fromName) $fromLabel = $fromName; + if ($toName) $toLabel = $toName; + } + $note = "【转交】{$fromLabel} → {$toLabel}"; + $r = trim($reason); + if ($r !== '') $note .= ",原因:{$r}"; + + // 通知被转交人(在内容里追加 note) + $this->sendNotification([$toUserId], (object)[ + 'id' => (int)$instance->id, + 'flow_type' => (string)$instance->flow_type, + 'biz_type' => (string)$instance->biz_type, + 'biz_id' => (string)$instance->biz_id, + 'initiator_id' => (int)$instance->initiator_id, + 'form_data' => $instance->form_data, + ], 'pending', false, ['note' => $note]); + + // 记转交流水 + Db::table('approval_transfers')->insert([ + 'instance_id' => (int)$instanceId, + 'step_index' => (int)$stepIndex, + 'record_id' => $effectiveRecordId, + 'from_user_id' => (int)$fromUserId, + 'to_user_id' => (int)$toUserId, + 'reason' => $reason, + 'created_at' => $now, + ]); + + $this->pushTodoChanged([$fromUserId, $toUserId]); + return ['status' => 'ok', 'record_id' => $effectiveRecordId]; + }); + } + + + + /** 新增:发起“作废审批” */ + public function startNullify( + string $voidFlowType, // 例如 'stock_orders_nullify' + string $bizType, // 原业务类型,如 'stock_orders' + string $bizId, // 原业务ID + array $formData, // 可包含 ['nullify_reason'=>...] + int $initiatorId + ): array { + return Db::transaction(function () use ($voidFlowType, $bizType, $bizId, $formData, $initiatorId) { + // 1) 流程定义校验 + $flow = Db::table('approval_flows')->where('type', $voidFlowType)->where('is_active', 1)->first(); + if (!$flow) throw new BusinessException("作废流程未找到或已停用: {$voidFlowType}"); + $steps = json_decode($flow->steps, true); + if (!is_array($steps) || empty($steps)) throw new BusinessException("作废流程步骤配置错误"); + + // 2) 必须存在“已通过”的最新有效实例(非作废流程) + $latestApproved = Db::table('approval_instances') + ->where('biz_type', $bizType) + ->where('biz_id', $bizId) + ->where('flow_type', '!=', $voidFlowType) + ->where('status', 'approved') + ->orderByDesc('id') + ->first(); + + if (!$latestApproved) { + throw new BusinessException('仅已通过的单据可发起作废'); + } + + // 3) 不允许并行的作废流程 + $voidProcessing = Db::table('approval_instances') + ->where('flow_type', $voidFlowType) + ->where('biz_id', $bizId) + ->where('status', 'processing') + ->exists(); + if ($voidProcessing) { + throw new BusinessException('已存在进行中的作废审批'); + } + + // A. 基于历史“作废流程已通过”的实例拦截 + $voidApproved = Db::table('approval_instances') + ->where('flow_type', $voidFlowType) + ->where('biz_id', $bizId) + ->whereIn('status', ['approved','voided']) + ->exists(); + if ($voidApproved) { + throw new BusinessException('该单据已作废,无需重复作废'); + } + + // B.(可选)直接查业务表状态拦截(若你用 approval_status=4 表示已作废) + /* + $orderVoided = \app\common\model\StockOrders::where('id', $bizId) + ->where('approval_status', 4) + ->exists(); + if ($orderVoided) { + throw new BusinessException('该单据已作废,无需重复作废'); + } + */ + + // 4) 计算首步 + // 注:作废流程可以有独立条件;若不需要条件,直接第一步 + $formData = array_merge([ + '_op' => 'nullify', + '_nullify_reason' => $formData['nullify_reason'] ?? ($formData['reason'] ?? ''), + '_nullify_of_instance_id' => (int)$latestApproved->id, + ], $formData); + + $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); + } + + // 5) 新建作废实例(round 固定从 1 开始) + $instanceId = Db::table('approval_instances')->insertGetId([ + 'flow_type' => $voidFlowType, + '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, + 'round' => 1, // + 'deadline_at' => $deadlineAt, + 'created_at' => $now, + 'updated_at' => $now, + ]); + + $instance = (object)[ + 'id' => $instanceId, + 'flow_type' => $voidFlowType, + 'biz_type' => $bizType, + 'biz_id' => $bizId, + 'form_data' => json_encode($formData, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), + 'initiator_id' => $initiatorId, + 'current_step' => $firstIndex, + 'status' => 'processing', + 'round' => 1, // + ]; + + // 6) 生成首步待办 + 通知(带上作废原因) + $approverIds = $this->resolveApprovers($firstStep, $instance, true); + foreach ($approverIds as $uid) { + $this->insertApprovalRecord($instance, $firstIndex, $uid); + } + $extra = []; + if (!empty($formData['_nullify_reason'])) { + $extra['note'] = '【作废原因】'.$formData['_nullify_reason']; + } + $this->sendNotification($approverIds, $instance, 'pending', false, $extra); + $this->notifyCCs($firstStep, $instance, 'pending'); + $this->pushTodoChanged($approverIds); + return ['instance_id' => $instanceId]; + }); + } + + + // A) 取组内全部成员 + private function getGroupMemberIds(int $groupId): array + { + $rows = Db::table('user_group_members') + ->where('group_id', $groupId) + ->where('status', 1) + ->pluck('user_id')->toArray(); + + return array_values(array_unique(array_map('intval', $rows))); + } + + // B) 按“场景”在发起人所在的组里找负责人(支持主归属/排序兜底) + private function getLeadersByScenarioForInitiator( + int $initiatorId, + string $scenarioCode, + bool $mustBelong = true, + bool $preferPrimary = true + ): array { + // 找出“该场景下发起人所在的所有组” + $groups = Db::table('user_group as g') + ->join('user_group_members as m', 'm.group_id', '=', 'g.id') + ->where('g.scenario_code', $scenarioCode) + ->where('g.status', 1) + ->where('m.user_id', $initiatorId) + ->where('m.status', 1) + ->orderByDesc('m.is_primary') // 主归属优先 + ->orderByDesc('g.sort') // 其次按组排序 + ->select('g.id', 'm.is_primary') + ->get()->toArray(); + + if (empty($groups)) { + if ($mustBelong) { + throw new BusinessException('发起人在该业务场景下未加入任何分组'); + } + // 不强制属于:允许只按“该场景下任一组”的负责人(不推荐) + $groups = Db::table('user_group as g') + ->where('g.scenario_code', $scenarioCode) + ->where('g.status', 1) + ->orderByDesc('g.sort') + ->select('g.id', Db::raw('0 as is_primary')) + ->get()->toArray(); + if (empty($groups)) { + throw new BusinessException('该业务场景下未配置任何分组'); + } + } + + // 选择一个“目标组” + $targetGroupId = null; + if ($preferPrimary) { + foreach ($groups as $g) { + if ((int)$g->is_primary === 1) { $targetGroupId = (int)$g->id; break; } + } + } + if (!$targetGroupId) { + $targetGroupId = (int)$groups[0]->id; // 用排序第一的组 + } + + // 取负责人(可支持多负责人) + $leaders = Db::table('user_group_members') + ->where('group_id', $targetGroupId) + ->where('is_leader', 1) + ->where('status', 1) + ->pluck('user_id')->toArray(); + + $ids = array_values(array_unique(array_map('intval', $leaders))); + if (empty($ids)) throw new BusinessException('目标分组未配置负责人'); + + return $ids; + } + + // + private function isNullifyFlow(string $flowType): bool + { + $t = strtolower(trim($flowType)); + return str_ends_with($t, '_nullify') || in_array($t, [ + // 所有“作废”类流程的类型 + 'stock_orders_nullify', + ], true); + } + + // 放到工具方法区 + private function normalizeFinalStatus(string $flowType, string $final): string + { + if ($this->isNullifyFlow($flowType) && $final === 'approved') { + return 'voided'; + } + return $final; + } + + /** WebSocket:把一组用户的“待办数量”推送给前端 */ + private function pushTodoChanged22(array $userIds): void + { + $userIds = array_values(array_unique(array_map('intval', $userIds))); + if (empty($userIds)) return; + + // 统计这些用户当前的待办数量(以 approval_records.status = 'pending' 为准) + $rows = Db::table('approval_records') + ->whereIn('approver_id', $userIds) + ->where('status', 'pending') + ->groupBy('approver_id') + ->select('approver_id', Db::raw('COUNT(*) as cnt')) + ->get(); + + // 默认都置 0,再填充真实值 + $payload = array_fill_keys($userIds, 0); + foreach ($rows as $r) { + $payload[(int)$r->approver_id] = (int)$r->cnt; + } + + try { + foreach ($payload as $uid => $cnt) { + $data = [ + 'event' => 'approval.todo.counts', + 'user_id' => $uid, + 'num' => $cnt, + ]; + \Webman\RedisQueue\Redis::send('ws-msg', $data); + } + } catch (\Throwable $e) { + Log::error('ws todo push error: '.$e->getMessage()); + } + } + + /** 取“某实例、某轮次、某步骤”里当前 pending 的审批人(用于推送受影响的用户) */ + private function getPendingApproverIds(int $instanceId, int $round, int $stepIndex): array + { + $ids = Db::table('approval_records') + ->where('instance_id', $instanceId) + ->where('round', $round) + ->where('step_index', $stepIndex) + ->where('status', 'pending') + ->pluck('approver_id')->toArray(); + + return array_values(array_unique(array_map('intval', $ids))); + } + + + /** 登录/首屏初始化:获取当前用户的待办总数(仅统计当前步骤、进行中的实例) */ + public function getInitTodoCount(int $userId): int + { + $userId = (int)$userId; + if ($userId <= 0) { + throw new BusinessException('无效的用户ID'); + } + + $row = Db::table('approval_records as ar') + ->join('approval_instances as ai', 'ai.id', '=', 'ar.instance_id') + ->where('ar.approver_id', $userId) + ->where('ar.status', 'pending') + ->where('ai.status', 'processing') + // 只统计实例“当前轮次、当前步骤”的待办,避免历史残留干扰 + ->whereRaw('ar.round = ai.round') + ->whereRaw('ar.step_index = ai.current_step') + ->selectRaw('COUNT(*) as c') + ->first(); + + return (int)($row->c ?? 0); + } + + /** WebSocket:把一组用户的“待办数量”推送给前端(与 getInitTodoCount 统一口径) */ + private function pushTodoChanged(array $userIds): void + { + $userIds = array_values(array_unique(array_map('intval', $userIds))); + if (empty($userIds)) return; + + // 只统计:实例进行中 + 当前轮次 + 当前步骤 的 pending + $rows = Db::table('approval_records as ar') + ->join('approval_instances as ai', 'ai.id', '=', 'ar.instance_id') + ->whereIn('ar.approver_id', $userIds) + ->where('ar.status', 'pending') + ->where('ai.status', 'processing') + ->whereRaw('ar.round = ai.round') + ->whereRaw('ar.step_index = ai.current_step') + ->groupBy('ar.approver_id') + ->select('ar.approver_id', Db::raw('COUNT(*) as cnt')) + ->get(); + + // 默认置 0,再填充真实值 + $payload = array_fill_keys($userIds, 0); + foreach ($rows as $r) { + $payload[(int)$r->approver_id] = (int)$r->cnt; + } + + try { + foreach ($payload as $uid => $cnt) { + (new \app\common\utils\WsEvents())->todoCounts($uid, $cnt); + } + } catch (\Throwable $e) { + Log::error('ws todo push error: '.$e->getMessage()); + } + } +} \ No newline at end of file diff --git a/app/common/utils/approval/traits/ApprovalDiscussionTrait.php b/app/common/utils/approval/traits/ApprovalDiscussionTrait.php new file mode 100644 index 0000000..3a74940 --- /dev/null +++ b/app/common/utils/approval/traits/ApprovalDiscussionTrait.php @@ -0,0 +1,151 @@ +where('id', $instanceId)->first(); + if (!$inst) return false; + if ((int)$inst->initiator_id === $userId) return true; + + $inRecords = Db::table('approval_records') + ->where('instance_id', $instanceId)->where('approver_id', $userId)->exists(); + if ($inRecords) return true; + + $inCc = Db::table('approval_ccs') + ->where('instance_id', $instanceId)->where('user_id', $userId)->exists(); + return $inCc; + } + + /** 是否该实例任一步的审批人 */ + private function isApproverOfInstance(int $instanceId, int $userId): bool + { + return Db::table('approval_records') + ->where('instance_id', $instanceId) + ->where('approver_id', $userId) + ->exists(); + } + + /** 获取讨论受众 */ + private function getDiscussionAudienceUserIds(int $instanceId, bool $approversOnly, int $excludeUserId): array + { + $uids = []; + if (!$approversOnly) { + $inst = Db::table('approval_instances')->where('id', $instanceId)->first(); + if ($inst) $uids[] = (int)$inst->initiator_id; + $ccs = Db::table('approval_ccs')->where('instance_id', $instanceId)->pluck('user_id')->toArray(); + foreach ($ccs as $u) $uids[] = (int)$u; + } + $approvers = Db::table('approval_records') + ->where('instance_id', $instanceId) + ->distinct()->pluck('approver_id')->toArray(); + foreach ($approvers as $u) $uids[] = (int)$u; + + $uids = array_values(array_unique(array_filter($uids, fn($u) => $u !== $excludeUserId))); + return $uids; + } + + /** 发表讨论 */ + public function postDiscussion( + int $instanceId, + int $userId, + string $content, + bool $isInternal = false, + array $attachments = [], + ?string $userNameColumn = null + ): array { + $content = trim($content); + if ($content === '') throw new BusinessException('讨论内容不能为空'); + if (!$this->isParticipantOfInstance($instanceId, $userId)) { + throw new BusinessException('无权参与该实例的讨论'); + } + if ($isInternal && !$this->isApproverOfInstance($instanceId, $userId)) { + throw new BusinessException('仅审批人可发布内部讨论'); + } + + $now = date('Y-m-d H:i:s'); + $id = Db::table('approval_discussions')->insertGetId([ + 'instance_id' => $instanceId, + 'user_id' => $userId, + 'is_internal' => $isInternal ? 1 : 0, + 'content' => $content, + 'attachments' => empty($attachments) ? null : json_encode($attachments, JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES), + 'created_at' => $now, + 'updated_at' => $now, + ]); + + // 通知其他人 + $inst = Db::table('approval_instances')->where('id', $instanceId)->first(); + if ($inst) { + $audience = $this->getDiscussionAudienceUserIds($instanceId, $isInternal, $userId); + // 发言人姓名(可选) + $speaker = (string)$userId; + if ($userNameColumn) { + $name = Db::table('user')->where('id', $userId)->value($userNameColumn); + if ($name) $speaker = $name; + } + $snippet = function_exists('mb_strimwidth') ? mb_strimwidth($content, 0, 80, '...', 'UTF-8') : $content; + $extra = ['note' => "【讨论】{$speaker}:{$snippet}"]; + + $this->sendNotification( + $audience, + (object)[ + 'id' => (int)$inst->id, + 'flow_type' => (string)$inst->flow_type, + 'initiator_id' => (int)$inst->initiator_id, + 'form_data' => $inst->form_data, + 'biz_type' => (string)$inst->biz_type, + 'biz_id' => (string)$inst->biz_id, + ], + 'pending', + false, + $extra + ); + } + + return [ + 'id' => (int)$id, + 'instance_id' => $instanceId, + 'user_id' => $userId, + 'is_internal' => (bool)$isInternal, + 'content' => $content, + 'attachments' => $attachments, + 'created_at' => $now, + ]; + } + + /** 获取讨论列表 */ + public function listDiscussions(int $instanceId, ?int $afterId = null, int $limit = 50, ?string $userNameColumn = null): array + { + $q = Db::table('approval_discussions as d')->where('d.instance_id', $instanceId); + if ($afterId) $q->where('d.id', '>', $afterId); + $q->orderBy('d.id', 'asc')->limit(max(1, min(200, $limit))); + + if ($userNameColumn) { + $rows = $q->leftJoin('user as u', 'd.user_id', '=', 'u.id') + ->select('d.*', Db::raw("u.`{$userNameColumn}` as user_name")) + ->get()->toArray(); + } else { + $rows = $q->select('d.*')->get()->toArray(); + } + + return array_map(function($r){ + return [ + 'id' => (int)$r->id, + 'instance_id' => (int)$r->instance_id, + 'user_id' => (int)$r->user_id, + 'is_internal' => (bool)$r->is_internal, + 'content' => $r->content, + 'attachments' => $r->attachments ? json_decode($r->attachments, true) : [], + 'created_at' => $r->created_at, + 'updated_at' => $r->updated_at, + 'user_name' => property_exists($r, 'user_name') ? $r->user_name : null, + ]; + }, $rows); + } +} diff --git a/app/common/utils/approval/traits/ApprovalEngineTrait.php b/app/common/utils/approval/traits/ApprovalEngineTrait.php new file mode 100644 index 0000000..b1fa986 --- /dev/null +++ b/app/common/utils/approval/traits/ApprovalEngineTrait.php @@ -0,0 +1,592 @@ +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, + 'round' => 1, + 'deadline_at' => $deadlineAt, + 'created_at' => $now, + 'updated_at' => $now, + ]); + + $instance = (object)[ + 'id' => $instanceId, + 'flow_type' => $flowType, + 'form_data' => json_encode($formData, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), + 'initiator_id' => $initiatorId, + 'current_step' => $firstIndex, + 'status' => 'processing', + 'flow_type' => $flowType, + 'biz_type' => $bizType, + 'biz_id' => $bizId, + 'round' => 1, + ]; + + // 解析首步审批人(支持 role_id 或 roles.name) + $approverIds = $this->resolveApprovers($firstStep, $instance, true); + foreach ($approverIds as $uid) { + $this->insertApprovalRecord($instance, $firstIndex, $uid); + } + + $extra = []; + $submitNote = $formData['submit_note'] ?? ''; + if (is_string($submitNote) && $submitNote !== '') { + $extra['note'] = $submitNote; + } + $this->sendNotification($approverIds, $instance, 'pending', false, $extra); + + // $this->notifyCCs($firstStep, $instance, 'pending'); + + $this->pushTodoChanged($approverIds); + + 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', '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; + $round = (int)$instance->round; + $step = $steps[$idx] ?? null; + if (!$step) throw new BusinessException("当前步骤配置错误"); + + // 找到该用户在本步的 pending 记录(加锁) + $pendingRecord = Db::table('approval_records') + ->where('instance_id', $instanceId) + ->where('round', $round) + ->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('round', $round) + ->where('approver_id', $approverId) + ->where('step_index', $idx) + ->first(); + if ($anyRecord && $anyRecord->status !== 'pending') { + return ['status' => 'noop', 'msg' => '已处理']; + } + // 步骤已推进 + if ((int)$instance->current_step !== $idx) { + return ['status' => 'noop', 'msg' => '非当前审批']; + } + 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, + ]); + + // 异步审批回调 9.23 add + $approvalcallback = [ + 'biz_id' => $instance->biz_id, + 'flow_type' => $instance->flow_type, + 'instance_id' => $instance->id, + 'initiator_id' => $instance->initiator_id, + 'status' => $action + ]; + \Webman\RedisQueue\Redis::send('approval-callback', $approvalcallback); + + // 2) 聚合本步结论(any / all) + $typeRaw = strtolower($step['type'] ?? 'all'); + $type = in_array($typeRaw, ['any','all']) ? $typeRaw : 'all'; + $outcome = $this->getStepOutcome($instanceId, $round, $idx, $type); // 'pending' | 'approve' | 'reject' + + if ($outcome === 'pending') { + $this->pushTodoChanged([$approverId]); + return ['status' => 'step_pending', 'msg' => '审批待定']; + } + + $otherPendingUids = $this->getPendingApproverIds($instanceId, $round, $idx); + + // 3) 有结论:关闭本步其余 pending + $this->closeOtherPending($instanceId, $round, $idx, [$approverId]); + + // 这些人 pending→skipped,待办数减少 + $this->pushTodoChanged($otherPendingUids); + + // 4) 拒绝 => 结束 + if ($outcome === 'reject') { + Db::table('approval_instances')->where('id', $instanceId)->update([ + 'status' => 'rejected', + 'finished_at' => $now, + 'updated_at' => $now, + ]); + $this->onFlowFinalized($instance, 'rejected'); + $this->sendNotification([$instance->initiator_id], $instance, 'reject', false); + $this->pushTodoChanged([$approverId]); + return ['status' => 'completed', 'msg' => '拒绝']; + } + + // 5) 通过 => 进入下一步或结束 + $formData = json_decode($instance->form_data, true) ?: []; + $nextIndex = $this->getNextStepIndex($steps, $formData, $idx); + + if ($nextIndex === null) { + $finalStatus = $this->normalizeFinalStatus((string)$instance->flow_type, 'approved'); + $notifyAction = ($finalStatus === 'voided') ? 'voided' : 'approve'; + + Db::table('approval_instances')->where('id', $instanceId)->update([ + 'status' => $finalStatus, + 'finished_at' => $now, + 'updated_at' => $now, + ]); + $this->onFlowFinalized($instance, $finalStatus); + + // 新增:在流程最终完成时才发本步骤的抄送 + if (!empty($step['cc'])) { + $this->notifyCCs($step, $instance, $notifyAction); + } + + $this->sendNotification([$instance->initiator_id], $instance, $notifyAction, false); + $this->pushTodoChanged([$approverId]); + return ['status' => 'completed', 'msg' => ($finalStatus === 'voided' ? '已作废' : '同意')]; + } + + // 乐观推进 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', 'msg' => '并发进展']; + } + + // 生成下一步待办 + 通知 + $nextStep = $steps[$nextIndex]; + $nextApprovers = $this->resolveApprovers($nextStep, $instance, true); + foreach ($nextApprovers as $uid) { + $this->insertApprovalRecord($instance, $nextIndex, $uid); + } + $this->sendNotification($nextApprovers, $instance, 'pending', false); + // $this->notifyCCs($nextStep, $instance, 'pending'); + $this->pushTodoChanged(array_merge([$approverId], $nextApprovers)); + 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, + ]); + + $this->onFlowFinalized($instance, 'revoked'); + + $pendingUids = Db::table('approval_records') + ->where('instance_id', $instanceId) + ->where('status', 'pending') + ->pluck('approver_id')->toArray(); + + 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); + $this->pushTodoChanged($pendingUids); + return ['status' => 'completed', 'result' => 'revoked']; + }); + } + + /** 定时任务:处理超时(实例级) */ + 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'; + $final = $this->normalizeFinalStatus((string)$instance->flow_type, $final); + $finalAction = ($final === 'voided') ? 'voided' : (($final === 'approved') ? 'approve' : 'reject'); + + Db::table('approval_instances')->where('id', $instance->id)->update([ + 'status' => $final, + 'finished_at' => $now, + 'updated_at' => $now, + ]); + + $this->onFlowFinalized((object)$instance, $final); + + $pendingUids = Db::table('approval_records') + ->where('instance_id', $instance->id) + ->where('round', (int)$instance->round) + ->where('step_index', $instance->current_step) + ->where('status', 'pending') + ->pluck('approver_id')->toArray(); + + Db::table('approval_records') + ->where('instance_id', $instance->id) + ->where('round', (int)$instance->round) + ->where('step_index', $instance->current_step) + ->where('status', 'pending') + ->update(['status' => 'timeout', 'approved_at' => $now, 'updated_at' => $now]); + + $this->notifyInitiator($instance, $finalAction); + + $this->pushTodoChanged($pendingUids); + }); + } + } + + /** 重提交(仅对 status=rejected 的实例;支持申诉说明) */ + public function resubmit(int $instanceId, int $initiatorId, array $patchData = [], string $appealText = ''): array + { + return Db::transaction(function () use ($instanceId, $initiatorId, $patchData, $appealText) { + $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 !== 'rejected') 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) ?: []; + + // 合并改动 & 打上重提标记 + $formData = array_merge($formData, $patchData); + $formData['resubmitted'] = 1; + + $appealText = trim((string)$appealText); + if ($appealText !== '') { + $formData['appeal_last'] = $appealText; + } + + // 计算重提后首个命中的步骤 + $firstIndex = $this->getNextStepIndex($steps, $formData, -1); + if ($firstIndex === null) throw new BusinessException('重提后未找到可执行的步骤'); + + $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); + } + + $oldPendingUids = Db::table('approval_records') + ->where('instance_id', $instanceId) + ->where('status', 'pending') + ->pluck('approver_id')->toArray(); + + // 清理可能遗留的 pending + Db::table('approval_records') + ->where('instance_id', $instanceId) + ->where('status', 'pending') + ->update(['status'=>'skipped','approved_at'=>$now,'updated_at'=>$now]); + + // 轮次 +1 + $newRound = ((int)$instance->round) + 1; + + // 重新开启实例 + Db::table('approval_instances')->where('id', $instanceId)->update([ + 'status' => 'processing', + 'current_step' => $firstIndex, + 'round' => $newRound, + 'form_data' => json_encode($formData, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), + 'finished_at' => null, + 'deadline_at' => $deadlineAt, + 'updated_at' => $now, + ]); + + // 创建待办 + 通知 + $firstStep = $steps[$firstIndex]; + $instanceLite = (object)[ + 'id' => (int)$instanceId, + 'flow_type' => (string)$instance->flow_type, + 'form_data' => json_encode($formData, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), + 'initiator_id' => (int)$instance->initiator_id, + 'biz_type' => (string)$instance->biz_type, + 'biz_id' => (string)$instance->biz_id, + 'round' => $newRound, + ]; + $approvers = $this->resolveApprovers($firstStep, $instanceLite, true); + if (empty($approvers)) throw new BusinessException('重提后首步未解析到审批人'); + + foreach ($approvers as $uid) { + $this->insertApprovalRecord($instanceLite, $firstIndex, (int)$uid); + } + + $extra = $appealText !== '' ? ['appeal' => $appealText] : []; + $this->sendNotification($approvers, $instanceLite, 'pending', false, $extra); + $this->notifyCCs($firstStep, $instanceLite, 'pending'); + $this->pushTodoChanged(array_merge($oldPendingUids, $approvers)); + + return [ + 'status' => 'resubmitted', + 'instance_id' => (int)$instanceId, + 'current_step' => $firstIndex, + 'step_name' => $firstStep['name'] ?? ('Step '.$firstIndex), + ]; + }); + } + + /** 新增:发起“作废审批” */ + public function startNullify( + string $voidFlowType, + string $bizType, + string $bizId, + array $formData, + int $initiatorId + ): array { + return Db::transaction(function () use ($voidFlowType, $bizType, $bizId, $formData, $initiatorId) { + // 1) 流程定义校验 + $flow = Db::table('approval_flows')->where('type', $voidFlowType)->where('is_active', 1)->first(); + if (!$flow) throw new BusinessException("作废流程未找到或已停用: {$voidFlowType}"); + $steps = json_decode($flow->steps, true); + if (!is_array($steps) || empty($steps)) throw new BusinessException("作废流程步骤配置错误"); + + // 2) 必须存在“已通过”的最新有效实例(非作废流程) + $latestApproved = Db::table('approval_instances') + ->where('biz_type', $bizType) + ->where('biz_id', $bizId) + ->where('flow_type', '!=', $voidFlowType) + ->where('status', 'approved') + ->orderByDesc('id') + ->first(); + + if (!$latestApproved) { + throw new BusinessException('仅已通过的单据可发起作废'); + } + + // 3) 不允许并行的作废流程 + $voidProcessing = Db::table('approval_instances') + ->where('flow_type', $voidFlowType) + ->where('biz_id', $bizId) + ->where('status', 'processing') + ->exists(); + if ($voidProcessing) { + throw new BusinessException('已存在进行中的作废审批'); + } + + // A. 基于历史“作废流程已通过”的实例拦截 + $voidApproved = Db::table('approval_instances') + ->where('flow_type', $voidFlowType) + ->where('biz_id', $bizId) + ->whereIn('status', ['approved','voided']) + ->exists(); + if ($voidApproved) { + throw new BusinessException('该单据已作废,无需重复作废'); + } + + // B.(可选)直接查业务表状态拦截(若你用 approval_status=4 表示已作废) + /* + $orderVoided = \app\common\model\StockOrders::where('id', $bizId) + ->where('approval_status', 4) + ->exists(); + if ($orderVoided) { + throw new BusinessException('该单据已作废,无需重复作废'); + } + */ + + // 4) 计算首步 + // 注:作废流程可以有独立条件;若不需要条件,直接第一步 + $formData = array_merge([ + '_op' => 'nullify', + '_nullify_reason' => $formData['nullify_reason'] ?? ($formData['reason'] ?? ''), + '_nullify_of_instance_id' => (int)$latestApproved->id, + ], $formData); + + $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); + } + + // 5) 新建作废实例(round 固定从 1 开始) + $instanceId = Db::table('approval_instances')->insertGetId([ + 'flow_type' => $voidFlowType, + '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, + 'round' => 1, // + 'deadline_at' => $deadlineAt, + 'created_at' => $now, + 'updated_at' => $now, + ]); + + $instance = (object)[ + 'id' => $instanceId, + 'flow_type' => $voidFlowType, + 'biz_type' => $bizType, + 'biz_id' => $bizId, + 'form_data' => json_encode($formData, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), + 'initiator_id' => $initiatorId, + 'current_step' => $firstIndex, + 'status' => 'processing', + 'round' => 1, // + ]; + + // 6) 生成首步待办 + 通知(带上作废原因) + $approverIds = $this->resolveApprovers($firstStep, $instance, true); + foreach ($approverIds as $uid) { + $this->insertApprovalRecord($instance, $firstIndex, $uid); + } + $extra = []; + if (!empty($formData['_nullify_reason'])) { + $extra['note'] = '【作废原因】'.$formData['_nullify_reason']; + } + $this->sendNotification($approverIds, $instance, 'pending', false, $extra); + $this->notifyCCs($firstStep, $instance, 'pending'); + $this->pushTodoChanged($approverIds); + return ['instance_id' => $instanceId]; + }); + } + + // 流程完结后的业务回调(approved|rejected|revoked|timeout?) + private function onFlowFinalized(object $instance, string $final): void + { + try { + switch ($instance->flow_type) { + case 'stock_orders': { + $order = \app\common\model\StockOrders::find($instance->biz_id); + if (!$order) { + throw new BusinessException('订单不存在'); + } + if ($final === 'approved') { + $order->approval_status = 2; + $order->save(); + } elseif ($final === 'rejected') { + $order->approval_status = 3; + $order->save(); + } elseif ($final === 'revoked') { + $order->approval_status = 0; + $order->save(); + } + break; + } + case 'stock_orders_nullify': + if ($final === 'approved') { + $final = 'voided'; + Db::table('approval_instances')->where('id', $instance->id)->update([ + 'status' => 'voided', + ]); + } + if (in_array($final, ['voided','approved'], true)) { + $order = \app\common\model\StockOrders::find($instance->biz_id); + if ($order && $order->approval_status !== 4) { + $order->approval_status = 4; + $order->save(); + } + } + break; + default: + // 非作废流程,无需处理 + break; + } + } catch (\Throwable $e) { + Log::error('onFlowFinalized error: '.$e->getMessage()); + } + } + + // 是否作废流程 + private function isNullifyFlow(string $flowType): bool + { + $t = strtolower(trim($flowType)); + return str_ends_with($t, '_nullify') || in_array($t, [ + // 所有“作废”类流程的类型 + 'stock_orders_nullify', + ], true); + } + + // 归一化最终状态 + private function normalizeFinalStatus(string $flowType, string $final): string + { + if ($this->isNullifyFlow($flowType) && $final === 'approved') { + return 'voided'; + } + return $final; + } +} diff --git a/app/common/utils/approval/traits/ApprovalNotificationTrait.php b/app/common/utils/approval/traits/ApprovalNotificationTrait.php new file mode 100644 index 0000000..4ad1f6b --- /dev/null +++ b/app/common/utils/approval/traits/ApprovalNotificationTrait.php @@ -0,0 +1,155 @@ + '待审批', + 'approve' => '已通过', + 'reject' => '已拒绝', + 'revoked' => '已撤回', + 'voided' => '已作废', + ]; + $prefix = $isCC ? '【抄送】' : '【审批】'; + $text = $map[$action] ?? $action; + + // 业务标题/内容 + $approvalType = array_flip(\app\common\service\SysDictService::pluckKeyValueByCode('approval_type')); + + $flowTxt = $approvalType[$instance->flow_type] ?? (string)$instance->flow_type; + $title = $prefix.'审批通知'; + $content = "{$prefix}你有一条审批 {$text}:类型 {$flowTxt},业务标识 {$instance->biz_id}"; + + switch ($instance->flow_type) { + case 'stock_orders': + // 备货单 + $stockOrder = \app\common\model\StockOrders::find($instance->biz_id); + if ($stockOrder) { + $title = $prefix."备货单:{$stockOrder->order_no}"; + $flowTxt = $approvalType[$instance->flow_type] ?? $instance->flow_type; + $content = "{$prefix}你有一条审批 {$text}:类型 {$flowTxt},业务单号 {$stockOrder->order_no}"; + } + break; + case 'stock_orders_nullify': + // 备货单作废 + $stockOrder = \app\common\model\StockOrders::find($instance->biz_id); + if ($stockOrder) { + $title = $prefix."备货单作废:{$stockOrder->order_no}"; + $flowTxt = $approvalType[$instance->flow_type] ?? '备货单作废'; + $content = "{$prefix}你有一条审批 {$text}:类型 {$flowTxt},业务单号 {$stockOrder->order_no}"; + // 把作废原因附带展示更清晰(也可依赖 $extra['note']) + $fd = json_decode($instance->form_data ?? '{}', true) ?: []; + if (!empty($fd['_nullify_reason'])) { + $content .= ';原因:'.(function_exists('mb_strimwidth') ? mb_strimwidth($fd['_nullify_reason'], 0, 120, '...', 'UTF-8') : $fd['_nullify_reason']); + } + } + break; + case 'purchase_orders': + // 采购单 + $purchaseOrder = \app\common\model\PurchaseOrders::find($instance->biz_id); + if ($purchaseOrder) { + $title = $prefix."采购单:{$purchaseOrder->order_no}"; + $flowTxt = $approvalType[$instance->flow_type] ?? $instance->flow_type; + $content = "{$prefix}你有一条审批 {$text}:类型 {$flowTxt},业务单号 {$purchaseOrder->order_no}"; + } + break; + case 'purchase_order': + // 采购单 + break; + } + + // 附加说明(提交说明/申诉/讨论摘要) + $append = $extra['appeal'] ?? ($extra['note'] ?? null); + if ($append) { + if (function_exists('mb_strimwidth')) { + $append = mb_strimwidth($append, 0, 120, '...', 'UTF-8'); + } + $content .= ";说明:{$append}"; + } + + noticePush([ + 'title' => $title, + 'content' => $content, + 'recipient' => $userIds, + 'email' => 1, + ]); + } + + private function notifyInitiator(object $instance, string $result): void + { + $this->sendNotification([(int)$instance->initiator_id], $instance, $result, false); + } + + /** WebSocket:把一组用户的“待办数量”推送给前端(旧版) */ + private function pushTodoChanged22(array $userIds): void + { + $userIds = array_values(array_unique(array_map('intval', $userIds))); + if (empty($userIds)) return; + + // 统计这些用户当前的待办数量(以 approval_records.status = 'pending' 为准) + $rows = Db::table('approval_records') + ->whereIn('approver_id', $userIds) + ->where('status', 'pending') + ->groupBy('approver_id') + ->select('approver_id', Db::raw('COUNT(*) as cnt')) + ->get(); + + // 默认都置 0,再填充真实值 + $payload = array_fill_keys($userIds, 0); + foreach ($rows as $r) { + $payload[(int)$r->approver_id] = (int)$r->cnt; + } + + try { + foreach ($payload as $uid => $cnt) { + $data = [ + 'event' => 'approval.todo.counts', + 'user_id' => $uid, + 'num' => $cnt, + ]; + \Webman\RedisQueue\Redis::send('ws-msg', $data); + } + } catch (\Throwable $e) { + Log::error('ws todo push error: '.$e->getMessage()); + } + } + + /** WebSocket:把一组用户的“待办数量”推送给前端(统一口径) */ + private function pushTodoChanged(array $userIds): void + { + $userIds = array_values(array_unique(array_map('intval', $userIds))); + if (empty($userIds)) return; + + // 只统计:实例进行中 + 当前轮次 + 当前步骤 的 pending + $rows = Db::table('approval_records as ar') + ->join('approval_instances as ai', 'ai.id', '=', 'ar.instance_id') + ->whereIn('ar.approver_id', $userIds) + ->where('ar.status', 'pending') + ->where('ai.status', 'processing') + ->whereRaw('ar.round = ai.round') + ->whereRaw('ar.step_index = ai.current_step') + ->groupBy('ar.approver_id') + ->select('ar.approver_id', Db::raw('COUNT(*) as cnt')) + ->get(); + + // 默认置 0,再填充真实值 + $payload = array_fill_keys($userIds, 0); + foreach ($rows as $r) { + $payload[(int)$r->approver_id] = (int)$r->cnt; + } + + try { + foreach ($payload as $uid => $cnt) { + (new \app\common\utils\WsEvents())->todoCounts($uid, $cnt); + } + } catch (\Throwable $e) { + Log::error('ws todo push error: '.$e->getMessage()); + } + } +} diff --git a/app/common/utils/approval/traits/ApprovalQueryTrait.php b/app/common/utils/approval/traits/ApprovalQueryTrait.php new file mode 100644 index 0000000..2f8e443 --- /dev/null +++ b/app/common/utils/approval/traits/ApprovalQueryTrait.php @@ -0,0 +1,475 @@ +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) + ->where('round', (int)$instance->round) + ->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 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, + ]; + } + + /** 简单时间线 */ + 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; + $round = (int)$instance->round; + + // 申请人 + $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) + ->where('ar.round', $round) + ->orderBy('ar.step_index') + ->orderByRaw('COALESCE(ar.approved_at, ar.created_at) asc') + ->select('ar.*') + ->get() + ->toArray(); + + // 一次性拉取姓名,避免 N+1 + $nameMap = []; + if ($userNameColumn && !empty($records)) { + $uids = []; + foreach ($records as $r) $uids[] = (int)$r->approver_id; + $uids = array_values(array_unique($uids)); + if (!empty($uids)) { + $pairs = Db::table('user')->whereIn('id', $uids)->pluck($userNameColumn, 'id')->toArray(); + foreach ($pairs as $uid => $name) $nameMap[(int)$uid] = $name; + } + } + + // 记录分组 + $byStep = []; + foreach ($records as $r) { + $idx = (int)$r->step_index; + $byStep[$idx][] = $r; + } + + $timeline = []; + + // 可选:发起申请块 + 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; + + // 每一步的“审批意见”(只展示有内容的 comment) + $opinions = []; // [{approver_id, approver_name?, action, status, comment, time}...] + + 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) { + // 收集“审批意见”(非空 comment 都算) + $cmt = trim((string)($rr->comment ?? '')); + if ($cmt !== '') { + $opinions[] = [ + 'approver_id' => (int)$rr->approver_id, + 'approver_name' => $userNameColumn ? ($nameMap[(int)$rr->approver_id] ?? null) : null, + 'action' => $rr->action, // approve | reject | null + 'status' => $rr->status, // approved|rejected|pending|skipped|timeout + 'comment' => $cmt, + 'time' => $rr->approved_at ?: $rr->created_at, + ]; + } + + 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]++; + } + + // 时间升序排序 opinions + if (!empty($opinions)) { + usort($opinions, function($a, $b) { + return strcmp($a['time'] ?? '', $b['time'] ?? ''); + }); + } + + $startedAt = $minCreated; + + // 总结果(注意:这里按你现在的签名调用,含 round) + $outcome = $this->getStepOutcome($instanceId, $round, $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, + 'opinions' => $opinions, // 这就是每个审批意见 + ]; + + // 把申请人信息并到第一步(可选) + if ($initiatorPlacement === 'attach' && empty($timeline)) { + $row['initiator_id'] = $initiatorId; + $row['initiator_name'] = $initiatorName; + } + + $timeline[] = $row; + } + + return $timeline; + } + + /** 按业务ID获取“最新进展”的极简状态 */ + 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; + $round = (int)$instance->round; + $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.round', $round) + ->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('round', $round) + ->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('round', $round) + ->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; + } + + /** 登录/首屏初始化:获取当前用户的待办总数 */ + public function getInitTodoCount(int $userId): int + { + $userId = (int)$userId; + if ($userId <= 0) { + throw new BusinessException('无效的用户ID'); + } + + $row = Db::table('approval_records as ar') + ->join('approval_instances as ai', 'ai.id', '=', 'ar.instance_id') + ->where('ar.approver_id', $userId) + ->where('ar.status', 'pending') + ->where('ai.status', 'processing') + // 只统计实例“当前轮次、当前步骤”的待办,避免历史残留干扰 + ->whereRaw('ar.round = ai.round') + ->whereRaw('ar.step_index = ai.current_step') + ->selectRaw('COUNT(*) as c') + ->first(); + + return (int)($row->c ?? 0); + } +} diff --git a/app/common/utils/approval/traits/ApprovalResolverTrait.php b/app/common/utils/approval/traits/ApprovalResolverTrait.php new file mode 100644 index 0000000..b6b3868 --- /dev/null +++ b/app/common/utils/approval/traits/ApprovalResolverTrait.php @@ -0,0 +1,406 @@ +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; + + case 'group_members': { + // $gid = (int)($cfg['group_id'] ?? 0); + $gid = (int)($cfg['value'] ?? 0); + if ($gid <= 0) throw new BusinessException('group_members 需要 group_id'); + $ids = $this->getGroupMemberIds($gid); // A 方案 + break; + } + + case 'scenario_leader_of_initiator': { + $formData = json_decode($instance->form_data ?? '{}', true) ?: []; + // $scenario = $cfg['scenario'] ?? ($formData[$cfg['scenario_field']] ?? null); + $scenario = $val ?? ($formData[$cfg['scenario_field']] ?? null); + if (!$scenario) throw new BusinessException('缺少业务场景(scenario 或 scenario_field)'); + + $mustBelong = array_key_exists('must_belong', $cfg) ? (bool)$cfg['must_belong'] : true; + $preferPrimary = array_key_exists('prefer_primary', $cfg) ? (bool)$cfg['prefer_primary'] : true; + + $ids = $this->getLeadersByScenarioForInitiator( + (int)$instance->initiator_id, + (string)$scenario, + $mustBelong, + $preferPrimary + ); // B 方案(按场景) + 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; + case 'group_members': { + $gid = (int)($val ?? 0); + if ($gid <= 0) throw new BusinessException('group_members 需要 group_id'); + $uids = $this->getGroupMemberIds($gid); // A 方案 + break; + } + case 'scenario_leader_of_initiator': { + $formData = json_decode($instance->form_data ?? '{}', true) ?: []; + $scenario = $val ?? ($formData[$cc['scenario_field']] ?? null); + if (!$scenario) throw new BusinessException('缺少业务场景(scenario 或 scenario_field)'); + + $mustBelong = array_key_exists('must_belong', $cc) ? (bool)$cc['must_belong'] : true; + $preferPrimary = array_key_exists('prefer_primary', $cc) ? (bool)$cc['prefer_primary'] : true; + + $uids = $this->getLeadersByScenarioForInitiator( + (int)$instance->initiator_id, + (string)$scenario, + $mustBelong, + $preferPrimary + ); // B 方案(按场景) + 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}"), + }; + } + + /** 获取下一个有效步骤索引 */ + 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; + } + + /** 聚合本步骤结论 */ + private function getStepOutcome(int $instanceId, int $round, int $stepIndex, string $type): string + { + $records = Db::table('approval_records') + ->where('instance_id', $instanceId) + ->where('round', $round) // + ->where('step_index', $stepIndex) + ->get(); + + $pending = $approved = $rejected = $timeout = $skipped = 0; + foreach ($records as $r) { + switch ($r->status) { + case 'pending': $pending++; break; + case 'approved': $approved++; break; + case 'rejected': $rejected++; break; + case 'timeout': $timeout++; break; + case 'skipped': $skipped++; break; + } + } + + $type = strtolower($type); + if ($type === 'any') { + if ($rejected > 0) return 'reject'; + if ($approved > 0) return 'approve'; + // 全为 skipped/timeout 且无 pending/rejected ⇒ 通过 + if ($pending === 0 && $rejected === 0 && $approved === 0) return 'approve'; + return 'pending'; + } + if ($rejected > 0) return 'reject'; + if ($pending > 0) return 'pending'; + return 'approve'; + } + + /** 关闭本步其余待办为 skipped */ + private function closeOtherPending(int $instanceId, int $round, int $stepIndex, array $keepApproverIds = []): void + { + $now = date('Y-m-d H:i:s'); + $q = Db::table('approval_records') + ->where('instance_id', $instanceId) + ->where('round', $round) // + ->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(object $instance, int $stepIndex, int $approverId): void + { + $round = (int)($instance->round ?? 1); + $now = date('Y-m-d H:i:s'); + + $exists = Db::table('approval_records') + ->where('instance_id', (int)$instance->id) + ->where('round', $round) // 限当前轮 + ->where('step_index', $stepIndex) + ->where('approver_id', $approverId) + ->where('status', 'pending') + ->exists(); + + if ($exists) return; + + Db::table('approval_records')->insert([ + 'instance_id' => (int)$instance->id, + 'approver_id' => $approverId, + 'step_index' => $stepIndex, + 'round' => $round, // + 'action' => null, + 'status' => 'pending', + 'comment' => null, + 'approved_at' => null, + 'created_at' => $now, + 'updated_at' => $now, + ]); + } + + /** 取“某实例、某轮次、某步骤”里当前 pending 的审批人 */ + private function getPendingApproverIds(int $instanceId, int $round, int $stepIndex): array + { + $ids = Db::table('approval_records') + ->where('instance_id', $instanceId) + ->where('round', $round) + ->where('step_index', $stepIndex) + ->where('status', 'pending') + ->pluck('approver_id')->toArray(); + + return array_values(array_unique(array_map('intval', $ids))); + } + + /** 从角色取用户ID */ + 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; + } + + /** A) 取组内全部成员 */ + private function getGroupMemberIds(int $groupId): array + { + $rows = Db::table('user_group_members') + ->where('group_id', $groupId) + ->where('status', 1) + ->pluck('user_id')->toArray(); + + return array_values(array_unique(array_map('intval', $rows))); + } + + /** B) 按“场景”在发起人所在的组里找负责人 */ + private function getLeadersByScenarioForInitiator( + int $initiatorId, + string $scenarioCode, + bool $mustBelong = true, + bool $preferPrimary = true + ): array { + // 找出“该场景下发起人所在的所有组” + $groups = Db::table('user_group as g') + ->join('user_group_members as m', 'm.group_id', '=', 'g.id') + ->where('g.scenario_code', $scenarioCode) + ->where('g.status', 1) + ->where('m.user_id', $initiatorId) + ->where('m.status', 1) + ->orderByDesc('m.is_primary') // 主归属优先 + ->orderByDesc('g.sort') // 其次按组排序 + ->select('g.id', 'm.is_primary') + ->get()->toArray(); + + if (empty($groups)) { + if ($mustBelong) { + throw new BusinessException('发起人在该业务场景下未加入任何分组'); + } + // 不强制属于:允许只按“该场景下任一组”的负责人(不推荐) + $groups = Db::table('user_group as g') + ->where('g.scenario_code', $scenarioCode) + ->where('g.status', 1) + ->orderByDesc('g.sort') + ->select('g.id', Db::raw('0 as is_primary')) + ->get()->toArray(); + if (empty($groups)) { + throw new BusinessException('该业务场景下未配置任何分组'); + } + } + + // 选择一个“目标组” + $targetGroupId = null; + if ($preferPrimary) { + foreach ($groups as $g) { + if ((int)$g->is_primary === 1) { $targetGroupId = (int)$g->id; break; } + } + } + if (!$targetGroupId) { + $targetGroupId = (int)$groups[0]->id; // 用排序第一的组 + } + + // 取负责人(可支持多负责人) + $leaders = Db::table('user_group_members') + ->where('group_id', $targetGroupId) + ->where('is_leader', 1) + ->where('status', 1) + ->pluck('user_id')->toArray(); + + $ids = array_values(array_unique(array_map('intval', $leaders))); + if (empty($ids)) throw new BusinessException('目标分组未配置负责人'); + + return $ids; + } +} diff --git a/app/common/utils/approval/traits/ApprovalTransferTrait.php b/app/common/utils/approval/traits/ApprovalTransferTrait.php new file mode 100644 index 0000000..8c00e21 --- /dev/null +++ b/app/common/utils/approval/traits/ApprovalTransferTrait.php @@ -0,0 +1,130 @@ +where('instance_id', $instanceId) + ->orderBy('id', 'asc') + ->get() + ->toArray(); + + return array_map(function($r){ + return [ + 'id' => (int)$r->id, + 'instance_id' => (int)$r->instance_id, + 'step_index' => (int)$r->step_index, + 'record_id' => (int)$r->record_id, + 'from_user_id' => (int)$r->from_user_id, + 'to_user_id' => (int)$r->to_user_id, + 'reason' => $r->reason, + 'created_at' => $r->created_at, + ]; + }, $rows); + } + + /** 转交当前待办给他人 */ + public function transferApproval( + int $instanceId, + int $stepIndex, + int $fromUserId, + int $toUserId, + string $reason = '', + ?string $userNameColumn = null + ): array { + if ($fromUserId === $toUserId) { + return ['status' => 'noop', 'msg' => '目标与来源相同,无需转交']; + } + + return \support\Db::transaction(function () use ($instanceId, $stepIndex, $fromUserId, $toUserId, $reason, $userNameColumn) { + // 锁实例,避免并发推进 + $instance = Db::table('approval_instances')->where('id', $instanceId)->lockForUpdate()->first(); + if (!$instance) throw new BusinessException('审批实例未找到'); + if ($instance->status !== 'processing') throw new BusinessException('流程非进行中,无法转交'); + + // 锁定来源人的 pending 记录 + $record = Db::table('approval_records') + ->where('instance_id', $instanceId) + ->where('round', (int)$instance->round) + ->where('step_index', $stepIndex) + ->where('approver_id', $fromUserId) + ->where('status', 'pending') + ->lockForUpdate() + ->first(); + if (!$record) throw new BusinessException('未找到可转交的待办或已处理'); + + $now = date('Y-m-d H:i:s'); + + // 若目标已有 pending:合并(本记录置为 skipped) + $target = Db::table('approval_records') + ->where('instance_id', $instanceId) + ->where('round', (int)$instance->round) + ->where('step_index', $stepIndex) + ->where('approver_id', $toUserId) + ->where('status', 'pending') + ->lockForUpdate() + ->first(); + + if ($target) { + Db::table('approval_records')->where('id', $record->id)->update([ + 'status' => 'skipped', + 'approved_at' => $now, + 'updated_at' => $now, + 'comment' => trim(($record->comment ?? '')."\n[转交给 {$toUserId},目标已有待办,已合并]"), + ]); + $effectiveRecordId = (int)$target->id; + } else { + // 原地替换 approver_id + Db::table('approval_records')->where('id', $record->id)->update([ + 'approver_id' => $toUserId, + 'updated_at' => $now, + 'comment' => trim(($record->comment ?? '')."\n[自 {$fromUserId} 转交]"), + ]); + $effectiveRecordId = (int)$record->id; + } + + // 组织“转交来源/原因”文案(优先姓名) + $fromLabel = (string)$fromUserId; + $toLabel = (string)$toUserId; + if ($userNameColumn) { + $fromName = Db::table('user')->where('id', $fromUserId)->value($userNameColumn); + $toName = Db::table('user')->where('id', $toUserId)->value($userNameColumn); + if ($fromName) $fromLabel = $fromName; + if ($toName) $toLabel = $toName; + } + $note = "【转交】{$fromLabel} → {$toLabel}"; + $r = trim($reason); + if ($r !== '') $note .= ",原因:{$r}"; + + // 通知被转交人(在内容里追加 note) + $this->sendNotification([$toUserId], (object)[ + 'id' => (int)$instance->id, + 'flow_type' => (string)$instance->flow_type, + 'biz_type' => (string)$instance->biz_type, + 'biz_id' => (string)$instance->biz_id, + 'initiator_id' => (int)$instance->initiator_id, + 'form_data' => $instance->form_data, + ], 'pending', false, ['note' => $note]); + + // 记转交流水 + Db::table('approval_transfers')->insert([ + 'instance_id' => (int)$instanceId, + 'step_index' => (int)$stepIndex, + 'record_id' => $effectiveRecordId, + 'from_user_id' => (int)$fromUserId, + 'to_user_id' => (int)$toUserId, + 'reason' => $reason, + 'created_at' => $now, + ]); + + $this->pushTodoChanged([$fromUserId, $toUserId]); + return ['status' => 'ok', 'record_id' => $effectiveRecordId]; + }); + } +} diff --git a/app/mobile/controller/InvRequisitionItemReturnController.php b/app/mobile/controller/InvRequisitionItemReturnController.php index 4a6ebbe..58df807 100644 --- a/app/mobile/controller/InvRequisitionItemReturnController.php +++ b/app/mobile/controller/InvRequisitionItemReturnController.php @@ -46,7 +46,7 @@ class InvRequisitionItemReturnController extends BaseController $data = $this->validate($this->service->getValidator(), $request->post(), 'returnConfirm'); // 处理申请逻辑 - $result = $this->service->returnConfirm($request, $data, 'returnConfirm'); + $result = $this->service->serviceReturnConfirm($request, $data, 'returnConfirm'); return $this->success($result, '申请提交成功'); } catch (\Throwable $e) { diff --git a/config/route.php b/config/route.php index 54f6cce..a93349d 100644 --- a/config/route.php +++ b/config/route.php @@ -646,5 +646,6 @@ Route::group('/mobile', function () { Route::any('/requisition/update', [app\mobile\controller\InvRequisitionController::class, 'update']); Route::any('/requisition/item/return', [app\mobile\controller\InvRequisitionItemReturnController::class, 'store']); Route::any('/requisition/item/return/update', [app\mobile\controller\InvRequisitionItemReturnController::class, 'update']); + Route::any('/requisition/item/return/confirm', [app\mobile\controller\InvRequisitionItemReturnController::class, 'returnConfirm']); }); }); \ No newline at end of file diff --git a/public/db_log/审批.sql b/public/db_log/审批.sql new file mode 100644 index 0000000..42e65da --- /dev/null +++ b/public/db_log/审批.sql @@ -0,0 +1,94 @@ +CREATE TABLE `approval_flows` ( + `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, + `name` varchar(255) NOT NULL COMMENT '流程名称', + `type` varchar(100) NOT NULL COMMENT '业务类型标识,如 leave, reimbursement', + `steps` json NOT NULL COMMENT '审批步骤配置', + `is_active` tinyint(1) NOT NULL DEFAULT '1' COMMENT '是否启用', + `timeout_hours` int(11) NOT NULL DEFAULT '0' COMMENT '实例级超时时间(小时),0为不超时', + `timeout_action` varchar(20) NOT NULL DEFAULT 'auto_reject' COMMENT 'auto_approve | auto_reject', + `created_at` timestamp NULL DEFAULT NULL, + `updated_at` timestamp NULL DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `uk_flow_type` (`type`) +) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8mb4 COMMENT='审批流程定义'; + +CREATE TABLE `approval_instances` ( + `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, + `flow_type` varchar(100) NOT NULL COMMENT '关联 approval_flows.type', + `biz_id` varchar(255) NOT NULL COMMENT '业务ID', + `biz_type` varchar(100) NOT NULL COMMENT '业务标识/表名', + `form_data` json NOT NULL COMMENT '提交表单快照', + `initiator_id` bigint(20) unsigned NOT NULL COMMENT '发起人', + `status` varchar(20) NOT NULL DEFAULT 'processing' COMMENT 'processing|approved|rejected|revoked|timed_out', + `current_step` int(11) NOT NULL DEFAULT '0' COMMENT '当前步骤索引', + `round` int(11) NOT NULL DEFAULT '1' COMMENT '当前轮次:1=首次,重提+1', + `finished_at` timestamp NULL DEFAULT NULL, + `deadline_at` timestamp NULL DEFAULT NULL, + `created_at` timestamp NULL DEFAULT NULL, + `updated_at` timestamp NULL DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `idx_flow_type` (`flow_type`), + KEY `idx_biz` (`biz_type`,`biz_id`), + KEY `idx_initiator` (`initiator_id`), + KEY `idx_status_updated` (`status`,`updated_at`) +) ENGINE=InnoDB AUTO_INCREMENT=68 DEFAULT CHARSET=utf8mb4 COMMENT='审批实例'; + +CREATE TABLE `approval_records` ( + `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, + `instance_id` bigint(20) unsigned NOT NULL COMMENT 'approval_instances.id', + `approver_id` bigint(20) unsigned NOT NULL COMMENT '审批人', + `step_index` int(11) NOT NULL COMMENT '步骤索引', + `round` int(11) NOT NULL DEFAULT '1' COMMENT '所属轮次', + `action` varchar(20) DEFAULT NULL COMMENT 'approve|reject,待办时为NULL', + `status` varchar(20) NOT NULL DEFAULT 'pending' COMMENT 'pending|approved|rejected|skipped|timeout', + `comment` text COMMENT '审批意见', + `approved_at` timestamp NULL DEFAULT NULL COMMENT '审批时间/关闭时间', + `created_at` timestamp NULL DEFAULT NULL, + `updated_at` timestamp NULL DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `uk_instance_step_approver` (`instance_id`,`step_index`,`approver_id`), + KEY `idx_instance_step_status` (`instance_id`,`step_index`,`status`), + KEY `idx_approver` (`approver_id`), + KEY `idx_instance_step_round` (`instance_id`,`step_index`,`round`) +) ENGINE=InnoDB AUTO_INCREMENT=105 DEFAULT CHARSET=utf8mb4 COMMENT='审批记录'; + +CREATE TABLE `approval_transfers` ( + `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, + `instance_id` bigint(20) unsigned NOT NULL, + `step_index` int(11) NOT NULL, + `record_id` bigint(20) unsigned NOT NULL COMMENT '转交后仍然有效的记录ID(若合并则为目标记录ID)', + `from_user_id` bigint(20) unsigned NOT NULL, + `to_user_id` bigint(20) unsigned NOT NULL, + `reason` text, + `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + KEY `idx_record` (`record_id`), + KEY `idx_instance_step` (`instance_id`,`step_index`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='审批转交日志'; + +CREATE TABLE `approval_ccs` ( + `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, + `instance_id` bigint(20) unsigned NOT NULL, + `user_id` bigint(20) unsigned NOT NULL, + `notified_at` timestamp NULL DEFAULT NULL, + `read_at` timestamp NULL DEFAULT NULL, + `created_at` timestamp NULL DEFAULT NULL, + `updated_at` timestamp NULL DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `uk_instance_user` (`instance_id`,`user_id`), + KEY `idx_user` (`user_id`), + KEY `idx_instance` (`instance_id`) +) ENGINE=InnoDB AUTO_INCREMENT=9 DEFAULT CHARSET=utf8mb4 COMMENT='审批抄送'; + +CREATE TABLE `approval_discussions` ( + `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, + `instance_id` bigint(20) unsigned NOT NULL, + `user_id` bigint(20) unsigned NOT NULL, + `is_internal` tinyint(1) NOT NULL DEFAULT '0' COMMENT '0=公开,1=仅审批人可见', + `content` text NOT NULL, + `attachments` json DEFAULT NULL, + `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP, + `updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + KEY `idx_instance_id` (`instance_id`) +) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COMMENT='审批讨论'; \ No newline at end of file From ca757e67ddb42aea2cf4d5db6f395bb5a75a0b49 Mon Sep 17 00:00:00 2001 From: "WIN-C27C5MSVTPO\\16961" <1696136552@qq.com> Date: Sun, 4 Jan 2026 16:27:20 +0800 Subject: [PATCH 093/110] =?UTF-8?q?=E7=A7=BB=E5=8A=A8=E7=AB=AF=E5=9B=9E?= =?UTF-8?q?=E8=B0=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/common/controller/AuthController.php | 2 +- app/common/model/CorpFeishuApp.php | 2 +- app/common/service/AuthService.php | 4 ++-- app/common/utils/Feishu.php | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/app/common/controller/AuthController.php b/app/common/controller/AuthController.php index 93c39fb..ba87140 100644 --- a/app/common/controller/AuthController.php +++ b/app/common/controller/AuthController.php @@ -31,7 +31,7 @@ class AuthController extends BaseController public function feishuScope(Request $request) { try { - $result = (new AuthService())->feishuAuthorize($request->corp_abbr, $request->system_code); + $result = (new AuthService())->feishuAuthorize($request->corp_abbr, $request->system_code, $request->is_mob); return $this->success($result); } catch (\Throwable $e) { return $this->error($e->getMessage()); diff --git a/app/common/model/CorpFeishuApp.php b/app/common/model/CorpFeishuApp.php index 8019a0b..53dcecf 100644 --- a/app/common/model/CorpFeishuApp.php +++ b/app/common/model/CorpFeishuApp.php @@ -40,7 +40,7 @@ class CorpFeishuApp extends Model /** * 允许自动填充模型字段 */ - protected $fillable = ['id', 'corp_id', 'system_id', 'name', 'tenant_key', 'app_id', 'app_secret', 'base_uri', 'r_url', 'scopes', 'state', 'sort', 'status', 'created_at', 'updated_at']; + protected $fillable = ['id', 'corp_id', 'system_id', 'name', 'tenant_key', 'app_id', 'app_secret', 'base_uri', 'm_url', 'r_url', 'scopes', 'state', 'sort', 'status', 'created_at', 'updated_at']; /** * 不允许自动填充模型字段 diff --git a/app/common/service/AuthService.php b/app/common/service/AuthService.php index 149e966..a675025 100644 --- a/app/common/service/AuthService.php +++ b/app/common/service/AuthService.php @@ -13,12 +13,12 @@ class AuthService /** * 飞书授权 */ - public function feishuAuthorize(string $corpAbbr, string $systemCcode) + public function feishuAuthorize(string $corpAbbr, string $systemCcode, $mobile) { $logic = new AuthLogic(); $certificate = $logic->feishuAppInfo($corpAbbr, $systemCcode); $feishu = new Feishu($certificate['app_id'], $certificate['app_secret'], $certificate['abbr']); - return ['auth_url' => $feishu->authorize($certificate)]; + return ['auth_url' => $feishu->authorize($certificate, $mobile)]; } /** diff --git a/app/common/utils/Feishu.php b/app/common/utils/Feishu.php index 8428f1c..e10672c 100644 --- a/app/common/utils/Feishu.php +++ b/app/common/utils/Feishu.php @@ -91,11 +91,11 @@ class Feishu /** * 授权 */ - public function authorize($certificate, $url = 'authen/v1/authorize') + public function authorize($certificate,$mobile, $url = 'authen/v1/authorize') { $arr = [ 'app_id' => $certificate['app_id'], - 'redirect_uri' => $certificate['r_url'], + 'redirect_uri' => $mobile == 1 ? $certificate['m_url'] : $certificate['r_url'], 'scope' => $certificate['scope'], 'state' => $certificate['state'] ]; From de9484f570cf9f154be088ab1a197dcc3007e665 Mon Sep 17 00:00:00 2001 From: "WIN-C27C5MSVTPO\\16961" <1696136552@qq.com> Date: Sun, 4 Jan 2026 16:42:19 +0800 Subject: [PATCH 094/110] =?UTF-8?q?=E7=A7=BB=E5=8A=A8=E7=AB=AF=E5=9B=9E?= =?UTF-8?q?=E8=B0=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/common/logic/AuthLogic.php | 1 + 1 file changed, 1 insertion(+) diff --git a/app/common/logic/AuthLogic.php b/app/common/logic/AuthLogic.php index 9abe5e1..792125f 100644 --- a/app/common/logic/AuthLogic.php +++ b/app/common/logic/AuthLogic.php @@ -298,6 +298,7 @@ class AuthLogic 'app_id' => $fsAppInfo->app_id, 'app_secret' => $fsAppInfo->app_secret, 'r_url' => $fsAppInfo->r_url, + 'm_url' => $fsAppInfo->m_url, 'scope' => $fsAppInfo->scopes, 'state' => $fsAppInfo->state, 'abbr' => $corpAbbr, From c2e402722dcb103c1d5d697e11936d8931c19d09 Mon Sep 17 00:00:00 2001 From: "WIN-C27C5MSVTPO\\16961" <1696136552@qq.com> Date: Sun, 4 Jan 2026 17:23:10 +0800 Subject: [PATCH 095/110] =?UTF-8?q?=E7=99=BB=E5=BD=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/common/logic/AuthLogic.php | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/app/common/logic/AuthLogic.php b/app/common/logic/AuthLogic.php index 792125f..1be7125 100644 --- a/app/common/logic/AuthLogic.php +++ b/app/common/logic/AuthLogic.php @@ -196,7 +196,15 @@ class AuthLogic { $certificate = $this->feishuAppInfo($data['corp_abbr'], $data['system_code']); $feishu = new Feishu($certificate['app_id'], $certificate['app_secret'], $certificate['abbr']); - $userAccessToken = $feishu->userAccessToken($data['feishu_code'], $certificate['r_url']); + + //判断移动端或pc端回调地址 + if(!empty($data['is_mob']) && $data['is_mob'] == 1){ + $redirect_uri = $certificate['m_url']; + }else{ + $redirect_uri = $certificate['r_url']; + } + + $userAccessToken = $feishu->userAccessToken($data['feishu_code'], $redirect_uri); $userInfo = $feishu->userInfo($userAccessToken['access_token']); if ($userInfo['code'] != 0) { From 07a40bf688a5351fdce6cee3838f590e7084f019 Mon Sep 17 00:00:00 2001 From: "WIN-C27C5MSVTPO\\16961" <1696136552@qq.com> Date: Mon, 5 Jan 2026 10:18:27 +0800 Subject: [PATCH 096/110] =?UTF-8?q?=E7=89=A9=E5=93=81=E7=9B=B8=E5=85=B3?= =?UTF-8?q?=E4=BF=A1=E6=81=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/admin/controller/InvItemController.php | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/app/admin/controller/InvItemController.php b/app/admin/controller/InvItemController.php index 287e734..b9db3c0 100644 --- a/app/admin/controller/InvItemController.php +++ b/app/admin/controller/InvItemController.php @@ -2,6 +2,8 @@ namespace app\admin\controller; use app\common\controller\BaseController; +use app\common\model\Corp; +use app\common\model\User; use app\common\traits\crud\CrudControllerTrait; use app\common\service\InvItemService; @@ -22,12 +24,16 @@ class InvItemController 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['company_id'])) + $data['company_name'] = Corp::where('id', $data['company_id'])->value('name'); + if (isset($data['manage_user_id'])) + $data['manage_user_name'] = User::where('id', $data['manage_user_id'])->value('name'); + return $data; + } } \ No newline at end of file From c38a53f053f58a49a6044194a09c647a77428f4d Mon Sep 17 00:00:00 2001 From: "WIN-C27C5MSVTPO\\16961" <1696136552@qq.com> Date: Mon, 5 Jan 2026 10:42:08 +0800 Subject: [PATCH 097/110] =?UTF-8?q?=E7=89=A9=E5=93=81=E4=B8=8B=E6=8B=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/admin/controller/InvItemController.php | 16 ++++++ app/common/logic/InvItemLogic.php | 5 ++ app/common/service/InvItemService.php | 61 +++++++++++++++++++++ app/mobile/controller/InvItemController.php | 55 +++++++++++++++++++ config/route.php | 6 ++ 5 files changed, 143 insertions(+) create mode 100644 app/mobile/controller/InvItemController.php diff --git a/app/admin/controller/InvItemController.php b/app/admin/controller/InvItemController.php index b9db3c0..ed8866f 100644 --- a/app/admin/controller/InvItemController.php +++ b/app/admin/controller/InvItemController.php @@ -6,6 +6,7 @@ use app\common\model\Corp; use app\common\model\User; use app\common\traits\crud\CrudControllerTrait; use app\common\service\InvItemService; +use support\Request; class InvItemController extends BaseController { @@ -18,6 +19,21 @@ class InvItemController extends BaseController $this->service = new InvItemService(); } + public function preset(Request $request){ + try { + $res = $this->service->getList(); + $result = []; + foreach ($res as $value) { + $value['company_name'] = Corp::where('id', $value['company_id'])->value('name'); + $value['manage_user_name'] = User::where('id', $value['manage_user_id'])->value('name'); + $result[] = $value; + } + return $this->success($result, '获取物品列表'); + } catch (\Throwable $e) { + return $this->error($e->getMessage()); + } + } + /** * 列表页数据格式化 * diff --git a/app/common/logic/InvItemLogic.php b/app/common/logic/InvItemLogic.php index 364c5c6..48c6ffd 100644 --- a/app/common/logic/InvItemLogic.php +++ b/app/common/logic/InvItemLogic.php @@ -14,4 +14,9 @@ class InvItemLogic */ protected string $modelClass = InvItem::class; + + public function getList() + { + return InvItem::where('status',1)->orderBy('sort','desc')->get(); + } } \ No newline at end of file diff --git a/app/common/service/InvItemService.php b/app/common/service/InvItemService.php index 43a1f44..eb49fb9 100644 --- a/app/common/service/InvItemService.php +++ b/app/common/service/InvItemService.php @@ -2,6 +2,7 @@ namespace app\common\service; +use app\common\logic\InvItemLogic; use app\common\validate\InvItemValidate; use app\common\traits\crud\CrudServiceTrait; use app\common\model\InvItem; @@ -23,6 +24,12 @@ class InvItemService */ protected $validator = InvItemValidate::class; + protected $logic; + + public function __construct() + { + $this->logic = (new InvItemLogic()); + } /** * 获取验证器 */ @@ -31,6 +38,52 @@ class InvItemService return new $this->validator; } + /** + * 构造 CrudUtil 查询配置 + */ + protected function buildQueryConfig(Request $request, string $scene = 'default'): array + { + $p = $request->all(); + + $config = []; + $config['model'] = $this->modelClass; + switch ($scene) { + case 'default': + $config['where']['status'] = 1; + $config['order']['sort'] = 'desc'; + $config['order']['id'] = 'desc'; + break; + case 'index': + $config['where']['status'] = 1; + + // 关键字 + if (isset($p['keyword']) && $p['keyword']) { + $k = '%' . $p['keyword'] . '%'; + foreach (['item_name'] as $col) { + $config['orWhere'][] = [$col, 'like', $k]; + } + } + + $config['order']['sort'] = 'desc'; + $config['order']['id'] = 'desc'; + break; + case 'show': + $config['where']['id'] = $p['id']; + $config['where']['status'] = 1; + break; + case 'store': + break; + case 'update': + $config['where']['id'] = $p['id']; + break; + case 'destroy': + $config['where']['id'] = $p['id']; + break; + } + + return $config; + } + /** * 生成 8-12 位的物品编码(默认10位) * @@ -135,4 +188,12 @@ class InvItemService } return $this->changeStock($itemId, $qty, true); } + + /** + * 获取车型list + */ + public function getList() + { + return $this->logic->getList(); + } } \ No newline at end of file diff --git a/app/mobile/controller/InvItemController.php b/app/mobile/controller/InvItemController.php new file mode 100644 index 0000000..3606afa --- /dev/null +++ b/app/mobile/controller/InvItemController.php @@ -0,0 +1,55 @@ +service = new InvItemService(); + } + + public function preset(Request $request){ + try { + $res = $this->service->getList(); + $result = []; + foreach ($res as $value) { + $value['company_name'] = Corp::where('id', $value['company_id'])->value('name'); + $value['manage_user_name'] = User::where('id', $value['manage_user_id'])->value('name'); + $result[] = $value; + } + return $this->success($result, '获取物品列表'); + } catch (\Throwable $e) { + return $this->error($e->getMessage()); + } + } + + /** + * 列表页数据格式化 + * + * @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['company_id'])) + $data['company_name'] = Corp::where('id', $data['company_id'])->value('name'); + if (isset($data['manage_user_id'])) + $data['manage_user_name'] = User::where('id', $data['manage_user_id'])->value('name'); + return $data; + } +} \ No newline at end of file diff --git a/config/route.php b/config/route.php index a93349d..1b0f178 100644 --- a/config/route.php +++ b/config/route.php @@ -642,8 +642,14 @@ Route::group('/crm', function () { Route::group('/mobile', function () { // 物品领用 Route::group('/inv', function () { + //物品下拉 + Route::any('/item/preset', [app\mobile\controller\InvItemController::class, 'preset']); + + // 物品领用申请 Route::any('/requisition/apply', [app\mobile\controller\InvRequisitionController::class, 'apply']); Route::any('/requisition/update', [app\mobile\controller\InvRequisitionController::class, 'update']); + + // 物品归还 Route::any('/requisition/item/return', [app\mobile\controller\InvRequisitionItemReturnController::class, 'store']); Route::any('/requisition/item/return/update', [app\mobile\controller\InvRequisitionItemReturnController::class, 'update']); Route::any('/requisition/item/return/confirm', [app\mobile\controller\InvRequisitionItemReturnController::class, 'returnConfirm']); From 139acd9e70468be95c075fed7dd4154fd15a0a6b Mon Sep 17 00:00:00 2001 From: "WIN-C27C5MSVTPO\\16961" <1696136552@qq.com> Date: Wed, 7 Jan 2026 09:42:08 +0800 Subject: [PATCH 098/110] =?UTF-8?q?=E4=BA=A7=E5=93=81=E8=A7=84=E6=A0=BC?= =?UTF-8?q?=E4=B8=8D=E5=BF=85=E4=BC=A0=E5=A4=84=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/common/service/InvItemService.php | 2 +- app/common/service/InvRequisitionService.php | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/common/service/InvItemService.php b/app/common/service/InvItemService.php index eb49fb9..5a4b8f1 100644 --- a/app/common/service/InvItemService.php +++ b/app/common/service/InvItemService.php @@ -112,7 +112,7 @@ class InvItemService $data['spec_model'] = ''; $data['unit'] = '件'; $data['is_consumable'] = 0; - $data['stock_qty'] = 1; + $data['stock_qty'] = $data['stock_qty']??1; $data['location'] = ''; return $data; } diff --git a/app/common/service/InvRequisitionService.php b/app/common/service/InvRequisitionService.php index 6c4e8e2..e7bf1a7 100644 --- a/app/common/service/InvRequisitionService.php +++ b/app/common/service/InvRequisitionService.php @@ -144,9 +144,9 @@ class InvRequisitionService 'requisition_id' => $created->id, 'item_id' => $item['item_id'], 'item_name_snap' => $item['item_name_snap'], // 修改:使用 snap 字段 - 'spec_model_snap' => $item['spec_model_snap'], // 修改:使用 snap 字段 - 'unit_snap' => $item['unit_snap'], // 修改:使用 snap 字段 - 'is_consumable_snap' => $item['is_consumable_snap'], // 修改:使用 snap 字段 + 'spec_model_snap' => $item['spec_model_snap']??'', // 修改:使用 snap 字段 + 'unit_snap' => $item['unit_snap']??'', // 修改:使用 snap 字段 + 'is_consumable_snap' => $item['is_consumable_snap']?? 0, // 修改:使用 snap 字段 'request_qty' => (int) $item['request_qty'] ?? 1, // 动态获取数量,转为整型 'approved_qty' => (int) $item['request_qty'] ?? 1, // 初始审批数量等于申请数量 'remark' => $item['remark'] ?? '' From cab15c4fab95532a0f70d5f85a4236092e2df393 Mon Sep 17 00:00:00 2001 From: "WIN-C27C5MSVTPO\\16961" <1696136552@qq.com> Date: Wed, 7 Jan 2026 10:49:33 +0800 Subject: [PATCH 099/110] =?UTF-8?q?=E5=80=9F=E7=94=A8=E5=88=97=E8=A1=A8?= =?UTF-8?q?=E4=B8=8E=E8=AF=A6=E6=83=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/InvRequisitionController.php | 42 +++++++++++++++---- config/route.php | 2 + 2 files changed, 36 insertions(+), 8 deletions(-) diff --git a/app/mobile/controller/InvRequisitionController.php b/app/mobile/controller/InvRequisitionController.php index 21d369e..bdcf409 100644 --- a/app/mobile/controller/InvRequisitionController.php +++ b/app/mobile/controller/InvRequisitionController.php @@ -2,6 +2,9 @@ namespace app\mobile\controller; use app\common\controller\BaseController; +use app\common\model\InvRequisitionAttachment; +use app\common\model\InvRequisitionItem; +use app\common\service\OssService; use app\common\traits\crud\CrudControllerTrait; use app\common\service\InvRequisitionService; use support\Request; @@ -44,12 +47,35 @@ class InvRequisitionController 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']); + return $data; + } + + /** + * 详情页数据格式化 + * + * @param array $data + * @return array + */ + protected function showFormat($data) + { + if (isset($data['created_at'])) + $data['created_at'] = date('Y-m-d H:i:s', $data['created_at']); + if (isset($data['updated_at'])) + $data['updated_at'] = date('Y-m-d H:i:s', $data['updated_at']); + if (isset($data['id'])){ + $data['item'] = InvRequisitionItem::where('requisition_id', $data['id'])->get(); + $data['attachment'] = InvRequisitionAttachment::where('requisition_id', $data['id'])->get(); + $oss = new OssService(); + foreach ($data['attachment'] as $key => $value) { + $data['attachment'][$key]['file_path_pull'] = $oss->generatePrivateUrl($value['file_path']); + } + } + return $data; + } } \ No newline at end of file diff --git a/config/route.php b/config/route.php index 1b0f178..ce61410 100644 --- a/config/route.php +++ b/config/route.php @@ -646,6 +646,8 @@ Route::group('/mobile', function () { Route::any('/item/preset', [app\mobile\controller\InvItemController::class, 'preset']); // 物品领用申请 + Route::any('/requisition/list', [app\mobile\controller\InvRequisitionController::class, 'index']); + Route::any('/requisition/show', [app\mobile\controller\InvRequisitionController::class, 'show']); Route::any('/requisition/apply', [app\mobile\controller\InvRequisitionController::class, 'apply']); Route::any('/requisition/update', [app\mobile\controller\InvRequisitionController::class, 'update']); From 2b529a27c8c16f0d4f093fdab1eb5b41dc62abda Mon Sep 17 00:00:00 2001 From: "WIN-C27C5MSVTPO\\16961" <1696136552@qq.com> Date: Wed, 7 Jan 2026 10:57:28 +0800 Subject: [PATCH 100/110] =?UTF-8?q?=E7=89=A9=E5=93=81=E5=80=9F=E7=94=A8?= =?UTF-8?q?=E5=AE=A1=E6=89=B9=E5=88=97=E8=A1=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/common/service/InvRequisitionService.php | 36 ++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/app/common/service/InvRequisitionService.php b/app/common/service/InvRequisitionService.php index e7bf1a7..bd1b848 100644 --- a/app/common/service/InvRequisitionService.php +++ b/app/common/service/InvRequisitionService.php @@ -35,6 +35,42 @@ class InvRequisitionService return new $this->validator; } + /** + * 构造 CrudUtil 查询配置 + */ + protected function buildQueryConfig(Request $request, string $scene = 'default'): array + { + $p = $request->all(); + + $config = []; + $config['model'] = $this->modelClass; + switch ($scene) { + case 'default': + $config['where']['status'] = 1; + $config['order']['sort'] = 'desc'; + $config['order']['id'] = 'desc'; + break; + case 'index': + $config['where']['applicant_id'] = $request->user->uid; + $config['order']['sort'] = 'desc'; + $config['order']['id'] = 'desc'; + break; + case 'show': + $config['where']['id'] = $p['id']; + $config['where']['status'] = 1; + break; + case 'store': + break; + case 'update': + $config['where']['id'] = $p['id']; + break; + case 'destroy': + $config['where']['id'] = $p['id']; + break; + } + + return $config; + } /** * 创建物品申请 前置 From 63011a03510d58393c9d42e29f589359a13c5d36 Mon Sep 17 00:00:00 2001 From: "WIN-C27C5MSVTPO\\16961" <1696136552@qq.com> Date: Wed, 7 Jan 2026 11:07:43 +0800 Subject: [PATCH 101/110] =?UTF-8?q?=E5=80=9F=E7=94=A8=E5=88=97=E8=A1=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/common/service/InvRequisitionService.php | 1 - 1 file changed, 1 deletion(-) diff --git a/app/common/service/InvRequisitionService.php b/app/common/service/InvRequisitionService.php index bd1b848..4b96794 100644 --- a/app/common/service/InvRequisitionService.php +++ b/app/common/service/InvRequisitionService.php @@ -52,7 +52,6 @@ class InvRequisitionService break; case 'index': $config['where']['applicant_id'] = $request->user->uid; - $config['order']['sort'] = 'desc'; $config['order']['id'] = 'desc'; break; case 'show': From 5841b342de67df22e2e7c34a6cb67b7005a8de2a Mon Sep 17 00:00:00 2001 From: "WIN-C27C5MSVTPO\\16961" <1696136552@qq.com> Date: Wed, 7 Jan 2026 11:32:13 +0800 Subject: [PATCH 102/110] =?UTF-8?q?=E5=80=9F=E7=94=A8=E6=98=8E=E7=BB=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/mobile/controller/InvRequisitionController.php | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/app/mobile/controller/InvRequisitionController.php b/app/mobile/controller/InvRequisitionController.php index bdcf409..0508ace 100644 --- a/app/mobile/controller/InvRequisitionController.php +++ b/app/mobile/controller/InvRequisitionController.php @@ -2,6 +2,8 @@ namespace app\mobile\controller; use app\common\controller\BaseController; +use app\common\model\Corp; +use app\common\model\InvItem; use app\common\model\InvRequisitionAttachment; use app\common\model\InvRequisitionItem; use app\common\service\OssService; @@ -53,6 +55,13 @@ class InvRequisitionController extends BaseController $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['id'])){ + $data['item'] = InvRequisitionItem::where('requisition_id', $data['id'])->get(); + foreach ($data['item'] as $key => $item){ + $corpId = InvItem::where('id', $item->item_id)->value('company_id'); + $item['company_name'] = Corp::where('id', $corpId)->value('name'); + } + } return $data; } From 5a7a6a45d41e15ea107c0398003bcbcac4e57a94 Mon Sep 17 00:00:00 2001 From: "WIN-C27C5MSVTPO\\16961" <1696136552@qq.com> Date: Wed, 7 Jan 2026 13:38:47 +0800 Subject: [PATCH 103/110] =?UTF-8?q?=E5=80=9F=E7=94=A8=E8=AF=A6=E6=83=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/common/service/InvRequisitionService.php | 1 - 1 file changed, 1 deletion(-) diff --git a/app/common/service/InvRequisitionService.php b/app/common/service/InvRequisitionService.php index 4b96794..90c54d1 100644 --- a/app/common/service/InvRequisitionService.php +++ b/app/common/service/InvRequisitionService.php @@ -56,7 +56,6 @@ class InvRequisitionService break; case 'show': $config['where']['id'] = $p['id']; - $config['where']['status'] = 1; break; case 'store': break; From 7cafb61797b86ca3a4f069612931d3bf4a552f0b Mon Sep 17 00:00:00 2001 From: "WIN-C27C5MSVTPO\\16961" <1696136552@qq.com> Date: Wed, 7 Jan 2026 17:23:03 +0800 Subject: [PATCH 104/110] =?UTF-8?q?=E7=89=A9=E5=93=81=E9=A2=86=E7=94=A8?= =?UTF-8?q?=E5=AE=A1=E6=A0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../InvRequisitionApproveController.php | 33 +++++++++++ .../InvRequisitionReturnController.php | 33 +++++++++++ .../logic/InvRequisitionApproveLogic.php | 17 ++++++ .../logic/InvRequisitionReturnLogic.php | 17 ++++++ app/common/model/InvRequisitionApprove.php | 49 ++++++++++++++++ app/common/model/InvRequisitionReturn.php | 49 ++++++++++++++++ .../service/InvRequisitionApproveService.php | 33 +++++++++++ .../InvRequisitionItemReturnService.php | 57 ++++++++++++++----- .../service/InvRequisitionReturnService.php | 31 ++++++++++ app/common/service/InvRequisitionService.php | 38 ++++++++++++- .../approval/service/ApprovalFlowService.php | 12 ++-- .../traits/ApprovalDiscussionTrait.php | 2 +- .../approval/traits/ApprovalEngineTrait.php | 2 +- .../traits/ApprovalNotificationTrait.php | 11 +++- .../approval/traits/ApprovalQueryTrait.php | 2 +- .../approval/traits/ApprovalResolverTrait.php | 2 +- .../approval/traits/ApprovalTransferTrait.php | 2 +- .../InvRequisitionApproveValidate.php | 29 ++++++++++ .../validate/InvRequisitionReturnValidate.php | 29 ++++++++++ app/controller/IndexController.php | 14 ++--- .../InvRequisitionReturnController.php | 33 +++++++++++ 21 files changed, 460 insertions(+), 35 deletions(-) create mode 100644 app/common/controller/InvRequisitionApproveController.php create mode 100644 app/common/controller/InvRequisitionReturnController.php create mode 100644 app/common/logic/InvRequisitionApproveLogic.php create mode 100644 app/common/logic/InvRequisitionReturnLogic.php create mode 100644 app/common/model/InvRequisitionApprove.php create mode 100644 app/common/model/InvRequisitionReturn.php create mode 100644 app/common/service/InvRequisitionApproveService.php create mode 100644 app/common/service/InvRequisitionReturnService.php create mode 100644 app/common/validate/InvRequisitionApproveValidate.php create mode 100644 app/common/validate/InvRequisitionReturnValidate.php create mode 100644 app/mobile/controller/InvRequisitionReturnController.php diff --git a/app/common/controller/InvRequisitionApproveController.php b/app/common/controller/InvRequisitionApproveController.php new file mode 100644 index 0000000..75d419d --- /dev/null +++ b/app/common/controller/InvRequisitionApproveController.php @@ -0,0 +1,33 @@ +service = new InvRequisitionApproveService(); + } + + /** + * 列表页数据格式化 + * + * @param array $data + * @return array + */ + // protected function indexFormat($data) + // { + // if (isset($data['created_at'])) + // $data['created_at'] = date('Y-m-d H:i:s', $data['created_at']); + // if (isset($data['updated_at'])) + // $data['updated_at'] = date('Y-m-d H:i:s', $data['updated_at']); + // return $data; + // } +} \ No newline at end of file diff --git a/app/common/controller/InvRequisitionReturnController.php b/app/common/controller/InvRequisitionReturnController.php new file mode 100644 index 0000000..62a4091 --- /dev/null +++ b/app/common/controller/InvRequisitionReturnController.php @@ -0,0 +1,33 @@ +service = new InvRequisitionReturnService(); + } + + /** + * 列表页数据格式化 + * + * @param array $data + * @return array + */ + // protected function indexFormat($data) + // { + // if (isset($data['created_at'])) + // $data['created_at'] = date('Y-m-d H:i:s', $data['created_at']); + // if (isset($data['updated_at'])) + // $data['updated_at'] = date('Y-m-d H:i:s', $data['updated_at']); + // return $data; + // } +} \ No newline at end of file diff --git a/app/common/logic/InvRequisitionApproveLogic.php b/app/common/logic/InvRequisitionApproveLogic.php new file mode 100644 index 0000000..4f1f795 --- /dev/null +++ b/app/common/logic/InvRequisitionApproveLogic.php @@ -0,0 +1,17 @@ +validator; + } + + +} \ No newline at end of file diff --git a/app/common/service/InvRequisitionItemReturnService.php b/app/common/service/InvRequisitionItemReturnService.php index a90f199..bdadafc 100644 --- a/app/common/service/InvRequisitionItemReturnService.php +++ b/app/common/service/InvRequisitionItemReturnService.php @@ -4,6 +4,7 @@ namespace app\common\service; use app\common\model\InvRequisition; use app\common\model\InvRequisitionItem; +use app\common\model\InvRequisitionReturn; use app\common\validate\InvRequisitionItemReturnValidate; use app\common\traits\crud\CrudServiceTrait; use app\common\model\InvRequisitionItemReturn; @@ -13,6 +14,7 @@ use support\exception\BusinessException; use app\common\service\UserService; use Illuminate\Database\Eloquent\Model; use app\common\model\InvRequisitionItemReturnAttachment; +use function Symfony\Component\String\u; class InvRequisitionItemReturnService { @@ -91,7 +93,7 @@ class InvRequisitionItemReturnService // ]; // 获取所有申请项ID - $reqItemIds = array_column($data, 'requisition_item_id'); + $reqItemIds = array_column($data['items'], 'requisition_item_id'); // 参数校验 if (empty($reqItemIds)) { @@ -99,7 +101,9 @@ class InvRequisitionItemReturnService } // 将申请项按 ID 重组为关联数组 - $newReqItems = $this->groupArrayByKey($data, 'requisition_item_id'); + $newReqItems = $this->groupArrayByKey($data['items'], 'requisition_item_id'); + + $data['return_at'] = is_int($data['return_at']) ? $data['return_at']: strtotime($data['return_at']); // 获取申请项明细 $reqItemsArr = InvRequisitionItem::whereIn('id', $reqItemIds)->get()->toArray(); @@ -112,10 +116,12 @@ class InvRequisitionItemReturnService // 获取用户信息 $userService = new UserService(); $user = $userService->getUserInfo($request->user->uid, ''); - + $data['applicant_id'] = $user['user']['id']; + $data['applicant_name'] = $user['user']['name']; + + $reqItemId = $data['requisition_id']; $arr = []; foreach ($reqItemsArr as $reqItem) { - $reqItemId = $reqItem['id']; // 检查是否有对应的新申请项数据 if (!isset($newReqItems[$reqItemId])) { @@ -123,19 +129,21 @@ class InvRequisitionItemReturnService } $arr[] = [ - 'requisition_id' => $reqItem['requisition_id'], - 'requisition_item_id' => $reqItemId, + 'requisition_id' => $data['requisition_id'], + 'requisition_item_id' => $reqItem['id'], 'item_id' => $reqItem['item_id'], 'apply_user_id' => $user['user']['id'], 'apply_user_name' => $user['user']['name'], - 'return_qty' => $newReqItems[$reqItemId]['return_qty'], + 'return_qty' => $newReqItems[$reqItemId]['return_qty'] ?: '1', 'apply_remark' => $newReqItems[$reqItemId]['apply_remark'] ?? '', - 'attachment' => $newReqItems[$reqItemId]['attachment'], - 'return_at' => time() + //'attachment' => $newReqItems[$reqItemId]['attachment'], + //'return_at' => time(), + 'return_at' => $data['return_at'], ]; } + $data['items'] =$arr; - return $arr; + return $data; } catch (BusinessException $e) { throw $e; } catch (\Throwable $th) { @@ -156,15 +164,24 @@ class InvRequisitionItemReturnService // 关联表增加事务处理 return Db::transaction(function () use ($request, $data) { $data = $this->preProcess('Store', $data, $request); - $dl = $this->getDataLimitFromRequest($request); - foreach ($data as $item) { - $created = $this->logicCreate($item, $dl); - $this->processRequisitionItemReturnAttachment($created, $item['attachment']); + //$dl = $this->getDataLimitFromRequest($request); + + $items = $data['items']; + $attachment = $data['attachment']; + unset($data['items']); + unset($data['attachment']); + $data['order_no'] = $this->generateOrderNo(); + $res = InvRequisitionReturn::create($data); + $created = []; + foreach ($items as $item) { + //$created = $this->logicCreate($item, $dl); + $created = $this->logicCreate($item); } + $this->processRequisitionItemReturnAttachment($res, $attachment); return $this->postProcess('Store', $created); }); } catch (\Throwable $e) { - throw new BusinessException("创建物品申请失败:" . $e->getMessage()); + throw new BusinessException("创建物品申请失败:" . $e->getMessage().$e->getTraceAsString()); } } @@ -446,4 +463,14 @@ class InvRequisitionItemReturnService return $config; } + + /** + * 生成申请单号 + * + * @return string + */ + private function generateOrderNo(): string + { + return 'RR' . date('Ymd') . strtoupper(substr(uniqid(), -6)); + } } \ No newline at end of file diff --git a/app/common/service/InvRequisitionReturnService.php b/app/common/service/InvRequisitionReturnService.php new file mode 100644 index 0000000..e2f1d13 --- /dev/null +++ b/app/common/service/InvRequisitionReturnService.php @@ -0,0 +1,31 @@ +validator; + } +} \ No newline at end of file diff --git a/app/common/service/InvRequisitionService.php b/app/common/service/InvRequisitionService.php index 90c54d1..646a7d9 100644 --- a/app/common/service/InvRequisitionService.php +++ b/app/common/service/InvRequisitionService.php @@ -2,6 +2,7 @@ namespace app\common\service; +use app\common\model\InvItem; use app\common\validate\InvRequisitionValidate; use app\common\traits\crud\CrudServiceTrait; use app\common\model\InvRequisition; @@ -91,6 +92,18 @@ class InvRequisitionService $data['leader_id'] = $user['user']['report_to']; $data['status'] = InvRequisition::STATUS_LEADER; + //查询物品管理人是否是一个 + if(count($data['items'])>=2){ + $manage_user = []; + foreach ($data['items'] as $item) { + $manage_user[] = InvItem::where('id',$item['item_id'])->value('manage_user_id'); + } + $manage_user = array_unique($manage_user); + if(count($manage_user)>1){ + throw new BusinessException("审批实例未找到"); + } + } + return $data; } @@ -113,13 +126,36 @@ class InvRequisitionService $this->processRequisitionItems($created, $data['items']); // 处理物品申请附件 $this->processRequisitionAttachmentItems($created, $data['attachment']); - return $this->postProcess('Store', $created); + $res = $this->postProcess('Store', $created); + + $manage_user_id = InvItem::where('id',$data['items']['item_id'])->value('manage_user_id'); + $approval = [ + 'id' => $res['id'], //申请单id + 'applicant_id' => $res['applicant_id'], //申请人id + 'manage_user_id' => $manage_user_id, + ]; + $this->startApproval($approval); + + return $res; }); } catch (\Throwable $e) { throw new BusinessException("创建物品申请失败:" . $e->getMessage()); } } + /** + * 启动审批 + */ + public function startApproval($requisition) + { + // 启动审批 + $appro = new \app\common\utils\approval\service\ApprovalFlowService(); + $formData = [ + 'manage_user_id' => $requisition['manage_user_id'], + ]; + $appro->start('inv_requisition', 'inv_requisition', $requisition['id'], $formData, $requisition['applicant_id']); + } + /** * 更新物品申请 * diff --git a/app/common/utils/approval/service/ApprovalFlowService.php b/app/common/utils/approval/service/ApprovalFlowService.php index 6b39023..e1b24d9 100644 --- a/app/common/utils/approval/service/ApprovalFlowService.php +++ b/app/common/utils/approval/service/ApprovalFlowService.php @@ -7,10 +7,10 @@ use support\Log; class ApprovalFlowService { - use traits\ApprovalEngineTrait; - use traits\ApprovalQueryTrait; - use traits\ApprovalResolverTrait; - use traits\ApprovalNotificationTrait; - use traits\ApprovalDiscussionTrait; - use traits\ApprovalTransferTrait; + use \app\common\utils\approval\traits\ApprovalEngineTrait; + use \app\common\utils\approval\traits\ApprovalQueryTrait; + use \app\common\utils\approval\traits\ApprovalResolverTrait; + use \app\common\utils\approval\traits\ApprovalNotificationTrait; + use \app\common\utils\approval\traits\ApprovalDiscussionTrait; + use \app\common\utils\approval\traits\ApprovalTransferTrait; } diff --git a/app/common/utils/approval/traits/ApprovalDiscussionTrait.php b/app/common/utils/approval/traits/ApprovalDiscussionTrait.php index 3a74940..0e152a4 100644 --- a/app/common/utils/approval/traits/ApprovalDiscussionTrait.php +++ b/app/common/utils/approval/traits/ApprovalDiscussionTrait.php @@ -1,5 +1,5 @@ biz_id); + if ($invRequisition) { + $title = $prefix."物品借用:{$invRequisition->order_no}"; + $flowTxt = $approvalType[$instance->flow_type] ?? $instance->flow_type; + $content = "{$prefix}你有一条审批 {$text}:类型 {$flowTxt},业务单号 {$invRequisition->order_no}"; + } + break; } // 附加说明(提交说明/申诉/讨论摘要) diff --git a/app/common/utils/approval/traits/ApprovalQueryTrait.php b/app/common/utils/approval/traits/ApprovalQueryTrait.php index 2f8e443..b630e99 100644 --- a/app/common/utils/approval/traits/ApprovalQueryTrait.php +++ b/app/common/utils/approval/traits/ApprovalQueryTrait.php @@ -1,5 +1,5 @@ [ + + ], + + 'update' => [ + + ] + ]; +} \ No newline at end of file diff --git a/app/common/validate/InvRequisitionReturnValidate.php b/app/common/validate/InvRequisitionReturnValidate.php new file mode 100644 index 0000000..0efc9ed --- /dev/null +++ b/app/common/validate/InvRequisitionReturnValidate.php @@ -0,0 +1,29 @@ + [ + + ], + + 'update' => [ + + ] + ]; +} \ No newline at end of file diff --git a/app/controller/IndexController.php b/app/controller/IndexController.php index 08922ab..0440733 100644 --- a/app/controller/IndexController.php +++ b/app/controller/IndexController.php @@ -73,26 +73,26 @@ class IndexController { $options = [ 'controller' => [ - 'module' => 'common', - 'name' => 'inv_requisition_item_return_attachment' + 'module' => 'mobile', + 'name' => 'inv_requisition_return' ], 'model' => [ 'module' => 'common', - 'name' => 'inv_requisition_item_return_attachment' + 'name' => 'inv_requisition_return' ], 'service' => [ 'module' => 'common', - 'name' => 'inv_requisition_item_return_attachment' + 'name' => 'inv_requisition_return' ], 'logic' => [ 'module' => 'common', - 'name' => 'inv_requisition_item_return_attachment' + 'name' => 'inv_requisition_return' ], 'validate' => [ 'module' => 'common', - 'name' => 'inv_requisition_item_return_attachment' + 'name' => 'inv_requisition_return' ], - 'table' => 'inv_requisition_item_return_attachment' + 'table' => 'inv_requisition_return' ]; $result = (new Generate($options))->doGenerate(); diff --git a/app/mobile/controller/InvRequisitionReturnController.php b/app/mobile/controller/InvRequisitionReturnController.php new file mode 100644 index 0000000..1885fb3 --- /dev/null +++ b/app/mobile/controller/InvRequisitionReturnController.php @@ -0,0 +1,33 @@ +service = new InvRequisitionReturnService(); + } + + /** + * 列表页数据格式化 + * + * @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 From e408f7e20e607551c7f5a2f324631763b42dc0bc Mon Sep 17 00:00:00 2001 From: "WIN-C27C5MSVTPO\\16961" <1696136552@qq.com> Date: Wed, 7 Jan 2026 17:38:06 +0800 Subject: [PATCH 105/110] =?UTF-8?q?=E7=89=A9=E5=93=81=E9=A2=86=E7=94=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/common/service/InvRequisitionService.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/common/service/InvRequisitionService.php b/app/common/service/InvRequisitionService.php index 646a7d9..20564e6 100644 --- a/app/common/service/InvRequisitionService.php +++ b/app/common/service/InvRequisitionService.php @@ -128,7 +128,7 @@ class InvRequisitionService $this->processRequisitionAttachmentItems($created, $data['attachment']); $res = $this->postProcess('Store', $created); - $manage_user_id = InvItem::where('id',$data['items']['item_id'])->value('manage_user_id'); + $manage_user_id = InvItem::where('id',$data['items'][0]['item_id'])->value('manage_user_id'); $approval = [ 'id' => $res['id'], //申请单id 'applicant_id' => $res['applicant_id'], //申请人id From a8d13093496afd09c8ee00a9937167bc1c11d2fa Mon Sep 17 00:00:00 2001 From: "WIN-C27C5MSVTPO\\16961" <1696136552@qq.com> Date: Fri, 30 Jan 2026 10:46:54 +0800 Subject: [PATCH 106/110] 1 --- app/admin/controller/InvItemController.php | 15 ++ .../controller/InvRequisitionController.php | 3 +- app/admin/service/InvRequisitionService.php | 31 +++ .../service/InvRequisitionApproveService.php | 193 ++++++++++++++++++ .../approval/traits/ApprovalResolverTrait.php | 4 + config/route.php | 4 + 6 files changed, 248 insertions(+), 2 deletions(-) create mode 100644 app/admin/service/InvRequisitionService.php diff --git a/app/admin/controller/InvItemController.php b/app/admin/controller/InvItemController.php index ed8866f..516300a 100644 --- a/app/admin/controller/InvItemController.php +++ b/app/admin/controller/InvItemController.php @@ -52,4 +52,19 @@ class InvItemController extends BaseController $data['manage_user_name'] = User::where('id', $data['manage_user_id'])->value('name'); return $data; } + + public function requisitionList(Request $request){ + try { + $res = $this->service->getList(); + $result = []; + foreach ($res as $value) { + $value['company_name'] = Corp::where('id', $value['company_id'])->value('name'); + $value['manage_user_name'] = User::where('id', $value['manage_user_id'])->value('name'); + $result[] = $value; + } + return $this->success($result, '审核列表'); + } catch (\Throwable $e) { + return $this->error($e->getMessage()); + } + } } \ No newline at end of file diff --git a/app/admin/controller/InvRequisitionController.php b/app/admin/controller/InvRequisitionController.php index 79b4375..14121f4 100644 --- a/app/admin/controller/InvRequisitionController.php +++ b/app/admin/controller/InvRequisitionController.php @@ -3,8 +3,7 @@ namespace app\admin\controller; use app\common\controller\BaseController; use app\common\traits\crud\CrudControllerTrait; -use app\common\service\InvRequisitionService; -use support\Request; +use app\admin\service\InvRequisitionService; class InvRequisitionController extends BaseController { diff --git a/app/admin/service/InvRequisitionService.php b/app/admin/service/InvRequisitionService.php new file mode 100644 index 0000000..88e84ae --- /dev/null +++ b/app/admin/service/InvRequisitionService.php @@ -0,0 +1,31 @@ +validator; + } +} \ No newline at end of file diff --git a/app/common/service/InvRequisitionApproveService.php b/app/common/service/InvRequisitionApproveService.php index 9c84831..56996db 100644 --- a/app/common/service/InvRequisitionApproveService.php +++ b/app/common/service/InvRequisitionApproveService.php @@ -2,9 +2,12 @@ namespace app\common\service; +use app\common\model\User; use app\common\validate\InvRequisitionApproveValidate; use app\common\traits\crud\CrudServiceTrait; use app\common\model\InvRequisitionApprove; +use support\Db; +use support\exception\BusinessException; class InvRequisitionApproveService { @@ -29,5 +32,195 @@ class InvRequisitionApproveService return new $this->validator; } + /** + * @param string $flowType + * @param string $bizType + * @param string $bizId + * @param array $formData ['manage_user_id'=>23]//物品管理员 + * @param int $initiatorId //申请人id + * @return array + */ + public function start(string $flowType, string $bizType, string $bizId, array $formData, int $initiatorId): array + { + return Db::transaction(function () use ($flowType, $bizType, $bizId, $formData, $initiatorId) { + $flow = Db::table('approval_flows')->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'); + $now = time(); + + + $service = new \app\common\utils\approval\service\ApprovalFlowService(); + + // 解析首步审批人(支持 role_id 或 roles.name) + $instance = (object)[ + 'initiator_id' => $initiatorId, + 'form_data'=>json_encode($formData, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) + ]; + $approverIds = $service->StetApprovers($firstStep, $instance, true); + + if(empty($approverIds)){ + throw new BusinessException('未解析到任何审批人'); + } + $uid = array_shift($approverIds); + $approver = User::where('id', $uid)->first(); + + $approveId = Db::table('inv_requisition_approve')->insertGetId([ + 'requisition_id' => $bizId, //申请单ID + 'node_type' => $bizType, + 'approver_id' => $approver->id, + 'approver_name' => $approver->name, + 'action' => 0, + 'comment' => '', + 'created_at' => $now, + 'updated_at' => $now, + ]); + +// foreach ($approverIds as $uid) { +// $this->insertApprovalRecord($instance, $firstIndex, $uid); +// } + + return ['instance_id' => $approveId]; + }); + } + + /** 执行审批 */ + public function approve(int $requisitionId, int $approverId, string $action, string $comment = '') + { + if (!in_array($action, ['1','2'], true)) { //审批动作:1通过 2驳回 + throw new BusinessException("无效的审批操作"); + } + + return Db::transaction(function () use ($requisitionId, $approverId, $action, $comment) { +// $instance = Db::table('approval_instances')->where('id', $instanceId)->lockForUpdate()->first(); + $requisition = Db::table('inv_requisition')->where('id', $requisitionId)->lockForUpdate()->first(); + if (!$requisition) throw new BusinessException("审批实例未找到"); + if ($requisition->status !== 'processing') { + return ['status' => 'noop', 'msg' => '实例未处理']; + } + + //审批流程 + $flow = Db::table('approval_flows')->where('type', $requisition->flow_type)->first(); + if (!$flow) throw new BusinessException("审批流程定义未找到"); + + $steps = json_decode($flow->steps, true); + //$idx = (int)$requisition->current_step; + //$step = $steps[$idx] ?? null; + //if (!$step) throw new BusinessException("当前步骤配置错误"); + + // 找到该用户在本步的 pending 记录(加锁) + $pendingRecord = Db::table('inv_requisition_approve') + ->where('requisition_id', $requisitionId) + ->where('approver_id', $approverId) + ->where('action', 0) + ->lockForUpdate() + ->first(); + + if (!$pendingRecord) { + // 已处理过? + $anyRecord = Db::table('inv_requisition_approve') + ->where('requisition_id', $requisitionId) + ->where('approver_id', $approverId) + ->first(); + if ($anyRecord && $anyRecord->action > 0) { + return ['status' => 'noop', 'msg' => '已处理']; + } + // 步骤已推进 +// if ((int)$requisition->current_step !== $idx) { +// return ['status' => 'noop', 'msg' => '非当前审批']; +// } + return ['status' => 'noop', 'msg' => '无待处理记录']; + } + + $now = date('Y-m-d H:i:s'); + + // 1) 更新当前人的审批记录 + Db::table('inv_requisition_approve')->where('id', $pendingRecord->id)->update([ + 'action' => $action, + 'comment' => $comment, + 'approved_at' => $now, + 'updated_at' => $now, + ]); + + // 2) 已通过步骤与总步数相等则结束审核流程 + $pass = Db::table('inv_requisition_approve') + ->where('requisition_id', $requisitionId) + ->where('approver_id', $approverId) + ->where('action', 1) + ->count(); + if($action == 1){ + if(count($steps) == $pass){ + $requisition->status = 12; + $requisition->save(); + return ['status' => 'complete', 'msg' => '已通过']; + } + } + + // 3) 拒绝 => 结束 + if($action == 2){ + Db::table('inv_requisition_approve')->where('id', $pendingRecord->id)->update([ + 'action' => $action, + 'comment' => $comment, + 'approved_at' => $now, + 'updated_at' => $now, + ]); + if(count($steps) == $pass){ + $requisition->status = 13; + $requisition->save(); + return ['status' => 'complete', 'msg' => '拒绝']; + } + } + + + // 4) 通过 => 进入下一步或结束 + $formData = ['manage_user_id'=>'']; + + $nextIndex = $pass; + $nextStep = $steps[$nextIndex]; + $service = new \app\common\utils\approval\service\ApprovalFlowService(); + + $instance = (object)[ + 'form_data'=>json_encode($formData, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) + ]; + $approverIds = $service->StetApprovers($nextStep, $instance, true); + $approver = User::where('id', array_pop($approverIds))->first(); + Db::table('inv_requisition_approve')->insertGetId([ + 'requisition_id' => $requisitionId, //申请单ID + 'node_type' => $nextStep['approvers']['initiator_leader'], + 'approver_id' => $approver->id, + 'approver_name' => $approver->name, + 'action' => 0, + 'comment' => '', + 'created_at' => $now, + 'updated_at' => $now, + ]);; + + }); + } + +// public function insertApprovalRecord() +// { +// } + + + /** 获取下一个有效步骤索引 */ + private function getNextStepIndex(array $steps, array $formData, int $currentStepIndex): ?int + { + for ($i = $currentStepIndex + 1; $i < count($steps); $i++) { + $s = $steps[$i]; + if (empty($s['condition'])) { + return $i; + } + } + return null; + } } \ No newline at end of file diff --git a/app/common/utils/approval/traits/ApprovalResolverTrait.php b/app/common/utils/approval/traits/ApprovalResolverTrait.php index 475d9ee..c80d15d 100644 --- a/app/common/utils/approval/traits/ApprovalResolverTrait.php +++ b/app/common/utils/approval/traits/ApprovalResolverTrait.php @@ -403,4 +403,8 @@ trait ApprovalResolverTrait return $ids; } + + public function StetApprovers(array $step, object $instance, bool $avoidSelf = true){ + return$this->resolveApprovers($step, $instance, $avoidSelf); + } } diff --git a/config/route.php b/config/route.php index ce61410..dfc9801 100644 --- a/config/route.php +++ b/config/route.php @@ -514,6 +514,10 @@ Route::group('/admin', function () { Route::any('/item/update', [app\admin\controller\InvItemController::class, 'update']); Route::any('/item/show', [app\admin\controller\InvItemController::class, 'show']); Route::any('/item/destroy', [app\admin\controller\InvItemController::class, 'destroy']); + + Route::group('/requisition', function () { + Route::any('/list', [app\admin\controller\InvItemController::class, 'requisitionList']); + }); }); }); From ab3bffe7cb1f82176a0c712f7b680c9708d3658d Mon Sep 17 00:00:00 2001 From: "WIN-C27C5MSVTPO\\16961" <1696136552@qq.com> Date: Mon, 2 Feb 2026 13:17:11 +0800 Subject: [PATCH 107/110] =?UTF-8?q?=E5=80=9F=E7=94=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/admin/controller/InvItemController.php | 15 ---- .../controller/InvRequisitionController.php | 88 +++++++++++++++++-- app/admin/service/InvRequisitionService.php | 57 ++++++++++++ .../service/InvRequisitionApproveService.php | 39 +++++--- app/common/service/InvRequisitionService.php | 7 +- config/route.php | 4 +- 6 files changed, 170 insertions(+), 40 deletions(-) diff --git a/app/admin/controller/InvItemController.php b/app/admin/controller/InvItemController.php index 516300a..ed8866f 100644 --- a/app/admin/controller/InvItemController.php +++ b/app/admin/controller/InvItemController.php @@ -52,19 +52,4 @@ class InvItemController extends BaseController $data['manage_user_name'] = User::where('id', $data['manage_user_id'])->value('name'); return $data; } - - public function requisitionList(Request $request){ - try { - $res = $this->service->getList(); - $result = []; - foreach ($res as $value) { - $value['company_name'] = Corp::where('id', $value['company_id'])->value('name'); - $value['manage_user_name'] = User::where('id', $value['manage_user_id'])->value('name'); - $result[] = $value; - } - return $this->success($result, '审核列表'); - } catch (\Throwable $e) { - return $this->error($e->getMessage()); - } - } } \ No newline at end of file diff --git a/app/admin/controller/InvRequisitionController.php b/app/admin/controller/InvRequisitionController.php index 14121f4..aabea33 100644 --- a/app/admin/controller/InvRequisitionController.php +++ b/app/admin/controller/InvRequisitionController.php @@ -2,8 +2,13 @@ namespace app\admin\controller; use app\common\controller\BaseController; +use app\common\model\InvRequisitionApprove; +use app\common\model\InvRequisitionAttachment; +use app\common\model\InvRequisitionItem; +use app\common\service\OssService; use app\common\traits\crud\CrudControllerTrait; use app\admin\service\InvRequisitionService; +use support\Request; class InvRequisitionController extends BaseController { @@ -16,18 +21,85 @@ class InvRequisitionController extends BaseController $this->service = new InvRequisitionService(); } + /** + * 审核 + * + * @param Request $request + * @return \support\Response + */ + public function approve(Request $request) + { + try { + $data = $this->service->serviceApprove($request); + return $this->success($data); + } catch (\Throwable $e) { + return $this->error($e->getMessage().$e->getFile().$e->getLine().$e->getTraceAsString()); + } + } + + /** + * 获取列表 + * + * @param Request $request 请求对象 + * @return mixed 返回分页数据及总数 + */ + public function index(Request $request) + { + try { + $raw = $this->service->serviceList($request); + $data = $this->transformData($raw, method_exists($this, 'indexFormat') + ? [$this, 'indexFormat'] + : null); + return $this->success($data); + } catch (\Throwable $e) { + return $this->error($e->getMessage()); + } + } + /** * 列表页数据格式化 * * @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['id'])){ +// $data['approve'] = InvRequisitionApprove::where('requisition_id', $data['id'])->get(); +// } + if (isset($data['created_at'])) + $data['created_at'] = date('Y-m-d H:i:s', $data['created_at']); + if (isset($data['updated_at'])) + $data['updated_at'] = date('Y-m-d H:i:s', $data['updated_at']); + if (isset($data['id'])){ + $data['item'] = InvRequisitionItem::where('requisition_id', $data['id'])->get(); + } + return $data; + } + + /** + * 列表页数据格式化 + * + * @param array $data + * @return array + */ + protected function showFormat($data) + { + if (isset($data['id'])){ + $data['approve'] = InvRequisitionApprove::where('requisition_id', $data['id'])->get(); + } + if (isset($data['created_at'])) + $data['created_at'] = date('Y-m-d H:i:s', $data['created_at']); + if (isset($data['updated_at'])) + $data['updated_at'] = date('Y-m-d H:i:s', $data['updated_at']); + if (isset($data['id'])){ + $data['item'] = InvRequisitionItem::where('requisition_id', $data['id'])->get(); + $data['attachment'] = InvRequisitionAttachment::where('requisition_id', $data['id'])->get(); + $oss = new OssService(); + foreach ($data['attachment'] as $key => $value) { + $data['attachment'][$key]['file_path_pull'] = $oss->generatePrivateUrl($value['file_path']); + } + } + return $data; + } } \ No newline at end of file diff --git a/app/admin/service/InvRequisitionService.php b/app/admin/service/InvRequisitionService.php index 88e84ae..a853ae7 100644 --- a/app/admin/service/InvRequisitionService.php +++ b/app/admin/service/InvRequisitionService.php @@ -5,6 +5,7 @@ namespace app\admin\service; use app\common\validate\InvRequisitionValidate; use app\common\traits\crud\CrudServiceTrait; use app\common\model\InvRequisition; +use support\Request; class InvRequisitionService { @@ -28,4 +29,60 @@ class InvRequisitionService { return new $this->validator; } + + /** + * 构造 CrudUtil 查询配置 + */ + protected function buildQueryConfig(Request $request, string $scene = 'default'): array + { + $p = $request->all(); + + $config = []; + $config['model'] = $this->modelClass; + switch ($scene) { + case 'default': + //$config['where']['status'] = 1; + $config['order']['id'] = 'desc'; + break; + case 'index': + if(isset($p['status']) && $p['status']){ + $config['where']['status'] = $p['status']; + } + if(isset($p['applicant_id']) && $p['applicant_id']){ + $config['where']['applicant_id'] = $p['applicant_id']; + } + $config['order']['id'] = 'desc'; + break; + case 'show': + $config['where']['id'] = $p['id']; + //$config['where']['status'] = 1; + break; + case 'store': + break; + case 'update': + $config['where']['id'] = $p['id']; + break; + case 'destroy': + $config['where']['id'] = $p['id']; + break; + } + + return $config; + } + + /** + * 审批 + * @param Request $request + * @return mixed|string[]|null + */ + public function serviceApprove(Request $request) + { + $param = $request->all(); + //$appro = new \app\common\utils\approval\service\ApprovalFlowService(); + $appro = new \app\common\service\InvRequisitionApproveService(); + //requisition_id:申请id + //action:审批动作:1通过 2驳回 + //comment:审批意见 + return $appro->approve($param['requisition_id'], $param['approver_id'], $request->user->uid, $param['action'], $param['comment']); + } } \ No newline at end of file diff --git a/app/common/service/InvRequisitionApproveService.php b/app/common/service/InvRequisitionApproveService.php index 56996db..6d438f4 100644 --- a/app/common/service/InvRequisitionApproveService.php +++ b/app/common/service/InvRequisitionApproveService.php @@ -93,7 +93,7 @@ class InvRequisitionApproveService } /** 执行审批 */ - public function approve(int $requisitionId, int $approverId, string $action, string $comment = '') + public function approve(int $requisitionId, int $approverId, int $uid, string $action, string $comment = '') { if (!in_array($action, ['1','2'], true)) { //审批动作:1通过 2驳回 throw new BusinessException("无效的审批操作"); @@ -103,12 +103,12 @@ class InvRequisitionApproveService // $instance = Db::table('approval_instances')->where('id', $instanceId)->lockForUpdate()->first(); $requisition = Db::table('inv_requisition')->where('id', $requisitionId)->lockForUpdate()->first(); if (!$requisition) throw new BusinessException("审批实例未找到"); - if ($requisition->status !== 'processing') { + if ($requisition->status !== 10 && $requisition->status !== 11) { return ['status' => 'noop', 'msg' => '实例未处理']; } //审批流程 - $flow = Db::table('approval_flows')->where('type', $requisition->flow_type)->first(); + $flow = Db::table('approval_flows')->where('type', 'inv_requisition')->first(); if (!$flow) throw new BusinessException("审批流程定义未找到"); $steps = json_decode($flow->steps, true); @@ -119,7 +119,7 @@ class InvRequisitionApproveService // 找到该用户在本步的 pending 记录(加锁) $pendingRecord = Db::table('inv_requisition_approve') ->where('requisition_id', $requisitionId) - ->where('approver_id', $approverId) + ->where('id', $approverId) ->where('action', 0) ->lockForUpdate() ->first(); @@ -140,26 +140,26 @@ class InvRequisitionApproveService return ['status' => 'noop', 'msg' => '无待处理记录']; } - $now = date('Y-m-d H:i:s'); + $now = time(); // 1) 更新当前人的审批记录 Db::table('inv_requisition_approve')->where('id', $pendingRecord->id)->update([ 'action' => $action, 'comment' => $comment, - 'approved_at' => $now, + 'action_at' => $now, 'updated_at' => $now, ]); // 2) 已通过步骤与总步数相等则结束审核流程 $pass = Db::table('inv_requisition_approve') ->where('requisition_id', $requisitionId) - ->where('approver_id', $approverId) ->where('action', 1) ->count(); if($action == 1){ if(count($steps) == $pass){ - $requisition->status = 12; - $requisition->save(); + Db::table('inv_requisition')->where('id', $requisition->id)->update(['status'=>12]); +// $requisition->status = 12; +// $requisition->save(); return ['status' => 'complete', 'msg' => '已通过']; } } @@ -173,28 +173,41 @@ class InvRequisitionApproveService 'updated_at' => $now, ]); if(count($steps) == $pass){ - $requisition->status = 13; - $requisition->save(); + Db::table('inv_requisition')->where('id', $requisition->id)->update(['status'=>12]); +// $requisition->status = 13; +// $requisition->save(); return ['status' => 'complete', 'msg' => '拒绝']; } } // 4) 通过 => 进入下一步或结束 - $formData = ['manage_user_id'=>'']; + $requisitionItem = Db::table('inv_requisition_item')->where('requisition_id', $requisitionId)->first(); + $item_id = Db::table('inv_item')->where('id', $requisitionItem->item_id)->first(); + $formData = ['manage_user_id'=>$item_id->manage_user_id]; $nextIndex = $pass; $nextStep = $steps[$nextIndex]; $service = new \app\common\utils\approval\service\ApprovalFlowService(); $instance = (object)[ + 'initiator_id' => $requisition->applicant_id, //申请人id 'form_data'=>json_encode($formData, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ]; $approverIds = $service->StetApprovers($nextStep, $instance, true); +// if($approverIds == "manage_user_id"){ //物品管理人审核步骤查询管理人 +// $requisitionItem = Db::table('inv_requisition_item')->where('id', $requisitionId)->first(); +// $invItem = Db::table('inv_requisition_item')->where('id', $requisitionItem->item_id)->first(); +// +// if(empty($invItem)){ +// return ['status' => 'noop', 'msg' => '未查询到物品管理人']; +// } +// $approverIds = [$invItem->manage_user_id]; +// } $approver = User::where('id', array_pop($approverIds))->first(); Db::table('inv_requisition_approve')->insertGetId([ 'requisition_id' => $requisitionId, //申请单ID - 'node_type' => $nextStep['approvers']['initiator_leader'], + 'node_type' => $nextStep['approvers'][0]['value'], 'approver_id' => $approver->id, 'approver_name' => $approver->name, 'action' => 0, diff --git a/app/common/service/InvRequisitionService.php b/app/common/service/InvRequisitionService.php index 20564e6..1c23acb 100644 --- a/app/common/service/InvRequisitionService.php +++ b/app/common/service/InvRequisitionService.php @@ -139,7 +139,7 @@ class InvRequisitionService return $res; }); } catch (\Throwable $e) { - throw new BusinessException("创建物品申请失败:" . $e->getMessage()); + throw new BusinessException("创建物品申请失败:" . $e->getMessage().$e->getLine().$e->getFile().$e->getTraceAsString()); } } @@ -149,11 +149,12 @@ class InvRequisitionService public function startApproval($requisition) { // 启动审批 - $appro = new \app\common\utils\approval\service\ApprovalFlowService(); + //$appro = new \app\common\utils\approval\service\ApprovalFlowService(); + $appro = new \app\common\service\InvRequisitionApproveService(); $formData = [ 'manage_user_id' => $requisition['manage_user_id'], ]; - $appro->start('inv_requisition', 'inv_requisition', $requisition['id'], $formData, $requisition['applicant_id']); + return $appro->start('inv_requisition', 'inv_requisition', $requisition['id'], $formData, $requisition['applicant_id']); } /** diff --git a/config/route.php b/config/route.php index dfc9801..58a4ec6 100644 --- a/config/route.php +++ b/config/route.php @@ -516,7 +516,9 @@ Route::group('/admin', function () { Route::any('/item/destroy', [app\admin\controller\InvItemController::class, 'destroy']); Route::group('/requisition', function () { - Route::any('/list', [app\admin\controller\InvItemController::class, 'requisitionList']); + Route::any('/list', [app\admin\controller\InvRequisitionController::class, 'index']); + Route::any('/show', [app\admin\controller\InvRequisitionController::class, 'show']); + Route::any('/approve', [app\admin\controller\InvRequisitionController::class, 'approve']); }); }); }); From 6496a406798ab26c7018b569425843b8719d7c85 Mon Sep 17 00:00:00 2001 From: "WIN-C27C5MSVTPO\\16961" <1696136552@qq.com> Date: Mon, 2 Feb 2026 15:25:16 +0800 Subject: [PATCH 108/110] =?UTF-8?q?=E7=89=A9=E5=93=81=E5=BD=92=E8=BF=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/common/service/InvRequisitionItemReturnService.php | 3 ++- app/mobile/controller/InvRequisitionItemReturnController.php | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/app/common/service/InvRequisitionItemReturnService.php b/app/common/service/InvRequisitionItemReturnService.php index bdadafc..d9fb66a 100644 --- a/app/common/service/InvRequisitionItemReturnService.php +++ b/app/common/service/InvRequisitionItemReturnService.php @@ -178,7 +178,8 @@ class InvRequisitionItemReturnService $created = $this->logicCreate($item); } $this->processRequisitionItemReturnAttachment($res, $attachment); - return $this->postProcess('Store', $created); + $this->postProcess('Store', $created); + return $res; }); } catch (\Throwable $e) { throw new BusinessException("创建物品申请失败:" . $e->getMessage().$e->getTraceAsString()); diff --git a/app/mobile/controller/InvRequisitionItemReturnController.php b/app/mobile/controller/InvRequisitionItemReturnController.php index 58df807..77ffae5 100644 --- a/app/mobile/controller/InvRequisitionItemReturnController.php +++ b/app/mobile/controller/InvRequisitionItemReturnController.php @@ -34,7 +34,7 @@ class InvRequisitionItemReturnController extends BaseController /** - * 物品兑换确认 + * 物品归还确认 * * @param Request $request * @return mixed From a16429ff3ab9057240591912012f0f46280c8025 Mon Sep 17 00:00:00 2001 From: "WIN-C27C5MSVTPO\\16961" <1696136552@qq.com> Date: Wed, 4 Feb 2026 09:28:54 +0800 Subject: [PATCH 109/110] =?UTF-8?q?=E7=89=A9=E5=93=81=E5=BD=92=E8=BF=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../InvRequisitionReturnController.php | 45 +++++++++++++++ .../InvRequisitionItemReturnService.php | 16 +++++- .../service/InvRequisitionReturnService.php | 57 +++++++++++++++++++ app/common/service/InvRequisitionService.php | 10 +++- .../controller/InvRequisitionController.php | 19 +++++++ .../InvRequisitionItemReturnController.php | 19 +++++++ .../InvRequisitionReturnController.php | 20 ++++--- config/route.php | 23 ++++++-- 8 files changed, 192 insertions(+), 17 deletions(-) create mode 100644 app/admin/controller/InvRequisitionReturnController.php diff --git a/app/admin/controller/InvRequisitionReturnController.php b/app/admin/controller/InvRequisitionReturnController.php new file mode 100644 index 0000000..9f4faef --- /dev/null +++ b/app/admin/controller/InvRequisitionReturnController.php @@ -0,0 +1,45 @@ +service = new InvRequisitionReturnService(); + } + +// /** +// * 列表页数据格式化 +// * +// * @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 returnConfirm(Request $request){ + try { + // 处理申请逻辑 + $result = $this->service->serviceReturnConfirm($request); + + return $this->success($result, '申请提交成功'); + } catch (\Throwable $e) { + return $this->error($e->getMessage()); + } + } +} \ No newline at end of file diff --git a/app/common/service/InvRequisitionItemReturnService.php b/app/common/service/InvRequisitionItemReturnService.php index d9fb66a..7200a8a 100644 --- a/app/common/service/InvRequisitionItemReturnService.php +++ b/app/common/service/InvRequisitionItemReturnService.php @@ -178,6 +178,9 @@ class InvRequisitionItemReturnService $created = $this->logicCreate($item); } $this->processRequisitionItemReturnAttachment($res, $attachment); + InvRequisition::where('id',$request->post('requisition_id'))->update( + ['status' => 14] //审批状态:10默认、11审批中、12已审批(待归还)、13拒绝、14申请归还、15归还审核中、16部分归还、17已全部归还、18已拒绝 + ); $this->postProcess('Store', $created); return $res; }); @@ -406,6 +409,7 @@ class InvRequisitionItemReturnService // 全部非耗材都已归还 $affected = InvRequisition::where('id', $requisitionId)->update([ 'is_returned' => 1, + 'status' => 17, 'updated_at' => time(), ]); @@ -413,6 +417,12 @@ class InvRequisitionItemReturnService if ($affected === 0) { throw new BusinessException('更新领用单状态失败,可能记录不存在'); } + }else{ + // 非耗材未全部都已归还 + $affected = InvRequisition::where('id', $requisitionId)->update([ + 'status' => 16, + 'updated_at' => time(), + ]); } // 7. 返回处理结果(保持你原来的 postProcess 习惯) @@ -444,13 +454,13 @@ class InvRequisitionItemReturnService $config['where']['id'] = $p['requisition_item_return_id']; break; case 'index': - $config['where']['status'] = 1; - $config['order']['sort'] = 'desc'; + if(isset($p['status'])){ + $config['where']['status'] = $p['status']; + } $config['order']['id'] = 'desc'; break; case 'show': $config['where']['id'] = $p['id']; - $config['where']['status'] = 1; break; case 'store': break; diff --git a/app/common/service/InvRequisitionReturnService.php b/app/common/service/InvRequisitionReturnService.php index e2f1d13..eef5322 100644 --- a/app/common/service/InvRequisitionReturnService.php +++ b/app/common/service/InvRequisitionReturnService.php @@ -5,6 +5,8 @@ namespace app\common\service; use app\common\validate\InvRequisitionReturnValidate; use app\common\traits\crud\CrudServiceTrait; use app\common\model\InvRequisitionReturn; +use support\exception\BusinessException; +use support\Request; class InvRequisitionReturnService { @@ -28,4 +30,59 @@ class InvRequisitionReturnService { return new $this->validator; } + + public function serviceReturnConfirm(Request$request) + { + $status = $request->post('status'); + if(in_array($status,[1,2])){ + throw new BusinessException("状态不正确"); + } + $requisitionReturn = InvRequisitionReturn::where(['id' => $request['id']])->first(); + if (!$requisitionReturn && $requisitionReturn->handler_id == $request->user->uid) { + $requisitionReturn->status = $status; + return $requisitionReturn->save(); + }else{ + throw new BusinessException("审核示例不正确或非物品管理员"); + } + } + + + /** + * 构建查询配置 + * + * @param Request $request + * @param string $scene + * @return array + */ + protected function buildQueryConfig(Request $request, string $scene = 'default'): array + { + $p = $request->all(); + + $config = []; + $config['model'] = $this->modelClass; + switch ($scene) { + case 'returnConfirm': + $config['where']['id'] = $p['requisition_item_return_id']; + break; + case 'index': + if(isset($p['status'])){ + $config['where']['status'] = $p['status']; + } + $config['order']['id'] = 'desc'; + break; + case 'show': + $config['where']['id'] = $p['id']; + break; + case 'store': + break; + case 'update': + $config['where']['id'] = $p['id']; + break; + case 'destroy': + $config['where']['id'] = $p['id']; + break; + } + + return $config; + } } \ No newline at end of file diff --git a/app/common/service/InvRequisitionService.php b/app/common/service/InvRequisitionService.php index 1c23acb..5e49baf 100644 --- a/app/common/service/InvRequisitionService.php +++ b/app/common/service/InvRequisitionService.php @@ -52,7 +52,15 @@ class InvRequisitionService $config['order']['id'] = 'desc'; break; case 'index': - $config['where']['applicant_id'] = $request->user->uid; + if(isset($p['status']) && $p['status']){ + $config['where']['status'] = $p['status']; + } + if(isset($p['applicant_id']) && $p['applicant_id'] == 'self'){ + $config['where']['applicant_id'] = $request->user->uid; + + }elseif(isset($p['applicant_id']) && $p['applicant_id']){ + $config['where']['applicant_id'] = $p['applicant_id']; + } $config['order']['id'] = 'desc'; break; case 'show': diff --git a/app/mobile/controller/InvRequisitionController.php b/app/mobile/controller/InvRequisitionController.php index 0508ace..033c7e5 100644 --- a/app/mobile/controller/InvRequisitionController.php +++ b/app/mobile/controller/InvRequisitionController.php @@ -43,6 +43,25 @@ class InvRequisitionController extends BaseController } } + /** + * 获取列表 + * + * @param Request $request 请求对象 + * @return mixed 返回分页数据及总数 + */ + public function index(Request $request) + { + try { + $raw = $this->service->serviceList($request); + $data = $this->transformData($raw, method_exists($this, 'indexFormat') + ? [$this, 'indexFormat'] + : null); + return $this->success($data); + } catch (\Throwable $e) { + return $this->error($e->getMessage()); + } + } + /** * 列表页数据格式化 * diff --git a/app/mobile/controller/InvRequisitionItemReturnController.php b/app/mobile/controller/InvRequisitionItemReturnController.php index 77ffae5..a5d8dd1 100644 --- a/app/mobile/controller/InvRequisitionItemReturnController.php +++ b/app/mobile/controller/InvRequisitionItemReturnController.php @@ -17,6 +17,25 @@ class InvRequisitionItemReturnController extends BaseController $this->service = new InvRequisitionItemReturnService(); } + /** + * 获取列表 + * + * @param Request $request 请求对象 + * @return mixed 返回分页数据及总数 + */ + public function index(Request $request) + { + try { + $raw = $this->service->serviceList($request); + $data = $this->transformData($raw, method_exists($this, 'indexFormat') + ? [$this, 'indexFormat'] + : null); + return $this->success($data); + } catch (\Throwable $e) { + return $this->error($e->getMessage()); + } + } + /** * 列表页数据格式化 * diff --git a/app/mobile/controller/InvRequisitionReturnController.php b/app/mobile/controller/InvRequisitionReturnController.php index 1885fb3..bee45ad 100644 --- a/app/mobile/controller/InvRequisitionReturnController.php +++ b/app/mobile/controller/InvRequisitionReturnController.php @@ -2,6 +2,7 @@ namespace app\mobile\controller; use app\common\controller\BaseController; +use app\common\model\InvRequisitionItem; use app\common\traits\crud\CrudControllerTrait; use app\common\service\InvRequisitionReturnService; @@ -22,12 +23,15 @@ class InvRequisitionReturnController 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['id'])) { + $data['item'] = InvRequisitionItem::where('requisition_id', $data['id'])->get(); + } + if (isset($data['created_at'])) + $data['created_at'] = date('Y-m-d H:i:s', $data['created_at']); + if (isset($data['updated_at'])) + $data['updated_at'] = date('Y-m-d H:i:s', $data['updated_at']); + return $data; + } } \ No newline at end of file diff --git a/config/route.php b/config/route.php index 58a4ec6..e892c3e 100644 --- a/config/route.php +++ b/config/route.php @@ -520,6 +520,9 @@ Route::group('/admin', function () { Route::any('/show', [app\admin\controller\InvRequisitionController::class, 'show']); Route::any('/approve', [app\admin\controller\InvRequisitionController::class, 'approve']); }); + // 物品归还 + Route::any('/requisition/return', [app\admin\controller\InvRequisitionReturnController::class, 'store']); +// Route::any('/requisition/return', [app\admin\controller\InvRequisitionReturnController::class, 'returnConfirm']); }); }); @@ -652,14 +655,24 @@ Route::group('/mobile', function () { Route::any('/item/preset', [app\mobile\controller\InvItemController::class, 'preset']); // 物品领用申请 - Route::any('/requisition/list', [app\mobile\controller\InvRequisitionController::class, 'index']); - Route::any('/requisition/show', [app\mobile\controller\InvRequisitionController::class, 'show']); +// Route::any('/requisition/list', [app\mobile\controller\InvRequisitionController::class, 'index']); +// Route::any('/requisition/show', [app\mobile\controller\InvRequisitionController::class, 'show']); Route::any('/requisition/apply', [app\mobile\controller\InvRequisitionController::class, 'apply']); Route::any('/requisition/update', [app\mobile\controller\InvRequisitionController::class, 'update']); + + + Route::group('/requisition', function () { + Route::any('/list', [app\mobile\controller\InvRequisitionController::class, 'index']); + Route::any('/show', [app\mobile\controller\InvRequisitionController::class, 'show']); +// Route::any('/approve', [app\mobile\controller\InvRequisitionController::class, 'approve']); + }); // 物品归还 - Route::any('/requisition/item/return', [app\mobile\controller\InvRequisitionItemReturnController::class, 'store']); - Route::any('/requisition/item/return/update', [app\mobile\controller\InvRequisitionItemReturnController::class, 'update']); - Route::any('/requisition/item/return/confirm', [app\mobile\controller\InvRequisitionItemReturnController::class, 'returnConfirm']); + Route::group('/requisition/return', function () { + Route::any('/store', [app\mobile\controller\InvRequisitionItemReturnController::class, 'store']); + Route::any('/update', [app\mobile\controller\InvRequisitionItemReturnController::class, 'update']); + Route::any('/list', [app\mobile\controller\InvRequisitionItemReturnController::class, 'index']); + Route::any('/confirm', [app\mobile\controller\InvRequisitionItemReturnController::class, 'returnConfirm']); + }); }); }); \ No newline at end of file From a8cab8c51c12ca0c45910c7887b782e56f0d32d3 Mon Sep 17 00:00:00 2001 From: "WIN-C27C5MSVTPO\\16961" <1696136552@qq.com> Date: Thu, 5 Feb 2026 09:33:27 +0800 Subject: [PATCH 110/110] =?UTF-8?q?=E9=A2=86=E7=94=A8=E8=80=97=E6=9D=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/common/service/InvRequisitionApproveService.php | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/app/common/service/InvRequisitionApproveService.php b/app/common/service/InvRequisitionApproveService.php index 6d438f4..940be41 100644 --- a/app/common/service/InvRequisitionApproveService.php +++ b/app/common/service/InvRequisitionApproveService.php @@ -157,7 +157,16 @@ class InvRequisitionApproveService ->count(); if($action == 1){ if(count($steps) == $pass){ - Db::table('inv_requisition')->where('id', $requisition->id)->update(['status'=>12]); + $requisitionItem = Db::table('inv_requisition_item') + ->where('requisition_id', $requisition->id) + ->where('is_consumable_snap', 1) + ->get(); + if(!$requisitionItem){ + Db::table('inv_requisition')->where('id', $requisition->id)->update(['status'=>12]); + }else{ + Db::table('inv_requisition')->where('id', $requisition->id)->update(['status'=>17]); + } + // $requisition->status = 12; // $requisition->save(); return ['status' => 'complete', 'msg' => '已通过'];