Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3d96152ea9 | |||
| cd0d850f89 | |||
| 515a16a633 | |||
| f0abdca45a | |||
| 00ab988469 | |||
| d388b6ace9 | |||
| f388ad41d6 | |||
| 8b3778e83e | |||
| 80fbf6c35a | |||
| 6c59fdef10 | |||
| aabd0f4634 | |||
| 359755fdf1 | |||
| a0935c4332 |
@@ -8,3 +8,6 @@
|
||||
/tests/.phpunit.result.cache
|
||||
/public/uploads
|
||||
/webman
|
||||
/start.php
|
||||
/windows.php
|
||||
/bootstrap.php
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
namespace app\admin\controller;
|
||||
|
||||
use app\common\controller\BaseController;
|
||||
use app\common\traits\crud\CrudControllerTrait;
|
||||
use app\common\service\FinanceReconDubaiFilesService;
|
||||
|
||||
class FinanceReconDubaiFilesController extends BaseController
|
||||
{
|
||||
use CrudControllerTrait;
|
||||
|
||||
protected $service;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->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;
|
||||
// }
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
namespace app\admin\controller;
|
||||
|
||||
use app\common\controller\BaseController;
|
||||
use app\common\traits\crud\CrudControllerTrait;
|
||||
use app\admin\service\SysConfigService;
|
||||
|
||||
class SysConfigController extends BaseController
|
||||
{
|
||||
use CrudControllerTrait;
|
||||
|
||||
protected $service;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
<?php
|
||||
|
||||
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
|
||||
{
|
||||
/**
|
||||
* 收款记录统计
|
||||
* @param Request $request
|
||||
* @return \support\Response
|
||||
*/
|
||||
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'] = array_values($this->receiptStatisticsSum($query, $currency)); //各币种的已认领金额
|
||||
$result['claimed_amount_sum'] =$this->amountSum($result['claimed_amount']); //已认领的人民币总金额
|
||||
|
||||
//未入账数量
|
||||
$query2 = FinReceipt::whereNull('received_at');
|
||||
$result['unreceived_num'] = $query2->count();
|
||||
//未入账金额
|
||||
// $result['unreceived_amount'] = $query2->sum('amount');
|
||||
$result['unreceived_amount'] = array_values($this->receiptStatisticsSum($query2, $currency)); //各币种的未入账金额
|
||||
$result['unreceived_amount_sum'] = $this->amountSum($result['unreceived_amount']); //未入账的已认领的人民币总金额
|
||||
|
||||
//今日入账
|
||||
$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();
|
||||
//未认领金额
|
||||
// $result['unclaimed_amount'] = $query3->sum('amount');
|
||||
$result['unclaimed_amount'] = $this->receiptStatisticsSum($query3, $currency); //各币种的未认领金额
|
||||
$result['unclaimed_amount_sum'] = $this->amountSum($result['unclaimed_amount']); //未认领的已认领的人民币总金额
|
||||
|
||||
//已超时未认领数量
|
||||
$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_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_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'] = $result['unclaimed_payment_num'] - $result['unclaimed_num'];
|
||||
|
||||
//未认领金额重新组织索引
|
||||
$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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,182 @@
|
||||
<?php
|
||||
namespace app\admin\controller\dashboard;
|
||||
|
||||
use app\common\controller\BaseController;
|
||||
use support\Db;
|
||||
use support\Request;
|
||||
|
||||
class OfficeClerkController extends BaseController
|
||||
{
|
||||
|
||||
protected $service;
|
||||
|
||||
public function __construct()
|
||||
{}
|
||||
|
||||
// 卡片
|
||||
public function card(Request $request)
|
||||
{
|
||||
$approved = Db::table('approval_records')
|
||||
->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)
|
||||
->where('fin_payment_apply.submit_files', '')
|
||||
->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(Request $request)
|
||||
{
|
||||
$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')
|
||||
->where('group_id', $groupId)
|
||||
->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')
|
||||
->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')
|
||||
->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.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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace app\admin\logic;
|
||||
|
||||
use app\common\traits\crud\CrudLogicTrait;
|
||||
use app\common\model\SysConfig;
|
||||
|
||||
class SysConfigLogic
|
||||
{
|
||||
use CrudLogicTrait;
|
||||
|
||||
/**
|
||||
* 指定当前要操作的模型
|
||||
*/
|
||||
protected string $modelClass = SysConfig::class;
|
||||
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
@@ -296,7 +254,174 @@ 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;
|
||||
$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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 抓取车型数据(通过接口)
|
||||
* @param Request $request
|
||||
* @return array
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function crawlKeyword(Request $request): array
|
||||
{
|
||||
$keyword = $request->get('keyword');
|
||||
|
||||
// 执行抓取
|
||||
$crawlService = new CrawlService();
|
||||
|
||||
//懂车帝模糊搜索返回车系地址
|
||||
$href = $crawlService->modelSearch($keyword);
|
||||
$seriesId = $this->extractSeriesId($href); //懂车帝车系id
|
||||
|
||||
if (empty($seriesId)) { //没有抓取到懂车帝系列id时,按关键词为品牌名抓取所有的系列名
|
||||
$crawlSeries = $crawlService->brandSeries($keyword);
|
||||
}
|
||||
|
||||
if (empty($seriesId) && empty($crawlSeries)) {
|
||||
throw new \Exception('抓取失败,未找到该系列车型');
|
||||
}
|
||||
|
||||
$brandSerieses = [];
|
||||
if(empty($seriesId) && !empty($crawlSeries)){ //关键词为品牌名时抓取所有车系的懂车帝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 {
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @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();
|
||||
@@ -354,24 +479,61 @@ class BrandSeriesService
|
||||
return $msg;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 从车型名中提取年份
|
||||
* @param string $carModel 车型名称
|
||||
* @return string|null 提取到的年份或null
|
||||
* 获取车系下车型数据
|
||||
* @return array
|
||||
* @throws \GuzzleHttp\Exception\GuzzleException
|
||||
*/
|
||||
function extractYearFromCarModel($carModel)
|
||||
public function crawlSeriesModel($seriesId)
|
||||
{
|
||||
// 匹配4位数字 + "款" 的模式
|
||||
if (preg_match('/(\d{4})款/', $carModel, $matches)) {
|
||||
return $matches[1];
|
||||
$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'],
|
||||
];
|
||||
}
|
||||
|
||||
// 匹配4位数字 + 空格或结尾的模式
|
||||
if (preg_match('/^(\d{4})(?=\s|$)/', $carModel, $matches)) {
|
||||
return $matches[1];
|
||||
}
|
||||
return array_values($models);
|
||||
|
||||
return null;
|
||||
} catch (\Exception $e) {
|
||||
throw new \Exception($e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
<?php
|
||||
|
||||
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
|
||||
{
|
||||
|
||||
use CrudServiceTrait;
|
||||
|
||||
/**
|
||||
* 指定当前要操作的模型
|
||||
*/
|
||||
protected string $modelClass = SysConfig::class;
|
||||
|
||||
/**
|
||||
* 验证器
|
||||
*/
|
||||
protected $validator = SysConfigValidate::class;
|
||||
|
||||
/**
|
||||
* 获取验证器
|
||||
*/
|
||||
public function getValidator()
|
||||
{
|
||||
return new $this->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']);
|
||||
}
|
||||
}
|
||||
@@ -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'];
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace app\admin\validate;
|
||||
|
||||
use think\Validate;
|
||||
|
||||
class SysConfigValidate extends Validate
|
||||
{
|
||||
// 规则
|
||||
protected $rule = [
|
||||
|
||||
];
|
||||
|
||||
// 提示
|
||||
protected $message = [
|
||||
|
||||
];
|
||||
|
||||
// 场景
|
||||
protected $scene = [
|
||||
'store' => [
|
||||
|
||||
],
|
||||
|
||||
'update' => [
|
||||
|
||||
]
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
namespace app\command;
|
||||
|
||||
use app\common\model\User;
|
||||
use app\common\model\UserSn;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Helper\Table;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use support\Db;
|
||||
use app\common\utils\email\Emails;
|
||||
|
||||
/**
|
||||
* 批量处理用户编号(也用于调试)
|
||||
*/
|
||||
class FinanceRecoCommand extends Command
|
||||
{
|
||||
protected static $defaultName = 'finance-reco';
|
||||
protected static $defaultDescription = '';
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
|
||||
// $tmp = (new \app\common\service\OssService)->generatePrivateUrl('fdesk_test/20251104/20251104_财务.xlsx');;
|
||||
|
||||
$service = new \app\common\service\FinanceReconDubaiFilesService();
|
||||
$result = $service->dImport('fdesk_test/20251104/20251104_迪拜.xlsx');
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
}
|
||||
@@ -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,40 @@ 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());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存抓取的系列车型
|
||||
* @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());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,181 @@
|
||||
<?php
|
||||
|
||||
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;
|
||||
use app\common\model\User;
|
||||
use app\common\service\OssService;
|
||||
use app\common\traits\crud\CrudControllerTrait;
|
||||
use app\common\service\FinPaymentApplyService;
|
||||
|
||||
class FinPaymentApplyController extends BaseController
|
||||
{
|
||||
use CrudControllerTrait;
|
||||
|
||||
protected $service;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->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 (!empty($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'],
|
||||
];
|
||||
}
|
||||
}else{
|
||||
$data['submit_files'] = [];
|
||||
}
|
||||
|
||||
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']))
|
||||
$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']); //认领时间
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 数据格式化
|
||||
*
|
||||
* @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 (!empty($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'],
|
||||
];
|
||||
}
|
||||
}else{
|
||||
$data['submit_files'] = [];
|
||||
}
|
||||
|
||||
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']))
|
||||
$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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 作废
|
||||
* @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());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
<?php
|
||||
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;
|
||||
use support\Redis;
|
||||
use support\Request;
|
||||
|
||||
class FinReceiptController extends BaseController
|
||||
{
|
||||
use CrudControllerTrait;
|
||||
|
||||
protected $service;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->service = new FinReceiptService();
|
||||
}
|
||||
|
||||
/**
|
||||
* 确认入账
|
||||
* @param Request $request
|
||||
* @return \support\Response
|
||||
*/
|
||||
public function credit(Request $request)
|
||||
{
|
||||
try {
|
||||
$id = $request->input('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());
|
||||
}
|
||||
}
|
||||
|
||||
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['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'])){
|
||||
$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')??'';
|
||||
}
|
||||
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 (!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'] = [];
|
||||
}
|
||||
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')??'';
|
||||
|
||||
$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)
|
||||
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());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
<?php
|
||||
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\model\User;
|
||||
use app\common\service\OssService;
|
||||
use app\common\traits\crud\CrudControllerTrait;
|
||||
use app\common\service\FinReceiptHisService;
|
||||
use support\Request;
|
||||
|
||||
class FinReceiptHisController extends BaseController
|
||||
{
|
||||
use CrudControllerTrait;
|
||||
|
||||
protected $service;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->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;
|
||||
}
|
||||
}
|
||||
@@ -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'])
|
||||
|
||||
@@ -47,6 +47,7 @@ class VehVehicleSkuController extends BaseController
|
||||
|
||||
public function store(Request $request)
|
||||
{
|
||||
return $this->error('请优先抓取车辆信息,如抓取不到相应车型请联系管理员,暂停手动录入车辆信息');
|
||||
try {
|
||||
$data = $this->validate($this->service->getValidator(), $request->post(), 'store');
|
||||
$res = $this->service->serviceStore($request, $data);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\logic;
|
||||
|
||||
use app\common\traits\crud\CrudLogicTrait;
|
||||
use app\common\model\FinPaymentApply;
|
||||
|
||||
class FinPaymentApplyLogic
|
||||
{
|
||||
use CrudLogicTrait;
|
||||
|
||||
/**
|
||||
* 指定当前要操作的模型
|
||||
*/
|
||||
protected string $modelClass = FinPaymentApply::class;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\logic;
|
||||
|
||||
use app\common\traits\crud\CrudLogicTrait;
|
||||
use app\common\model\FinReceiptHis;
|
||||
|
||||
class FinReceiptHisLogic
|
||||
{
|
||||
use CrudLogicTrait;
|
||||
|
||||
/**
|
||||
* 指定当前要操作的模型
|
||||
*/
|
||||
protected string $modelClass = FinReceiptHis::class;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\logic;
|
||||
|
||||
use app\common\traits\crud\CrudLogicTrait;
|
||||
use app\common\model\FinReceipt;
|
||||
|
||||
class FinReceiptLogic
|
||||
{
|
||||
use CrudLogicTrait;
|
||||
|
||||
/**
|
||||
* 指定当前要操作的模型
|
||||
*/
|
||||
protected string $modelClass = FinReceipt::class;
|
||||
|
||||
public function credit($id, $data)
|
||||
{
|
||||
return FinReceipt::where('id', $id)->update($data);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\logic;
|
||||
|
||||
use app\common\traits\crud\CrudLogicTrait;
|
||||
use app\common\model\FinanceReconDubaiFiles;
|
||||
|
||||
class FinanceReconDubaiFilesLogic
|
||||
{
|
||||
use CrudLogicTrait;
|
||||
|
||||
/**
|
||||
* 指定当前要操作的模型
|
||||
*/
|
||||
protected string $modelClass = FinanceReconDubaiFiles::class;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\logic;
|
||||
|
||||
use app\common\model\SysConfig;
|
||||
use app\common\model\SysCurrency;
|
||||
use app\common\traits\crud\CrudLogicTrait;
|
||||
|
||||
class SysConfigLogic
|
||||
{
|
||||
use CrudLogicTrait;
|
||||
|
||||
/**
|
||||
* 指定当前要操作的模型
|
||||
*/
|
||||
protected string $modelClass = SysCurrency::class;
|
||||
|
||||
public function findByKey(string $key)
|
||||
{
|
||||
return SysConfig::where('status', 1)->where('config_key', $key)->get(['id', 'config_key', 'config_value']);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\model;
|
||||
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
use support\Model;
|
||||
|
||||
class FinPaymentApply extends Model
|
||||
{
|
||||
use SoftDeletes;
|
||||
/**
|
||||
* The connection name for the model.
|
||||
*
|
||||
* @var string|null
|
||||
*/
|
||||
protected $connection = 'mysql';
|
||||
|
||||
/**
|
||||
* The table associated with the model.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $table = 'fin_payment_apply';
|
||||
|
||||
/**
|
||||
* The primary key associated with the table.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $primaryKey = 'id';
|
||||
|
||||
/**
|
||||
* Indicates if the model should be timestamped.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $timestamps = true;
|
||||
|
||||
|
||||
protected $dateFormat = 'U';
|
||||
|
||||
/**
|
||||
* 允许自动填充模型字段
|
||||
*/
|
||||
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',];
|
||||
|
||||
/**
|
||||
* 不允许自动填充模型字段
|
||||
*/
|
||||
protected $guarded = [];
|
||||
|
||||
/**
|
||||
* 获取对应的收款记录(1-1,可为空)
|
||||
*/
|
||||
public function receipt()
|
||||
{
|
||||
return $this->hasOne(FinReceipt::class, 'payment_apply_id', 'id');
|
||||
}
|
||||
|
||||
/**
|
||||
* 关联的备货单(通过中间表)
|
||||
*/
|
||||
public function stocks()
|
||||
{
|
||||
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');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\model;
|
||||
|
||||
use support\Model;
|
||||
|
||||
class FinPaymentStock extends Model
|
||||
{
|
||||
/**
|
||||
* The connection name for the model.
|
||||
*
|
||||
* @var string|null
|
||||
*/
|
||||
protected $connection = 'mysql';
|
||||
|
||||
/**
|
||||
* The table associated with the model.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $table = 'fin_payment_stock';
|
||||
|
||||
/**
|
||||
* The primary key associated with the table.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $primaryKey = 'id';
|
||||
|
||||
/**
|
||||
* Indicates if the model should be timestamped.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $timestamps = true;
|
||||
|
||||
|
||||
protected $dateFormat = 'U';
|
||||
|
||||
/**
|
||||
* 允许自动填充模型字段
|
||||
*/
|
||||
protected $fillable = ['id', 'pi_no', 'payment_apply_id', 'receipt_id', 'stock_id', 'allocated_amount', 'allocated_amount_cny', 'updated_at', 'created_at'];
|
||||
|
||||
/**
|
||||
* 不允许自动填充模型字段
|
||||
*/
|
||||
protected $guarded = [];
|
||||
|
||||
// 付款申请
|
||||
public function payment_apply()
|
||||
{
|
||||
return $this->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');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\model;
|
||||
|
||||
use support\Model;
|
||||
|
||||
class FinReceipt extends Model
|
||||
{
|
||||
/**
|
||||
* The connection name for the model.
|
||||
*
|
||||
* @var string|null
|
||||
*/
|
||||
protected $connection = 'mysql';
|
||||
|
||||
/**
|
||||
* The table associated with the model.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $table = 'fin_receipt';
|
||||
|
||||
/**
|
||||
* The primary key associated with the table.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $primaryKey = 'id';
|
||||
|
||||
/**
|
||||
* Indicates if the model should be timestamped.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $timestamps = true;
|
||||
|
||||
|
||||
protected $dateFormat = 'U';
|
||||
|
||||
/**
|
||||
* 允许自动填充模型字段
|
||||
*/
|
||||
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'];
|
||||
|
||||
/**
|
||||
* 不允许自动填充模型字段
|
||||
*/
|
||||
protected $guarded = [];
|
||||
|
||||
/**
|
||||
* 对应的付款申请(1-1,可为空)
|
||||
*/
|
||||
public function payment()
|
||||
{
|
||||
return $this->belongsTo(FinPaymentApply::class, 'payment_apply_id', 'id');
|
||||
}
|
||||
|
||||
/**
|
||||
* 关联的备货单(通过中间表)
|
||||
*/
|
||||
public function stocks()
|
||||
{
|
||||
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');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\model;
|
||||
|
||||
use support\Model;
|
||||
|
||||
class FinReceiptHis extends Model
|
||||
{
|
||||
/**
|
||||
* The connection name for the model.
|
||||
*
|
||||
* @var string|null
|
||||
*/
|
||||
protected $connection = 'mysql';
|
||||
|
||||
/**
|
||||
* The table associated with the model.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $table = 'fin_receipt_his';
|
||||
|
||||
/**
|
||||
* The primary key associated with the table.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $primaryKey = 'id';
|
||||
|
||||
/**
|
||||
* Indicates if the model should be timestamped.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $timestamps = true;
|
||||
|
||||
|
||||
protected $dateFormat = 'U';
|
||||
|
||||
/**
|
||||
* 允许自动填充模型字段
|
||||
*/
|
||||
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'];
|
||||
|
||||
/**
|
||||
* 不允许自动填充模型字段
|
||||
*/
|
||||
protected $guarded = [];
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\model;
|
||||
|
||||
use support\Model;
|
||||
|
||||
class FinanceReconDubaiFiles extends Model
|
||||
{
|
||||
/**
|
||||
* The connection name for the model.
|
||||
*
|
||||
* @var string|null
|
||||
*/
|
||||
protected $connection = 'mysql';
|
||||
|
||||
/**
|
||||
* The table associated with the model.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $table = 'finance_recon_dubai_files';
|
||||
|
||||
/**
|
||||
* The primary key associated with the table.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $primaryKey = 'id';
|
||||
|
||||
/**
|
||||
* Indicates if the model should be timestamped.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $timestamps = true;
|
||||
|
||||
|
||||
protected $dateFormat = 'U';
|
||||
|
||||
/**
|
||||
* 允许自动填充模型字段
|
||||
*/
|
||||
protected $fillable = ['id', 'caiwu_file_name', 'caiwu_file_url', 'dubai_file_name', 'dubai_file_url', 'created_by', 'created_at', 'updated_at'];
|
||||
|
||||
/**
|
||||
* 不允许自动填充模型字段
|
||||
*/
|
||||
protected $guarded = [];
|
||||
}
|
||||
@@ -107,4 +107,20 @@ 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');
|
||||
}
|
||||
|
||||
/**
|
||||
* 关联收款记录(多对多,经由中间表)
|
||||
*/
|
||||
public function receipts()
|
||||
{
|
||||
return $this->belongsToMany(FinReceipt::class, 'fin_payment_stock', 'stock_id', 'receipt_id');
|
||||
}
|
||||
}
|
||||
@@ -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'];
|
||||
|
||||
/**
|
||||
* 不允许自动填充模型字段
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\model;
|
||||
|
||||
use support\Model;
|
||||
|
||||
class SysConfig extends Model
|
||||
{
|
||||
/**
|
||||
* The connection name for the model.
|
||||
*
|
||||
* @var string|null
|
||||
*/
|
||||
protected $connection = 'mysql';
|
||||
|
||||
/**
|
||||
* The table associated with the model.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $table = 'sys_config';
|
||||
|
||||
/**
|
||||
* The primary key associated with the table.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $primaryKey = 'id';
|
||||
|
||||
/**
|
||||
* Indicates if the model should be timestamped.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $timestamps = true;
|
||||
|
||||
|
||||
protected $dateFormat = 'U';
|
||||
|
||||
/**
|
||||
* 允许自动填充模型字段
|
||||
*/
|
||||
protected $fillable = ['id', 'category', 'config_key', 'config_value', 'config_type', 'description', 'status', 'created_at', 'updated_at'];
|
||||
|
||||
/**
|
||||
* 不允许自动填充模型字段
|
||||
*/
|
||||
protected $guarded = [];
|
||||
}
|
||||
@@ -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]];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,293 @@
|
||||
<?php
|
||||
|
||||
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;
|
||||
use support\Redis;
|
||||
use support\Request;
|
||||
|
||||
class FinPaymentApplyService
|
||||
{
|
||||
|
||||
use CrudServiceTrait;
|
||||
|
||||
/**
|
||||
* 指定当前要操作的模型
|
||||
*/
|
||||
protected string $modelClass = FinPaymentApply::class;
|
||||
|
||||
/**
|
||||
* 验证器
|
||||
*/
|
||||
protected $validator = FinPaymentApplyValidate::class;
|
||||
|
||||
/**
|
||||
* 获取验证器
|
||||
*/
|
||||
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['account_id']) && !empty($p['account_id'])) {
|
||||
$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'] . '%';
|
||||
$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['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'] == '""')
|
||||
$data['voucher_file'] = '';
|
||||
if($data['submit_files'] == '[]' || $data['submit_files'] == '{}' || $data['submit_files'] == '""')
|
||||
$data['submit_files'] = '';
|
||||
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)
|
||||
{
|
||||
unset($data['stock']);
|
||||
$data['submit_files'] = json_encode($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'] == '{}' || $data['voucher_file'] == '""')
|
||||
$data['voucher_file'] = '';
|
||||
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位字母数字组合
|
||||
*
|
||||
* @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 = [];
|
||||
$orWhere = [];
|
||||
|
||||
// 汇款国家
|
||||
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['scene']) && $param['scene'] == 'file_timeout') {
|
||||
$where[] = ['submit_files', ''];
|
||||
$where[] = ['claimed_at', '<', time()-24*60*60];
|
||||
$where[] = ['claimed_at', '>', 1];
|
||||
}
|
||||
|
||||
// 关键字
|
||||
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'] == 'pending'){ //待入账列表
|
||||
$query->where('receipt_id', '>' ,'0');
|
||||
$query->where('submit_files','!=','');
|
||||
$query->whereNull('received_at');
|
||||
}
|
||||
|
||||
if($param['scene'] == 'delete'){ //查询作废数据
|
||||
$query->onlyTrashed();
|
||||
}elseif ($param['scene'] == 'all'){ //查询包含作废数据的全量数据
|
||||
$query->withTrashed();
|
||||
}
|
||||
|
||||
if ((isset($param['scene']) && ($param['scene'] == 'list' || $param['scene'] == 'pending'))|| 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})";
|
||||
$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);
|
||||
}
|
||||
|
||||
public function delete($id)
|
||||
{
|
||||
$res = FinPaymentApply::where('id', $id)->delete();
|
||||
return $res;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\service;
|
||||
|
||||
use app\common\validate\FinReceiptHisValidate;
|
||||
use app\common\traits\crud\CrudServiceTrait;
|
||||
use app\common\model\FinReceiptHis;
|
||||
use support\exception\BusinessException;
|
||||
use support\Request;
|
||||
|
||||
class FinReceiptHisService
|
||||
{
|
||||
|
||||
use CrudServiceTrait;
|
||||
|
||||
/**
|
||||
* 指定当前要操作的模型
|
||||
*/
|
||||
protected string $modelClass = FinReceiptHis::class;
|
||||
|
||||
/**
|
||||
* 验证器
|
||||
*/
|
||||
protected $validator = FinReceiptHisValidate::class;
|
||||
|
||||
/**
|
||||
* 获取验证器
|
||||
*/
|
||||
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':
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,335 @@
|
||||
<?php
|
||||
|
||||
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;
|
||||
use app\common\model\FinReceipt;
|
||||
use support\exception\BusinessException;
|
||||
use support\Redis;
|
||||
use support\Request;
|
||||
|
||||
class FinReceiptService
|
||||
{
|
||||
|
||||
use CrudServiceTrait;
|
||||
|
||||
/**
|
||||
* 指定当前要操作的模型
|
||||
*/
|
||||
protected string $modelClass = FinReceipt::class;
|
||||
|
||||
/**
|
||||
* 验证器
|
||||
*/
|
||||
protected $validator = FinReceiptValidate::class;
|
||||
|
||||
|
||||
protected $logic;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->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'] = 0;
|
||||
} elseif (isset($p['payment_apply']) && $p['payment_apply'] == 'already') { //已认领
|
||||
$config['where']['payment_apply_id'] = ['>', 0];
|
||||
} elseif (isset($p['payment_apply']) && $p['payment_apply'] == 'timeout') { //超时未认领
|
||||
$config['where']['payment_apply_id'] = 0;
|
||||
$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;
|
||||
$data['fx_receipt_file'] = json_encode($data['fx_receipt_file']);
|
||||
if($data['fx_receipt_file'] == '[]' || $data['fx_receipt_file'] == '{}' || $data['fx_receipt_file'] == '""')
|
||||
$data['fx_receipt_file'] = '';
|
||||
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']);
|
||||
|
||||
$data['fx_receipt_file'] = json_encode($data['fx_receipt_file']);
|
||||
if($data['fx_receipt_file'] == '[]' || $data['fx_receipt_file'] == '{}' || $data['fx_receipt_file'] == '""')
|
||||
$data['fx_receipt_file'] = '';
|
||||
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();
|
||||
|
||||
if(isset($param['voucher_no_timeout_hour'])){
|
||||
redis::set('voucher_no_timeout_hour', $param['voucher_no_timeout_hour'], 'EX', 30);
|
||||
}
|
||||
|
||||
$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->where('payment_apply_id', 0);
|
||||
} elseif (isset($param['payment_apply']) && $param['payment_apply'] == 'already') {
|
||||
$query->where('payment_apply_id', '>', 0);
|
||||
} elseif (isset($param['payment_apply']) && $param['payment_apply'] == 'timeout') { //超时未认领
|
||||
$query->where('payment_apply_id', 0);
|
||||
$query->where('created_at', '<' ,time() - 24*60*60);
|
||||
}
|
||||
|
||||
if(isset($param['scene']) && $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);
|
||||
}
|
||||
|
||||
// 计算七天前的时间戳
|
||||
$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, $exchange_rate)
|
||||
{
|
||||
$finReceipt = FinReceipt::find($id);
|
||||
if(!empty($finReceipt->received_at)){
|
||||
throw new BusinessException('该记录已将确认入账');
|
||||
}
|
||||
|
||||
$data = [
|
||||
'received_at' => time(),
|
||||
'exchange_rate' => $exchange_rate,
|
||||
'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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新:绑定收款记录,计算折让金额
|
||||
* @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]);
|
||||
FinPaymentStock::where('payment_apply_id', $payment_apply_id)->update(['receipt_id' => $id]);
|
||||
|
||||
//折让金额
|
||||
$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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,290 @@
|
||||
<?php
|
||||
namespace app\common\service;
|
||||
|
||||
use app\common\validate\FinanceReconDubaiFilesValidate;
|
||||
use app\common\traits\crud\CrudServiceTrait;
|
||||
use app\common\model\FinanceReconDubaiFiles;
|
||||
use app\common\utils\excel\ExcelImportTrait;
|
||||
use support\Request;
|
||||
|
||||
/**
|
||||
* FinanceReconDubaiFilesService
|
||||
*
|
||||
* - 提供两类导入:
|
||||
* 1) M项目对账明细(sheet: M项目对账明细)
|
||||
* 2) 迪拜两表合并(sheets: Stock in Jebel Ali / Under shipping)
|
||||
*
|
||||
* - 公共导入流程复用 ExcelImportTrait
|
||||
* - OSS 读取通过你的 OssService->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);
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
{
|
||||
// 处理原单
|
||||
@@ -161,7 +242,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]);
|
||||
}
|
||||
@@ -270,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);
|
||||
}
|
||||
@@ -295,7 +381,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);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -364,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']);
|
||||
@@ -479,18 +569,23 @@ 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],
|
||||
];
|
||||
|
||||
// 只有当file_url不为空时才创建记录
|
||||
if (!empty($fileData['file_url'])) {
|
||||
StockOrdersFiles::create($fileData);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* (更新时)清除已存在的订单项
|
||||
@@ -560,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);
|
||||
@@ -789,6 +884,7 @@ class StockOrdersService
|
||||
'num' => 1,
|
||||
'mileage' => $v->mileage,
|
||||
'remark' => $v->remark,
|
||||
'vin' => $v->vin,
|
||||
] + $moneyValues;
|
||||
}
|
||||
return ['list' => $vehList];
|
||||
@@ -830,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, '.', ',');
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -905,6 +1002,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 +1076,57 @@ 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
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\service;
|
||||
|
||||
use app\common\logic\SysConfigLogic;
|
||||
use app\common\logic\SysCurrencyLogic;
|
||||
use app\common\logic\SystemsLogic;
|
||||
use support\Redis;
|
||||
|
||||
class SysConfigService
|
||||
{
|
||||
|
||||
protected $logic;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->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;
|
||||
}
|
||||
}
|
||||
@@ -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)) {
|
||||
|
||||
@@ -159,14 +159,10 @@ class UserService
|
||||
/** 分页列表 */
|
||||
public function serviceTeam(Request $request)
|
||||
{
|
||||
if (method_exists($this, 'complexQuery')) {
|
||||
$result = $this->complexQuery($request->get());
|
||||
} else {
|
||||
$config = $this->buildQueryConfig($request, 'team');
|
||||
$limit = (int)$request->get('size', 15);
|
||||
$page = (int)$request->get('current', 1);
|
||||
$result = $this->logicList($config, $limit, $page);
|
||||
}
|
||||
|
||||
foreach ($result as &$data){
|
||||
$data['customer_count'] = Customer::where('user_id', $data['id'])->count();
|
||||
@@ -219,9 +215,14 @@ class UserService
|
||||
$config['where']['status'] = $p['status'];
|
||||
}
|
||||
|
||||
$config['dataLimit']['type'] = 'relation';
|
||||
$config['dataLimit']['userId'] = $request->user->uid;
|
||||
$config['dataLimit']['userField'] = 'report_to';
|
||||
$config['with']= ['reportToInfo'];
|
||||
if (!in_array(1, $request->user->role_ids)) {
|
||||
$config['where']['reportToInfo.id'] = $request->user->uid;
|
||||
}
|
||||
|
||||
// $config['dataLimit']['type'] = 'relation';
|
||||
// $config['dataLimit']['userId'] = $request->user->uid;
|
||||
// $config['dataLimit']['userField'] = 'report_to';
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
@@ -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)];
|
||||
});
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
<?php
|
||||
namespace app\common\utils\excel;
|
||||
|
||||
use support\Db;
|
||||
|
||||
/**
|
||||
* ExcelImportTrait
|
||||
*
|
||||
* 把“读取 Sheet → 映射字段 → 规范化数据 → 打时间戳 → 批量入库”
|
||||
* 这条通用导入链路抽成 Trait,便于在多个 Service 里复用。
|
||||
*
|
||||
* 依赖:
|
||||
* - ExcelReader:提供 readSheet / readSheetFromUrl / remap / toDecimal / toBool / toDate
|
||||
* - 你的 OssService:通过 generatePrivateUrl($objectKey) 生成 OSS 私有签名 URL
|
||||
*/
|
||||
trait ExcelImportTrait
|
||||
{
|
||||
/**
|
||||
* 读取某个 Sheet 的“行数组”
|
||||
* - $file 可为本地路径或 URL(由 $isUrl 控制)
|
||||
* - headerRow:表头所在行(Excel 行号,从 1 开始)
|
||||
* - startRow:数据起始行(不传则为 headerRow+1)
|
||||
*/
|
||||
protected function readSheetRows(string $file, string $sheet, bool $isUrl, int $headerRow = 1, ?int $startRow = null): array
|
||||
{
|
||||
$ret = $isUrl
|
||||
? ExcelReader::readSheetFromUrl($file, $sheet, $headerRow, $startRow)
|
||||
: ExcelReader::readSheet($file, $sheet, $headerRow, $startRow);
|
||||
return $ret['rows'] ?? [];
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取某个 Sheet(传入 OSS 对象键),内部会用你的 OssService 生成签名 URL 再读取
|
||||
*/
|
||||
protected function readSheetRowsFromOssKey(string $objectKey, string $sheet, int $headerRow = 1, ?int $startRow = null): array
|
||||
{
|
||||
$signedUrl = $this->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()];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,292 @@
|
||||
<?php
|
||||
namespace app\common\utils\excel;
|
||||
|
||||
use PhpOffice\PhpSpreadsheet\IOFactory;
|
||||
use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
|
||||
use PhpOffice\PhpSpreadsheet\Shared\Date as ExcelDate;
|
||||
|
||||
class ExcelReader
|
||||
{
|
||||
/**
|
||||
* (可选)环境自检:避免因缺少扩展导致读取失败
|
||||
* - simplexml_load_string 存在性(ext-simplexml)
|
||||
* - ext-zip
|
||||
*/
|
||||
public static function assertRequirements(): void
|
||||
{
|
||||
if (!function_exists('simplexml_load_string')) {
|
||||
throw new \RuntimeException('缺少 PHP 扩展:ext-simplexml。请安装/启用后再试。');
|
||||
}
|
||||
if (!extension_loaded('zip')) {
|
||||
throw new \RuntimeException('缺少 PHP 扩展:ext-zip。请安装/启用后再试。');
|
||||
}
|
||||
// 可按需继续检查 ext-mbstring/ext-dom 等
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取指定 sheet,按某一行作为表头
|
||||
* 返回:['headers'=>[], '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;
|
||||
}
|
||||
}
|
||||
@@ -89,7 +89,7 @@
|
||||
<div class="container">
|
||||
<!-- 头部 -->
|
||||
<div class="header">
|
||||
<h1 class="title">合塑实业-汽车事业部-备货单(客户下定)</h1>
|
||||
<h1 class="title">合塑实业-汽车事业部-备货单<?php if ($data['is_final'] == 0) { echo '(客户下定)'; } else { echo '(终版)'; } ?></h1>
|
||||
<div>
|
||||
<span class="sub-title">备货单号: <?php echo htmlspecialchars($data['order_no'] ?? 'N/A'); ?></span>
|
||||
<span class="sub-title">业务员: <?php echo htmlspecialchars($data['salesperson_snapshot'] ?? ' - '); ?></span>
|
||||
@@ -126,7 +126,7 @@
|
||||
<th>车型</th>
|
||||
<th>数量</th>
|
||||
<th>颜色</th>
|
||||
<th colspan="7">费用明细</th>
|
||||
<th colspan="8">费用明细</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
@@ -142,9 +142,12 @@
|
||||
|
||||
<!-- 车型 跨4行(不含备注行) -->
|
||||
<td rowspan="4">
|
||||
<?php echo htmlspecialchars($vehicle['model_snapshot'] ?? ' - '); ?>
|
||||
<?php echo htmlspecialchars($vehicle['model_snapshot'] ?? ' - '); ?><br/><br/>
|
||||
<?php if (!empty($vehicle['mileage']) && $vehicle['mileage'] > 0): ?>
|
||||
<span class="nowrap">(<?php echo htmlspecialchars($vehicle['mileage']); ?>km)</span>
|
||||
<span class="nowrap">里程:<?php echo htmlspecialchars($vehicle['mileage']); ?>km</span>
|
||||
<?php endif; ?><br/>
|
||||
<?php if ($vehicle['vin']): ?>
|
||||
<span class="nowrap">VIN:<?php echo htmlspecialchars($vehicle['vin']); ?></span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
|
||||
@@ -160,6 +163,7 @@
|
||||
</td>
|
||||
|
||||
<!-- 费用明细(第1组)标题 -->
|
||||
<th>手续费</th>
|
||||
<th>改装费</th>
|
||||
<th>配件费</th>
|
||||
<th>收款价</th>
|
||||
@@ -171,6 +175,7 @@
|
||||
|
||||
<!-- 行2:费用明细(第1组)数据 -->
|
||||
<tr>
|
||||
<td class="nowrap"><?php echo htmlspecialchars($vehicle['handling_fee']['currency_info'] ?? ' - '); ?></td>
|
||||
<td class="nowrap"><?php echo htmlspecialchars($vehicle['retrofit_fee']['currency_info'] ?? ' - '); ?></td>
|
||||
<td class="nowrap"><?php echo htmlspecialchars($vehicle['accessory_price']['currency_info'] ?? ' - '); ?></td>
|
||||
<td class="nowrap"><?php echo htmlspecialchars($vehicle['unit_price']['currency_info'] ?? ' - '); ?></td>
|
||||
@@ -191,6 +196,7 @@
|
||||
|
||||
<!-- 行3:费用明细(第2组)标题 -->
|
||||
<tr>
|
||||
<th>加价</th>
|
||||
<th>报关价</th>
|
||||
<th>FOB运费</th>
|
||||
<th>车价</th>
|
||||
@@ -202,6 +208,7 @@
|
||||
|
||||
<!-- 行4:费用明细(第2组)数据 -->
|
||||
<tr>
|
||||
<td class="nowrap"><?php echo htmlspecialchars($vehicle['increase_price']['currency_info'] ?? ' - '); ?></td>
|
||||
<td class="nowrap"><?php echo htmlspecialchars($vehicle['clearance_price']['currency_info'] ?? ' - '); ?></td>
|
||||
<td class="nowrap"><?php echo htmlspecialchars($vehicle['fob_transport_fee']['currency_info'] ?? ' - '); ?></td>
|
||||
<td class="nowrap"><?php echo htmlspecialchars($vehicle['car_price']['currency_info'] ?? ' - '); ?></td>
|
||||
@@ -214,7 +221,7 @@
|
||||
<!-- 行5:备注(因为“编号”已跨到本行,所以这里合并剩余10列) -->
|
||||
<tr>
|
||||
<td colspan="1"><strong>备注</strong></td>
|
||||
<td colspan="9" style="text-align:left;">
|
||||
<td colspan="10" style="text-align:left;">
|
||||
<span class="info-value">
|
||||
<?php echo nl2br($vehicle['remark'] ?? ''); ?>
|
||||
</span>
|
||||
@@ -231,7 +238,7 @@
|
||||
<span class="info-label">合同汇率:</span>
|
||||
<span class="info-value"><?php echo htmlspecialchars($data['contract_exchange_rate'] ?? ' - '); ?></span>
|
||||
</td>
|
||||
<td colspan="7" style="text-align:left;">
|
||||
<td colspan="8" style="text-align:left;">
|
||||
<span class="info-label">合计到岸价:</span>
|
||||
<span class="info-value"><?php echo htmlspecialchars($data['cif_total']['currency_info'] ?? ' - '); ?></span>
|
||||
</td>
|
||||
@@ -261,7 +268,8 @@
|
||||
|
||||
<td><span class="info-label">出运方式:</span> <span class="info-value"><?php echo htmlspecialchars($data['shipping_method_snapshot'] ?? ' - '); ?></span></td>
|
||||
<td><span class="info-label">国内收货港:</span> <span class="info-value"><?php echo htmlspecialchars($data['domestic_port_snapshot'] ?? ' - '); ?></span></td>
|
||||
<td><span class="info-label">贸易条款/清关目的港:</span> <span class="info-value"><?php echo htmlspecialchars($data['trade_term_snapshot'] ?? ' - '); ?>/<?php echo htmlspecialchars($data['clearance_port_snapshot'] ?? ' - '); ?></span></td>
|
||||
<td><span class="info-label">贸易条款:</span> <span class="info-value"><?php echo htmlspecialchars($data['trade_term_snapshot'] ?? ' - '); ?></span></td>
|
||||
<td><span class="info-label">清关目的港:</span> <span class="info-value"><?php echo htmlspecialchars($data['clearance_port_snapshot'] ?? ' - '); ?></span></td>
|
||||
|
||||
<?php if ($data['is_final'] == 1): ?>
|
||||
<td><span class="info-label">提车日期:</span> <span class="info-value nowrap"><?php echo htmlspecialchars($data['pick_up_date'] ?? ' - '); ?></span></td>
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>邮箱提醒</title>
|
||||
<style type="text/css">
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
line-height: 1.6;
|
||||
color: #333333;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
.email-container {
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
.header {
|
||||
padding: 20px;
|
||||
background-color: #0056b3;
|
||||
color: white;
|
||||
text-align: center;
|
||||
}
|
||||
.content {
|
||||
padding: 20px;
|
||||
}
|
||||
.button {
|
||||
display: inline-block;
|
||||
padding: 10px 20px;
|
||||
background-color: #0056b3;
|
||||
color: white !important;
|
||||
text-decoration: none;
|
||||
border-radius: 4px;
|
||||
font-weight: bold;
|
||||
margin: 10px 0;
|
||||
}
|
||||
.footer {
|
||||
padding: 15px;
|
||||
text-align: center;
|
||||
font-size: 12px;
|
||||
color: #777777;
|
||||
background-color: #f0f0f0;
|
||||
}
|
||||
@media only screen and (max-width: 480px) {
|
||||
.email-container {
|
||||
width: 100%;
|
||||
}
|
||||
.button {
|
||||
display: block;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<!--[if mso]>
|
||||
<style type="text/css">
|
||||
.button {
|
||||
background-color: #0056b3 !important;
|
||||
color: white !important;
|
||||
}
|
||||
</style>
|
||||
<![endif]-->
|
||||
|
||||
<table class="email-container" width="100%" cellspacing="0" cellpadding="0" border="0" align="center">
|
||||
<tr>
|
||||
<td class="header">
|
||||
<h1>付款单提醒</h1>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="content">
|
||||
<p>您好,</p>
|
||||
<p>您有新的付款单待处理。请及时查看:</p>
|
||||
|
||||
<ul>
|
||||
<li>付款单编号:{{code}}</li>
|
||||
<li>付款主体:{{payment_entity}}</li>
|
||||
<li>问题描述:{{content}}</li>
|
||||
{{reply}}
|
||||
</ul>
|
||||
|
||||
<p>请点击下方按钮立即查看详情:</p>
|
||||
|
||||
<div style="text-align: center;">
|
||||
<a href="https://x.hnxinyouhe.com" class="button" style="color: white; text-decoration: none;">立即查看</a>
|
||||
</div>
|
||||
|
||||
<!--<p>如果您未请求此操作,请忽略此邮件或联系我们的客服团队。</p>-->
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="footer">
|
||||
<p>© 2025 河南新友合科技有限公司. 保留所有权利。</p>
|
||||
<p>这是系统自动发送的邮件,请勿直接回复。</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\validate;
|
||||
|
||||
use think\Validate;
|
||||
|
||||
class FinPaymentApplyValidate extends Validate
|
||||
{
|
||||
// 规则
|
||||
protected $rule = [
|
||||
|
||||
];
|
||||
|
||||
// 提示
|
||||
protected $message = [
|
||||
|
||||
];
|
||||
|
||||
// 场景
|
||||
protected $scene = [
|
||||
'store' => [
|
||||
|
||||
],
|
||||
|
||||
'update' => [
|
||||
|
||||
]
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\validate;
|
||||
|
||||
use think\Validate;
|
||||
|
||||
class FinReceiptHisValidate extends Validate
|
||||
{
|
||||
// 规则
|
||||
protected $rule = [
|
||||
|
||||
];
|
||||
|
||||
// 提示
|
||||
protected $message = [
|
||||
|
||||
];
|
||||
|
||||
// 场景
|
||||
protected $scene = [
|
||||
'store' => [
|
||||
|
||||
],
|
||||
|
||||
'update' => [
|
||||
|
||||
]
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\validate;
|
||||
|
||||
use think\Validate;
|
||||
|
||||
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',
|
||||
];
|
||||
|
||||
// 提示
|
||||
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'
|
||||
]
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\validate;
|
||||
|
||||
use think\Validate;
|
||||
|
||||
class FinanceReconDubaiFilesValidate extends Validate
|
||||
{
|
||||
// 规则
|
||||
protected $rule = [
|
||||
|
||||
];
|
||||
|
||||
// 提示
|
||||
protected $message = [
|
||||
|
||||
];
|
||||
|
||||
// 场景
|
||||
protected $scene = [
|
||||
'store' => [
|
||||
|
||||
],
|
||||
|
||||
'update' => [
|
||||
|
||||
]
|
||||
];
|
||||
}
|
||||
@@ -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)
|
||||
{
|
||||
@@ -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]);
|
||||
}
|
||||
}
|
||||
@@ -61,7 +61,7 @@ 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,
|
||||
@@ -70,34 +70,40 @@ class StockOrdersController extends BaseController
|
||||
|
||||
// 初始化文件字段
|
||||
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']);
|
||||
}
|
||||
|
||||
|
||||
+2
-1
@@ -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];
|
||||
}
|
||||
|
||||
+39
-2
@@ -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,42 @@ 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()]);
|
||||
$where = [
|
||||
['submit_files', '=', ''],
|
||||
['warn', '<', 2],
|
||||
['created_at', '>', time() - 60 * 60 * 48],
|
||||
['created_at', '<', time() - 60 * 60 * 24],
|
||||
];
|
||||
$payment = FinPaymentApply::where($where)->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()]);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
'oss_accessid' => 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')
|
||||
];
|
||||
+61
-1
@@ -109,6 +109,8 @@ 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']);
|
||||
Route::any('/crawlKeywordCreate', [app\common\controller\BrandSeriesController::class, 'crawlKeywordCreate']);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -389,7 +391,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']);
|
||||
@@ -422,6 +423,65 @@ Route::group('/admin', function () {
|
||||
Route::any('/update', [app\common\controller\SysUpdateLogController::class, 'update']);
|
||||
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('/delete', [app\common\controller\FinPaymentApplyController::class, 'delete']);
|
||||
});
|
||||
|
||||
// 系统配置
|
||||
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 () {
|
||||
// 内勤
|
||||
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']);
|
||||
});
|
||||
});
|
||||
|
||||
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']);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user