Merge branch 'develop' into feature/stock_next

This commit is contained in:
2025-09-12 15:15:14 +08:00
80 changed files with 7446 additions and 1234 deletions
@@ -0,0 +1,44 @@
<?php
namespace app\admin\controller;
use app\common\controller\BaseController;
use app\common\traits\crud\CrudControllerTrait;
use app\admin\service\ApprovalFlowsService;
class ApprovalFlowsController extends BaseController
{
use CrudControllerTrait;
protected $service;
public function __construct()
{
$this->service = new ApprovalFlowsService();
}
/**
* 列表页数据格式化
*
* @param array $data
* @return array
*/
// protected function indexFormat($data)
// {
// if (isset($data['created_at']))
// $data['created_at'] = date('Y-m-d H:i:s', $data['created_at']);
// if (isset($data['updated_at']))
// $data['updated_at'] = date('Y-m-d H:i:s', $data['updated_at']);
// return $data;
// }
public function desc()
{
// type sequential(顺序会签), parallel(并行会签), any(或签)
}
}
@@ -0,0 +1,33 @@
<?php
namespace app\admin\controller;
use app\common\controller\BaseController;
use app\common\traits\crud\CrudControllerTrait;
use app\admin\service\ApprovalInstancesService;
class ApprovalInstancesController extends BaseController
{
use CrudControllerTrait;
protected $service;
public function __construct()
{
$this->service = new ApprovalInstancesService();
}
/**
* 列表页数据格式化
*
* @param array $data
* @return array
*/
// protected function indexFormat($data)
// {
// if (isset($data['created_at']))
// $data['created_at'] = date('Y-m-d H:i:s', $data['created_at']);
// if (isset($data['updated_at']))
// $data['updated_at'] = date('Y-m-d H:i:s', $data['updated_at']);
// return $data;
// }
}
@@ -0,0 +1,90 @@
<?php
namespace app\admin\controller;
use app\common\controller\BaseController;
use app\common\traits\crud\CrudControllerTrait;
use app\admin\service\ApprovalRecordsService;
use app\admin\service\SysDictService;
use app\common\model\SysDictItem;
use support\Request;
class ApprovalRecordsController extends BaseController
{
use CrudControllerTrait;
protected $service;
public function __construct()
{
$this->service = new ApprovalRecordsService();
}
/**
* 列表页数据格式化
*
* @param array $data
* @return array
*/
protected function indexFormat($data)
{
if (isset($data['records']) && count($data['records']) > 0) {
foreach ($data['records'] as $key => &$record) {
$result = [
'id' => $record['id'],
'flows_name' => $record['approvalInstance']['flows']['name'],
'instance_id' => $record['instance_id'],
'biz_id' => $record['approvalInstance']['biz_id'],
'biz_desc' => '备货单【'.$record['approvalInstance']['bizInfo']['order_no'].'】',
'biz_type' => $record['approvalInstance']['flow_type'],
'initiator_name' => $record['approvalInstance']['initiator']['name'],
'initiator_avatar' => $record['approvalInstance']['initiator']['avatar'],
'approver_name' => $record['approver']['name'],
'approver_avatar' => $record['approver']['avatar'],
'step_index' => $record['step_index'],
'action' => $record['action'],
'action_desc' => SysDictItem::where('item_value', $record['action'])->where('status', 1)->value('item_label'),
'status' => $record['status'],
'status_desc' => SysDictItem::where('item_value', $record['status'])->where('status', 1)->value('item_label'),
'comment' => $record['comment'],
'approved_at' => $record['approved_at'],
'finished_at' => $record['approvalInstance']['finished_at'],
'deadline_at' => $record['approvalInstance']['deadline_at'],
'created_at' => $record['created_at']
];
$record = $result;
}
}
return $data;
}
public function approve(Request $request)
{
try {
$res = $this->service->serviceApprove($request);
return $this->success([], $res['msg']);
} catch (\Throwable $e) {
return $this->error($e->getMessage());
}
}
public function logs(Request $request)
{
try {
$instanceId = $request->get('instance_id');
$appro = new \app\common\utils\approval\service\ApprovalFlowService();
$res = $appro->getSimpleTimeline($instanceId, 'name', 'prepend');
return $this->success($res);
} catch (\Throwable $e) {
return $this->error($e->getMessage());
}
}
}
@@ -0,0 +1,251 @@
<?php
namespace app\admin\controller;
use app\common\controller\BaseController;
use app\common\traits\crud\CrudControllerTrait;
use app\common\service\StockOrdersService;
use support\Request;
class StockOrdersController extends BaseController
{
protected $noNeedLogin = ['print'];
protected $noNeedHeaderData = ['print'];
use CrudControllerTrait;
protected $service;
public function __construct()
{
$this->service = new StockOrdersService();
}
/**
* 列表页数据格式化
*
* @param array $data
* @return array
*/
protected function indexFormat($data)
{
if (isset($data['created_at'])) $data['created_at'] = date('Y-m-d H:i:s', $data['created_at']);
if (isset($data['updated_at'])) $data['updated_at'] = date('Y-m-d H:i:s', $data['updated_at']);
if (isset($data['eta_ship_date'])) $data['eta_ship_date'] = date('Y-m-d', $data['eta_ship_date']);
if (isset($data['biz_date'])) $data['biz_date'] = date('Y-m-d', $data['biz_date']);
if (isset($data['latest_arrival_date'])) $data['latest_arrival_date'] = date('Y-m-d', $data['latest_arrival_date']);
if (isset($data['id'])) {
$appro = new \app\common\utils\approval\service\ApprovalFlowService();
$appro = $appro->getBriefStatusByBiz($data['id'], null, 'name');
$map = ['processing'=>'审批中','pending'=>'审批中','approved'=>'已通过','rejected'=>'已拒绝','revoked'=>'已撤回'];
if ($appro) {
$data['appro_status'] = [
'status' => $appro['status'],
'status_desc' => $map[$appro['status']],
'name' => isset($appro['step'])?$appro['step']['name']:''
];
}
}
return $data;
}
protected function showFormat($data)
{
if (isset($data['created_at'])) $data['created_at'] = date('Y-m-d H:i:s', $data['created_at']);
if (isset($data['updated_at'])) $data['updated_at'] = date('Y-m-d H:i:s', $data['updated_at']);
if (isset($data['eta_ship_date'])) $data['eta_ship_date'] = date('Y-m-d', $data['eta_ship_date']);
if (isset($data['biz_date'])) $data['biz_date'] = date('Y-m-d', $data['biz_date']);
if (isset($data['latest_arrival_date'])) $data['latest_arrival_date'] = date('Y-m-d', $data['latest_arrival_date']);
if (!empty($data['contract'])) {
$data['contract_url'] = (new \app\common\service\OssService)->generatePrivateUrl($data['contract']);
}
if (count($data['moneyValues']) > 0) {
$data['cif_total_amount'] = [
'exchange_rate' => $data['moneyValues']['exchange_rate'],
'amount' => $data['moneyValues']['amount'],
'currency' => $data['moneyValues']['currency'],
'id' => $data['moneyValues']['id']
];
}
foreach ($data['items'] as &$item) {
foreach ($item['moneyValues'] as $mv) {
$item[$mv['field']] = [
'exchange_rate' => $mv['exchange_rate'],
'amount' => $mv['amount'],
'currency' => $mv['currency'],
'id' => $mv['id']
];
}
unset($item['moneyValues']);
foreach ($item['options'] as $opt) {
$item['modify_list'][] = [
"id" => $opt['id'],
"order_item_id" => $opt['order_item_id'],
"option_id" => $opt['option_id'],
"option_name_snapshot" => $opt['option_name_snapshot'],
"option_price" => $opt['option_price']
];
}
unset($item['options']);
}
unset($item);
foreach ($data['accounts'] as $acc) {
$data['account_list'][] = [
"id" => $acc['id'],
"order_id" => $acc['order_id'],
"seq_no" => $acc['seq_no'],
"remit_country_id" => $acc['remit_country_id'],
"remit_country_name_snapshot" => $acc['remit_country_name_snapshot'],
"remit_date" => date('Y-m-d', $acc['remit_date']),
"currency_code" => $acc['currency_code'],
"currency_name_snapshot" => $acc['currency_name_snapshot'],
"account_id" => $acc['account_id'],
"account_name_snapshot" => $acc['account_name_snapshot'],
"remitter_name" => $acc['remitter_name'],
"amount" => $acc['amount']
];
}
unset($data['accounts']);
$data['veh_list'] = $data['items'];
unset($data['items']);
return $data;
}
protected function printFormat($data)
{
$data['file_url_full'] = (!empty($data['file_url'])) ? (new \app\common\service\OssService)->generatePrivateUrl($data['file_url']) : '';
return $data;
}
public function print(Request $request)
{
try {
$raw = $this->service->servicePrint($request);
$data = $this->transformData($raw, method_exists($this, 'printFormat')
? [$this, 'printFormat']
: null);
return $this->success($data);
} catch (\Throwable $e) {
return $this->error($e->getMessage());
}
}
public function detail(Request $request)
{
try {
$raw = $this->service->serviceDetail($request);
$data = $this->transformData($raw, method_exists($this, 'detailFormat')
? [$this, 'detailFormat']
: null);
return $this->success($data);
} catch (\Throwable $e) {
return $this->error($e->getMessage());
}
}
public function desc(Request $request)
{
$data = [
'id' => '', //'主键',
'order_no' => '', //'备货单编号(业务唯一)',
'pi_no' => '', //'PI号'/'阿里订单号',
'biz_date' => '', //'单据日期(时间戳)',
'salesperson_id' => '', //'业务员ID(关联数据)',
'salesperson_snapshot' => '', //'业务员姓名快照',
'clearance_country_id' => '', //'清关目的国ID(关联数据)',
'clearance_country_snapshot' => '', //'清关目的国名称快照',
// 'ali_order_no' => '', //'阿里订单号',
'customer_source_id' => '', //'客户来源ID(关联数据)',
'customer_source_snapshot' => '', //'客户来源名称快照',
'customer_id' => '', //'客户ID(关联数据)',
'customer_snapshot' => '', //'客户名称快照',
'customer_country_id' => '', //'客户所在国ID(关联数据)',
'customer_country_snapshot' => '', //'客户所在国名称快照',
'declarant_name' => '', //'清关人(文本)',
'eta_ship_date' => '', //'预计发运日(时间戳)',
'shipping_method' => '', //'出运方式(如海运/空运/铁路等)',
'cif_total_amount' => '', //'0.00' COMMENT '合计到岸价(CIF)',
'domestic_port_id' => '', //'国内收货港ID(关联数据)',
'domestic_port_snapshot' => '', //'国内收货港名称快照',
'trade_term_id' => '', //'贸易条款ID(关联数据,EXW/FOB/CIF等)',
'trade_term_snapshot' => '', //'贸易条款名称快照',
'clearance_port_id' => '', //'清关目的港ID(关联数据)',
'clearance_port_snapshot' => '', //'清关目的港名称快照',
'latest_arrival_date' => '', //'最迟到港时间(时间戳)',
'contract_exchange_rate' => '', //'合同汇率(下单锁定)',
'settlement_currency' => '', //'CNY' COMMENT '结算币种',
'remark' => '', //'备注',
// 'real_veh' => '', //'0' COMMENT '实车ID',
'is_final' => '', //'0' COMMENT '是否终版1是0否',
'status' => '', //'draft' COMMENT '单据状态:draft/submitted/approved/void/reversed等',
];
// (?<=\x60 )[^\r\n']*(?=')
$items = [
// 'id' => '', // '主键',
// 'order_id' => '', // '所属备货单ID',
// 'product_code' => '', // '商品编号(可作外部编码)',
// 'sku_snapshot' => '', // '商品/车型名称快照',
// 'unit_price_value_id' => '', // '单价',
// 'handling_fee_value_id' => '', // '操作费',
// 'fob_transport_fee_value_id' => '', // 'FOB内陆运费',
// 'port_misc_fee_value_id' => '', // '港杂费',
// 'retrofit_fee_value_id' => '', // '改装费',
// 'total_price_value_id' => '', // '合计(按订单结算币计算)',
'vin' => '', // '车架号',
'sku_id' => '', // '商品/车型ID(关联数据)',
'model_snapshot' => '', // '车型(若与sku一致可复用;此处为显示快照)',
'quantity' => '', // '1.000000' COMMENT '数量',
'color_interior_id' => '', // '颜色内饰ID(关联数据)',
'color_interior_snapshot' => '', // '颜色内饰名称快照',
'color_exterior_id' => '', // '颜色外饰ID(关联数据)',
'color_exterior_snapshot' => '', // '颜色外饰名称快照',
'remark' => '', // '备注',
'unit_price' => '', // '0.000000' COMMENT '单价(含/不含税按业务口径约定)',
'handling_fee' => '', // '0.000000' COMMENT '手续费(按单车)',
'fob_transport_fee' => '', // '0.000000' COMMENT 'FOB运输费(按单车)',
'port_misc_fee' => '', // '0.000000' COMMENT '港杂(按单车)',
'retrofit_fee' => '', // '0.000000' COMMENT '改装费(按单车)',
'total_price' => '', // '0.000000' COMMENT '总价(quantity*unit_price,写时落地)',
'official_price' => '', // 官方指导价
'discount_price' => '', // 优惠价
'modify_list' => [
// 'order_item_id' => '', // '所属明细ID',
'option_id' => '', // '配置项ID(关联数据)',
'option_name_snapshot' => '', // '配置项名称快照',
'option_price' => '', // '配置项价格(可选,写时快照)',
]
];
$account = [
'order_id' => '', // '所属备货单ID',
// 'seq_no' => '', // '1' COMMENT '账户序号(行号)',
'remit_country_id' => '', // '汇款国ID(关联数据)',
'remit_country_name_snapshot' => '', // '汇款国名称快照',
'remit_date' => '', // '汇款日期(时间戳)',
'currency_code' => '', // 'USD' COMMENT '币种代码(如USD/CNY/AED等)',
'currency_name_snapshot' => '', // '币种名称快照(可选)',
'account_id' => '', // '账户ID(关联数据)',
'account_name_snapshot' => '', // '账户名称/开户名快照',
'remitter_name' => '', // '汇款人',
'amount' => '', // '0.00' COMMENT '金额',
];
}
}
+12
View File
@@ -154,4 +154,16 @@ class UserController extends BaseController
return $this->error($e->getMessage());
}
}
public function updateSign(Request $request)
{
try {
$data = $request->post();
$this->service->serviceUpdateSign($request, $data);
return $this->success([], '签名成功');
} catch (\Throwable $e) {
return $this->error($e->getMessage());
}
}
}
+17
View File
@@ -0,0 +1,17 @@
<?php
namespace app\admin\logic;
use app\common\traits\crud\CrudLogicTrait;
use app\admin\model\ApprovalFlows;
class ApprovalFlowsLogic
{
use CrudLogicTrait;
/**
* 指定当前要操作的模型
*/
protected string $modelClass = ApprovalFlows::class;
}
@@ -0,0 +1,17 @@
<?php
namespace app\admin\logic;
use app\common\traits\crud\CrudLogicTrait;
use app\admin\model\ApprovalInstances;
class ApprovalInstancesLogic
{
use CrudLogicTrait;
/**
* 指定当前要操作的模型
*/
protected string $modelClass = ApprovalInstances::class;
}
+17
View File
@@ -0,0 +1,17 @@
<?php
namespace app\admin\logic;
use app\common\traits\crud\CrudLogicTrait;
use app\admin\model\ApprovalRecords;
class ApprovalRecordsLogic
{
use CrudLogicTrait;
/**
* 指定当前要操作的模型
*/
protected string $modelClass = ApprovalRecords::class;
}
+54
View File
@@ -0,0 +1,54 @@
<?php
namespace app\admin\model;
use support\Model;
class ApprovalFlows extends Model
{
/**
* The connection name for the model.
*
* @var string|null
*/
protected $connection = 'mysql';
/**
* The table associated with the model.
*
* @var string
*/
protected $table = 'approval_flows';
/**
* The primary key associated with the table.
*
* @var string
*/
protected $primaryKey = 'id';
/**
* Indicates if the model should be timestamped.
*
* @var bool
*/
public $timestamps = false;
protected $dateFormat = 'U';
/**
* 允许自动填充模型字段
*/
protected $fillable = ['id', 'name', 'type', 'steps', 'is_active', 'timeout_hours', 'timeout_action', 'created_at', 'updated_at'];
/**
* 不允许自动填充模型字段
*/
protected $guarded = [];
protected $casts = [
'steps' => 'array',
];
}
+107
View File
@@ -0,0 +1,107 @@
<?php
namespace app\admin\model;
use app\common\model\StockOrders;
use app\common\model\User;
use support\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
class ApprovalInstances extends Model
{
/**
* The connection name for the model.
*
* @var string|null
*/
protected $connection = 'mysql';
/**
* The table associated with the model.
*
* @var string
*/
protected $table = 'approval_instances';
/**
* The primary key associated with the table.
*
* @var string
*/
protected $primaryKey = 'id';
/**
* Indicates if the model should be timestamped.
*
* @var bool
*/
public $timestamps = false;
protected $dateFormat = 'U';
/**
* 允许自动填充模型字段
*/
// protected $fillable = ['id', 'flow_type', 'biz_id', 'biz_type', 'form_data', 'initiator_id', 'status', 'current_step', 'finished_at', 'deadline_at', 'created_at', 'updated_at'];
/**
* 不允许自动填充模型字段
*/
protected $guarded = [];
protected $fillable = [
'flow_type',
'biz_id',
'biz_type',
'form_data',
'initiator_id',
'status',
'current_step',
'finished_at',
'deadline_at',
];
protected $casts = [
'form_data' => 'array', // 自动转换 JSON 字段为数组
'finished_at' => 'datetime',
'deadline_at' => 'datetime',
'created_at' => 'datetime',
'updated_at' => 'datetime',
];
/**
* 一个审批实例可以有多个审批记录
*/
public function approvalRecords(): HasMany
{
return $this->hasMany(ApprovalRecords::class, 'instance_id');
}
/**
* 获取当前步骤的所有审批记录(pending 状态)
*/
public function currentStepRecords()
{
return $this->approvalRecords()
->where('step_index', $this->current_step)
->where('status', 'pending');
}
public function flows()
{
return $this->belongsTo(ApprovalFlows::class, 'flow_type', 'type');
}
public function initiator()
{
return $this->belongsTo(User::class, 'initiator_id', 'id');
}
public function bizInfo()
{
return $this->hasOne(StockOrders::class, 'id', 'biz_id');
}
}
+84
View File
@@ -0,0 +1,84 @@
<?php
namespace app\admin\model;
use app\common\model\User;
use support\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class ApprovalRecords extends Model
{
/**
* The connection name for the model.
*
* @var string|null
*/
protected $connection = 'mysql';
/**
* The table associated with the model.
*
* @var string
*/
protected $table = 'approval_records';
/**
* The primary key associated with the table.
*
* @var string
*/
protected $primaryKey = 'id';
/**
* Indicates if the model should be timestamped.
*
* @var bool
*/
public $timestamps = false;
protected $dateFormat = 'U';
/**
* 允许自动填充模型字段
*/
// protected $fillable = ['id', 'instance_id', 'approver_id', 'step_index', 'action', 'status', 'comment', 'approved_at', 'created_at', 'updated_at'];
/**
* 不允许自动填充模型字段
*/
protected $guarded = [];
protected $fillable = [
'instance_id',
'approver_id',
'step_index',
'action',
'status',
'comment',
'approved_at',
];
protected $casts = [
'approved_at' => 'datetime',
'created_at' => 'datetime',
'updated_at' => 'datetime',
];
/**
* 一条审批记录属于一个审批实例
*/
public function approvalInstance(): BelongsTo
{
return $this->belongsTo(ApprovalInstances::class, 'instance_id');
}
/**
* 关联审批人
*/
public function approver()
{
return $this->belongsTo(User::class, 'approver_id');
}
}
@@ -0,0 +1,62 @@
<?php
namespace app\admin\service;
use app\admin\validate\ApprovalFlowsValidate;
use app\common\traits\crud\CrudServiceTrait;
use app\admin\model\ApprovalFlows;
use support\Request;
class ApprovalFlowsService
{
use CrudServiceTrait;
/**
* 指定当前要操作的模型
*/
protected string $modelClass = ApprovalFlows::class;
/**
* 验证器
*/
protected $validator = ApprovalFlowsValidate::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';
break;
case 'show':
$config['where']['id'] = $p['id'];
break;
case 'store':
break;
case 'update':
$config['where']['id'] = $p['id'];
break;
case 'destroy':
$config['where']['id'] = $p['id'];
break;
}
return $config;
}
}
@@ -0,0 +1,31 @@
<?php
namespace app\admin\service;
use app\admin\validate\ApprovalInstancesValidate;
use app\common\traits\crud\CrudServiceTrait;
use app\admin\model\ApprovalInstances;
class ApprovalInstancesService
{
use CrudServiceTrait;
/**
* 指定当前要操作的模型
*/
protected string $modelClass = ApprovalInstances::class;
/**
* 验证器
*/
protected $validator = ApprovalInstancesValidate::class;
/**
* 获取验证器
*/
public function getValidator()
{
return new $this->validator;
}
}
@@ -0,0 +1,91 @@
<?php
namespace app\admin\service;
use app\admin\validate\ApprovalRecordsValidate;
use app\common\traits\crud\CrudServiceTrait;
use app\admin\model\ApprovalRecords;
use support\Request;
class ApprovalRecordsService
{
use CrudServiceTrait;
/**
* 指定当前要操作的模型
*/
protected string $modelClass = ApprovalRecords::class;
/**
* 验证器
*/
protected $validator = ApprovalRecordsValidate::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';
$config['with'] = [
'approvalInstance',
'approver',
'approvalInstance.flows',
'approvalInstance.initiator',
'approvalInstance.bizInfo'
];
// $config['dataLimit']['type'] = 'relation';
// $config['dataLimit']['userId'] = $request->user->uid;
// $config['dataLimit']['userField'] = 'approver_id';
break;
case 'show':
$config['where']['id'] = $p['id'];
break;
case 'store':
break;
case 'update':
$config['where']['id'] = $p['id'];
break;
case 'destroy':
$config['where']['id'] = $p['id'];
break;
case 'logs':
$config['where']['instance_id'] = $p['instance_id'];
$config['order']['id'] = 'desc';
break;
}
return $config;
}
public function serviceApprove(Request $request)
{
$param = $request->all();
$appro = new \app\common\utils\approval\service\ApprovalFlowService();
return $appro->approve($param['instance_id'], $request->user->uid, $param['action'], $param['comment']);
}
public function serviceLogs(Request $request)
{
$config = $this->buildQueryConfig($request, 'logs');
$item = $this->logicListAll($config);
return $this->postProcess('Logs', $item);
}
}
+18
View File
@@ -5,6 +5,8 @@ namespace app\admin\service;
use app\admin\validate\SysCurrencyValidate;
use app\common\traits\crud\CrudServiceTrait;
use app\common\model\SysCurrency;
use app\common\utils\translation\service\TranslationService;
use app\queue\utils\Translat;
use support\Request;
class SysCurrencyService
@@ -58,4 +60,20 @@ class SysCurrencyService
return $config;
}
public $translatFields = ['currency_name'];
public function afterServiceStore($res)
{
$sourceLocale = locale();
$data = $res->toArray();
$traitsourceFields = Translat::extractSourceFields($data, $this->translatFields);
(new TranslationService())->queueTranslations(
'sys_currency',
$res['id'],
$sourceLocale,
$traitsourceFields
);
}
}
+18
View File
@@ -5,6 +5,8 @@ namespace app\admin\service;
use app\admin\validate\SysDictItemValidate;
use app\common\traits\crud\CrudServiceTrait;
use app\common\model\SysDictItem;
use app\common\utils\translation\service\TranslationService;
use app\queue\utils\Translat;
use support\Request;
class SysDictItemService
@@ -72,4 +74,20 @@ class SysDictItemService
return $config;
}
public $translatFields = ['item_label','remark'];
public function afterServiceStore($res)
{
$sourceLocale = locale();
$data = $res->toArray();
$traitsourceFields = Translat::extractSourceFields($data, $this->translatFields);
(new TranslationService())->queueTranslations(
'sys_dict_item',
$res['id'],
$sourceLocale,
$traitsourceFields
);
}
}
+115 -2
View File
@@ -5,6 +5,9 @@ namespace app\admin\service;
use app\admin\validate\SysDictValidate;
use app\common\traits\crud\CrudServiceTrait;
use app\common\model\SysDict;
use app\common\model\SysDictCategory;
use app\common\utils\translation\service\TranslationService;
use app\queue\utils\Translat;
use support\Request;
class SysDictService
@@ -80,8 +83,17 @@ class SysDictService
break;
case 'scope':
$config['order']['id'] = 'desc';
// 决定查id还是code
$scopeType = 0;
if (isset($p['scope_type']) && $p['scope_type'] > 0) {
$scopeType = 1;
}
if (isset($p['scope']) && !empty($p['scope'])) {
$config['where']['id'] = ['in', explode(',', $p['scope'])];
if ($scopeType == 1) {
$config['where']['dict_code'] = ['in', explode(',', $p['scope'])];
} else {
$config['where']['id'] = ['in', explode(',', $p['scope'])];
}
}
$config['with']['sysDictItem'][] = 'dict_id';
$config['with']['sysDictItem'][] = 'item_label';
@@ -95,7 +107,7 @@ class SysDictService
$config['with']['sysDictItem'][] = 'item_value';
break;
}
return $config;
}
@@ -124,4 +136,105 @@ class SysDictService
return $data;
}
public $translatFields = ['dict_name', 'description'];
public function afterServiceStore($res)
{
$sourceLocale = locale();
$data = $res->toArray();
$traitsourceFields = Translat::extractSourceFields($data, $this->translatFields);
(new TranslationService())->queueTranslations(
'sys_dict',
$res['id'],
$sourceLocale,
$traitsourceFields
);
}
/*
示例用法:
// 1) 获取分类为“车辆类型”的所有字典
$list = $dictService->getDictsByCategoryName('车辆类型');
// => [['dict_code' => 'SUV', 'dict_name' => '越野车'], …]
// 2) 获取某分类下某个 code 的名称
$name = $dictService->getDictName('车辆类型', 'SUV');
// => "越野车"
// 3) 获取某分类下拉选项
$options = $dictService->getDictOptions('车辆类型');
// => ['SUV' => '越野车', 'SEDAN' => '轿车', …]
// 4) 通过 code 直接拿名称(不管分类)
$name = $dictService->getNameByCode('SUV');
// => "越野车"
*/
/**
* 根据分类名获取字典列表
*/
public function getDictsByCategoryName(string $categoryName): array
{
$category = SysDictCategory::where('category_name', $categoryName)
->where('status', 1)
->first();
if (!$category) {
return [];
}
return $category->dicts()->get(['dict_code', 'dict_name'])->toArray();
}
/**
* 根据分类名 + 字典code 获取字典项名称
*/
public function getDictName(string $categoryName, string $dictCode): ?string
{
$dict = SysDict::whereHas('category', function ($q) use ($categoryName) {
$q->where('category_name', $categoryName)->where('status', 1);
})
->where('dict_code', $dictCode)
->where('status', 1)
->whereNull('deleted_at')
->first();
return $dict?->dict_name;
}
/**
* 根据分类名获取下拉键值对
* 返回格式:['code1' => '名称1', 'code2' => '名称2']
*/
public function getDictOptions(string $categoryName): array
{
$dicts = $this->getDictsByCategoryName($categoryName);
$options = [];
foreach ($dicts as $item) {
$options[$item['dict_code']] = $item['dict_name'];
}
return $options;
}
/**
* 根据字典 code 获取对应的名称(不依赖分类)
*/
public function getNameByCode(string $dictCode): ?string
{
$dict = SysDict::where('dict_code', $dictCode)
->where('status', 1)
->whereNull('deleted_at')
->first();
return $dict?->dict_name;
}
}
+18
View File
@@ -5,6 +5,8 @@ namespace app\admin\service;
use app\admin\validate\SysPortValidate;
use app\common\traits\crud\CrudServiceTrait;
use app\common\model\SysPort;
use app\common\utils\translation\service\TranslationService;
use app\queue\utils\Translat;
use support\Request;
class SysPortService
@@ -22,6 +24,8 @@ class SysPortService
*/
protected $validator = SysPortValidate::class;
public $translatFields = ['port_name','description'];
/**
* 获取验证器
*/
@@ -97,6 +101,20 @@ class SysPortService
return $data;
}
public function afterServiceStore($res)
{
$sourceLocale = locale();
$data = $res->toArray();
$traitsourceFields = Translat::extractSourceFields($data, $this->translatFields);
(new TranslationService())->queueTranslations(
'sys_port_translation',
$res['id'],
$sourceLocale,
$traitsourceFields
);
}
/**
* 添加前置处理
*/
+10
View File
@@ -116,6 +116,7 @@ class UserService
$config['where']['id'] = $p['id'];
break;
case 'edit':
case 'updateSign':
$config['where']['id'] = $request->user->uid;
break;
}
@@ -157,4 +158,13 @@ class UserService
$user = $this->logicUpdateBy($config,$data);
return $user;
}
public function serviceUpdateSign(Request $request, array $data)
{
$config = $this->buildQueryConfig($request, 'updateSign');
$user = $this->logicUpdateBy($config,$data);
return $user;
}
}
+18
View File
@@ -5,6 +5,8 @@ namespace app\admin\service;
use app\admin\validate\VehColorValidate;
use app\common\traits\crud\CrudServiceTrait;
use app\common\model\VehColor;
use app\common\utils\translation\service\TranslationService;
use app\queue\utils\Translat;
use support\Request;
class VehColorService
@@ -22,6 +24,8 @@ class VehColorService
*/
protected $validator = VehColorValidate::class;
public $translatFields = ['color_text'];
/**
* 获取验证器
*/
@@ -30,6 +34,20 @@ class VehColorService
return new $this->validator;
}
public function afterServiceStore($res)
{
$sourceLocale = locale();
$data = $res->toArray();
$traitsourceFields = Translat::extractSourceFields($data, $this->translatFields);
(new TranslationService())->queueTranslations(
'veh_color_translation',
$res['id'],
$sourceLocale,
$traitsourceFields
);
}
/**
* 添加前置处理
*/
@@ -0,0 +1,29 @@
<?php
namespace app\admin\validate;
use think\Validate;
class ApprovalFlowsValidate extends Validate
{
// 规则
protected $rule = [
];
// 提示
protected $message = [
];
// 场景
protected $scene = [
'store' => [
],
'update' => [
]
];
}
@@ -0,0 +1,29 @@
<?php
namespace app\admin\validate;
use think\Validate;
class ApprovalInstancesValidate extends Validate
{
// 规则
protected $rule = [
];
// 提示
protected $message = [
];
// 场景
protected $scene = [
'store' => [
],
'update' => [
]
];
}
@@ -0,0 +1,29 @@
<?php
namespace app\admin\validate;
use think\Validate;
class ApprovalRecordsValidate extends Validate
{
// 规则
protected $rule = [
];
// 提示
protected $message = [
];
// 场景
protected $scene = [
'store' => [
],
'update' => [
]
];
}
@@ -1,626 +0,0 @@
<?php
namespace app\common\approval\service;
use DateTime;
use support\exception\BusinessException;
use support\Db;
use app\common\approval\model\ApprovalCcs;
use app\common\approval\model\ApprovalFlows;
use app\common\approval\model\ApprovalInstances;
use app\common\approval\model\ApprovalRecords;
class ApprovalFlowService
{
// --- 公共方法 ---
/**
* 启动审批流程
*/
public function start(string $flowType, string $bizType, string $bizId, array $formData, int $initiatorId): array
{
// 1. 获取流程定义
$flow = Db::table('approval_flows')
->where('type', $flowType)
->where('is_active', 1)
->first();
if (!$flow) {
throw new BusinessException("审批流程未找到或已停用: {$flowType}");
}
$steps = json_decode($flow->steps, true);
if (!is_array($steps) || empty($steps)) {
throw new BusinessException("流程步骤配置错误");
}
// 2. 查找第一个有效步骤(考虑 condition)
$firstStep = $this->getNextValidStep($steps, $formData);
if (!$firstStep) {
throw new BusinessException("未找到可执行的审批步骤");
}
// 3. 创建审批实例
$instanceId = Db::table('approval_instances')->insertGetId([
'flow_type' => $flowType,
'biz_type' => $bizType,
'biz_id' => $bizId,
'form_data' => json_encode($formData, JSON_UNESCAPED_UNICODE),
'initiator_id' => $initiatorId,
'status' => 'processing',
'current_step' => 0,
'created_at' => date('Y-m-d H:i:s'),
'updated_at' => date('Y-m-d H:i:s'),
]);
// 4. 创建审批记录(待办)
$approverIds = $this->resolveApprovers($firstStep, (object)['initiator_id' => $initiatorId]);
foreach ($approverIds as $approverId) {
Db::table('approval_records')->insert([
'instance_id' => $instanceId,
'step_name' => $firstStep['name'],
'approver_id' => $approverId,
'status' => 'pending',
'created_at' => date('Y-m-d H:i:s'),
]);
}
// 5. 通知审批人
$this->sendNotification($approverIds, (object)['id' => $instanceId, 'flow_type' => $flowType], 'pending', false);
// 6. 通知抄送人(当前步骤)
$this->notifyCCs($firstStep, (object)['id' => $instanceId, 'flow_type' => $flowType, 'form_data' => json_encode($formData)], 'pending');
return ['instance_id' => $instanceId];
}
/**
* 执行审批
*/
public function approve(int $instanceId, int $approverId, string $action, string $comment = '')
{
$instance = Db::table('approval_instances')->where('id', $instanceId)->first();
if (!$instance) {
throw new BusinessException("审批实例未找到");
}
$steps = json_decode($instance->steps, true) ?: json_decode(Db::table('approval_flows')->where('type', $instance->flow_type)->value('steps'), true);
$currentStep = $steps[$instance->current_step] ?? null;
if (!$currentStep) {
throw new BusinessException("当前步骤配置错误");
}
// 检查用户是否有权限审批
if (!$this->canUserApproveStep($currentStep, $approverId, $instance)) {
throw new BusinessException("您无权审批此步骤");
}
// 更新审批记录
Db::table('approval_records')
->where('instance_id', $instanceId)
->where('approver_id', $approverId)
->where('status', 'pending')
->update([
'status' => $action,
'comment' => $comment,
'approved_at' => date('Y-m-d H:i:s'),
'updated_at' => date('Y-m-d H:i:s'),
]);
// 检查当前步骤是否完成(根据 type: any, all, sequential)
$stepCompleted = $this->isStepCompleted($instanceId, $instance->current_step, $currentStep['type']);
if (!$stepCompleted) {
// 步骤未完成,不进入下一步
return ['status' => 'step_pending'];
}
// 步骤已完成,进入下一步或结束
$nextStep = $this->getNextValidStep($steps, json_decode($instance->form_data, true), $instance->current_step + 1);
if ($nextStep === null) {
// 流程结束
Db::table('approval_instances')->where('id', $instanceId)->update([
'status' => $action === 'approve' ? 'approved' : 'rejected',
'updated_at' => date('Y-m-d H:i:s'),
]);
// 通知发起人结果
$this->sendNotification([$instance->initiator_id], $instance, $action, false);
return ['status' => 'completed', 'result' => $action];
}
// 进入下一步
$nextStepIndex = array_search($nextStep, $steps);
Db::table('approval_instances')->where('id', $instanceId)->update([
'current_step' => $nextStepIndex,
'updated_at' => date('Y-m-d H:i:s'),
]);
// 创建下一步的审批记录
$nextApprovers = $this->resolveApprovers($nextStep, $instance);
foreach ($nextApprovers as $approverId) {
Db::table('approval_records')->insert([
'instance_id' => $instanceId,
'step_name' => $nextStep['name'],
'approver_id' => $approverId,
'status' => 'pending',
'created_at' => date('Y-m-d H:i:s'),
]);
}
// 通知下一步审批人
$this->sendNotification($nextApprovers, $instance, 'pending', false);
// 通知下一步抄送人
$this->notifyCCs($nextStep, $instance, 'pending');
return ['status' => 'next_step', 'step_name' => $nextStep['name']];
}
/**
* 解析审批人
*/
private function resolveApprovers(array $step, object $instance): array
{
$resolved = [];
$formData = json_decode($instance->form_data ?? '{}', true);
foreach ($step['approvers'] as $approverConfig) {
$approverIds = [];
switch ($approverConfig['type']) {
case 'role':
$approverIds = $this->getUserIdsByRole($approverConfig['value']);
break;
case 'user':
$approverIds = [(int)$approverConfig['value']];
break;
case 'initiator_leader':
$leaderId = $this->getLeaderIdByUserId($instance->initiator_id);
if ($leaderId) $approverIds = [$leaderId];
break;
case 'field_user':
if (isset($formData[$approverConfig['value']])) {
$approverIds = [(int)$formData[$approverConfig['value']]];
}
break;
case 'dept_head':
$approverIds = $this->getDeptHeadId($approverConfig['value']);
if ($approverIds) $approverIds = [$approverIds];
break;
default:
throw new BusinessException("不支持的审批人类型: {$approverConfig['type']}");
}
$resolved = array_merge($resolved, $approverIds);
}
return array_unique(array_filter($resolved));
}
/**
* 通知抄送人
*/
private function notifyCCs(array $step, object $instance, string $action = 'pending')
{
$ccConfig = $step['cc'] ?? [];
$ccUserIds = [];
$formData = json_decode($instance->form_data ?? '{}', true);
foreach ($ccConfig as $cc) {
switch ($cc['type']) {
case 'role':
$ccUserIds = array_merge($ccUserIds, $this->getUserIdsByRole($cc['value']));
break;
case 'user':
$ccUserIds[] = (int)$cc['value'];
break;
case 'initiator_leader':
$leaderId = $this->getLeaderIdByUserId($instance->initiator_id);
if ($leaderId) $ccUserIds[] = $leaderId;
break;
case 'field_user':
if (isset($formData[$cc['value']])) {
$ccUserIds[] = (int)$formData[$cc['value']];
}
break;
case 'dept_head':
$deptHeadId = $this->getDeptHeadId($cc['value']);
if ($deptHeadId) $ccUserIds[] = $deptHeadId;
break;
default:
break;
}
}
$ccUserIds = array_unique(array_filter($ccUserIds));
if (!empty($ccUserIds)) {
$this->sendNotification($ccUserIds, $instance, $action, true);
}
}
/**
* 评估条件
*/
private function evaluateCondition(?string $condition, array $formData): bool
{
if (!$condition) return true;
$pattern = '/^(\w+)\s*(==|!=|>|<|>=|<=)\s*(.+)$/';
if (!preg_match($pattern, $condition, $matches)) {
throw new BusinessException("条件格式错误: {$condition}");
}
$field = $matches[1];
$op = $matches[2];
$value = trim($matches[3], "\"'");
$actualValue = $formData[$field] ?? null;
if (is_numeric($value) && is_numeric($actualValue)) {
$value = (float)$value;
$actualValue = (float)$actualValue;
}
switch ($op) {
case '==': return $actualValue == $value;
case '!=': return $actualValue != $value;
case '>': return $actualValue > $value;
case '<': $actualValue < $value;
case '>=': return $actualValue >= $value;
case '<=': return $actualValue <= $value;
default: throw new BusinessException("不支持的操作符: {$op}");
}
}
/**
* 获取下一个有效步骤(考虑 condition)
*/
private function getNextValidStep(array $steps, array $formData, int $startIndex = 0): ?array
{
for ($i = $startIndex; $i < count($steps); $i++) {
$step = $steps[$i];
if (!isset($step['condition']) || $this->evaluateCondition($step['condition'], $formData)) {
return $step;
}
}
return null;
}
/**
* 检查用户是否有权限审批
*/
private function canUserApproveStep(array $step, int $userId, object $instance): bool
{
$approvers = $this->resolveApprovers($step, $instance);
return in_array($userId, $approvers);
}
/**
* 检查当前步骤是否完成
*/
private function isStepCompleted(int $instanceId, int $stepIndex, string $type): bool
{
$pendingCount = Db::table('approval_records')
->where('instance_id', $instanceId)
->where('status', 'pending')
->count();
if ($type === 'any') {
return $pendingCount === 0; // 任一通过即可
}
if ($type === 'all' || $type === 'sequential') {
return $pendingCount === 0; // 所有人都必须审批
}
return false;
}
/**
* 发送通知(你可以替换为你的通知系统)
*/
private function sendNotification(array $userIds, object $instance, string $action, bool $isCC)
{
$actionText = [
'pending' => '待审批',
'approve' => '已通过',
'reject' => '已拒绝'
][$action] ?? $action;
$prefix = $isCC ? '[抄送]' : '';
$message = "{$prefix}您有一条审批 {$actionText}:流程ID {$instance->id},类型 {$instance->flow_type}";
// TODO: 调用你的消息系统,如邮件、站内信、企业微信等
// 示例:MessageService::send($userIds, $message);
echo "通知用户 " . implode(',', $userIds) . ":$message\n";
}
/**
* 获取用户待审批列表
* @param int $userId 用户ID
* @return array
*/
public function getPendingApprovals(int $userId): array
{
// 简化查询:找到用户需要审批的实例ID
// 更复杂的逻辑(如并行会签)需要更精细的查询
$instanceIds = Db::table('approval_records')
->where('approver_id', $userId)
->pluck('instance_id')
->unique()
->toArray();
if (empty($instanceIds)) {
return [];
}
return Db::table('approval_instances')
->whereIn('id', $instanceIds)
->where('status', 'processing')
->get()
->toArray();
}
/**
* 获取审批历史记录
* @param int $instanceId
* @return array
*/
public function getApprovalHistory(int $instanceId): array
{
return Db::table('approval_records')
->where('instance_id', $instanceId)
->orderBy('step_index')
->orderBy('approved_at')
->get()
->toArray();
}
/**
* 获取审批流程图数据 (简化版)
* @param int $instanceId
* @return array
*/
public function getFlowDiagramData(int $instanceId): array
{
$instance = Db::table('approval_instances')->find($instanceId);
if (!$instance) return [];
$flow = Db::table('approval_flows')->where('type', $instance->flow_type)->first();
if (!$flow) return [];
$steps = json_decode($flow->steps, true);
$records = Db::table('approval_records')
->where('instance_id', $instanceId)
->orderBy('step_index')
->orderBy('approved_at')
->get()
->groupBy('step_index');
$diagramData = [];
foreach ($steps as $index => $step) {
$status = 'pending';
$actions = $records->get($index, collect())->toArray();
if (!empty($actions)) {
$hasReject = collect($actions)->contains('action', 'reject');
if ($hasReject) {
$status = 'rejected';
} else {
if ($step['type'] === 'parallel') {
// 简化处理
$status = 'approved';
} else {
$status = 'approved';
}
}
} elseif ($index < $instance->current_step) {
$status = 'approved';
} elseif ($index == $instance->current_step) {
$status = 'processing';
}
$diagramData[] = [
'index' => $index,
'name' => $step['name'],
'type' => $step['type'],
'status' => $status,
'actions' => $actions
];
}
return $diagramData;
}
/**
* 处理超时的审批实例 (定时任务调用)
*/
public function handleTimeouts()
{
$now = date('Y-m-d H:i:s');
$expiredInstances = Db::table('approval_instances')
->where('status', 'processing')
->where('deadline_at', '<=', $now)
->get();
foreach ($expiredInstances as $instance) {
$flow = Db::table('approval_flows')->where('type', $instance->flow_type)->first();
if ($flow && $flow->timeout_action) {
$updateData = [
'finished_at' => $now,
'updated_at' => $now
];
if ($flow->timeout_action === 'auto_reject') {
$updateData['status'] = 'rejected';
Db::table('approval_instances')->where('id', $instance->id)->update($updateData);
$this->notifyInitiator($instance, 'rejected_timeout');
} elseif ($flow->timeout_action === 'auto_approve') {
$updateData['status'] = 'approved';
Db::table('approval_instances')->where('id', $instance->id)->update($updateData);
$this->notifyInitiator($instance, 'approved_timeout');
}
}
}
}
// --- 辅助方法 ---
// private function canUserApproveStep(array $step, int $userId, object $instance): bool
// {
// $approvers = $this->resolveApprovers($step, $instance);
// return in_array($userId, $approvers);
// }
// private function resolveApprovers(array $step, object $instance): array
// {
// $resolved = [];
// foreach ($step['approvers'] as $approver) {
// if ($approver['type'] === 'role') {
// // 示例:根据角色查找用户ID,你需要替换为实际逻辑
// if ($approver['value'] === 'admin') {
// $resolved[] = 1; // 假设 admin 角色的用户ID是1
// } elseif ($approver['value'] === 'manager') {
// $resolved[] = 2; $resolved[] = 3; // 假设有多个manager
// }
// } elseif ($approver['type'] === 'user') {
// $resolved[] = (int)$approver['value'];
// } elseif ($approver['type'] === 'initiator_leader') {
// // 示例:获取发起人的领导
// $resolved[] = 4; // 假设领导ID是4
// }
// }
// return array_unique($resolved);
// }
// private function resolveApprovers(array $step, object $instance): array
// {
// $resolved = [];
// foreach ($step['approvers'] as $approverConfig) {
// $approverIds = [];
// switch ($approverConfig['type']) {
// // 1. 按角色查找
// case 'role':
// $roleCode = $approverConfig['value'];
// // 所有拥有该角色的用户ID
// $approverIds = $this->getUserIdsByRole($roleCode);
// break;
// // 2. 按具体用户ID
// case 'user':
// $approverIds = [(int)$approverConfig['value']];
// break;
// // 3. 按发起人的直属领导 (业务一般是组长)
// case 'initiator_leader':
// // 用户表中 report_to 字段
// $approverIds = $this->getLeaderIdByUserId($instance->initiator_id);
// if ($approverIds) {
// $approverIds = [$approverIds];
// } else {
// $approverIds = []; // 没有领导,跳过
// }
// break;
// // 4. 按业务字段关联的用户 (高级功能)
// case 'field_user':
// $fieldName = $approverConfig['value']; // 如 'project_manager_id'
// $formData = json_decode($instance->form_data, true);
// if (isset($formData[$fieldName])) {
// $approverIds = [(int)$formData[$fieldName]];
// } else {
// $approverIds = [];
// }
// break;
// // 5. 按组织架构层级 (如发起人所在部门的负责人)
// case 'dept_head':
// $deptId = $approverConfig['value']; // 也可以从 instance 或 formData 中获取
// $approverIds = $this->getDeptHeadId($deptId);
// if ($approverIds) {
// $approverIds = [$approverIds];
// } else {
// $approverIds = [];
// }
// break;
// default:
// throw new BusinessException("不支持的审批人类型: {$approverConfig['type']}");
// }
// $resolved = array_merge($resolved, $approverIds);
// }
// return array_unique($resolved);
// }
// 角色层
private function getUserIdsByRole(string $roleCode): array
{
// user_roles 表和 roles 表
return Db::table('user_roles as ur')
->join('roles as r', 'ur.role_id', '=', 'r.id')
->where('r.code', $roleCode) // 假设 roles 表有 code 字段
->pluck('ur.user_id')
->toArray();
}
// 领导层
private function getLeaderIdByUserId(int $userId): ?int
{
// users 表 report_to 字段
return Db::table('user')->where('id', $userId)->value('report_to');
}
// 组织架构层
private function getDeptHeadId(int $deptId): ?int
{
// departments 表 head_user_id 字段(部门逻辑暂时不用,根据后续业务实际情况开发)
return Db::table('departments')->where('id', $deptId)->value('head_user_id');
}
private function calculateNextStepIndex(int $instanceId, int $currentStepIndex, array $currentStep): int
{
// 对于 sequential 和 any,单次 approve 即进入下一步
if ($currentStep['type'] === 'sequential' || $currentStep['type'] === 'any') {
return $currentStepIndex + 1;
} elseif ($currentStep['type'] === 'parallel') {
// 对于并行会签,需要检查是否所有审批人都已 approve
// 这里简化处理,实际需要精确计算
// 假设只要有一次 approve 就算完成(不准确,仅为示例)
// 正确做法是查询所有 approvers 是否都 approve 了
return $currentStepIndex + 1;
}
return $currentStepIndex + 1;
}
private function notifyNextApprovers(int $instanceId)
{
// 实现你的通知逻辑,如发送站内信、邮件、企业微信等
// 可以查询 instance_id 对应的当前步骤审批人
echo "通知下一个审批人 (Instance ID: {$instanceId})\n";
}
private function notifyInitiator(object $instance, string $result)
{
// 通知发起人审批结果
echo "通知发起人 {$instance->initiator_id} 审批结果: {$result}\n";
}
private function notifyAllApprovers(object $instance, string $status)
{
// 通知所有相关审批人(如撤回时)
echo "通知所有审批人,实例 {$instance->id} 状态变为: {$status}\n";
}
private function notifyCc(int $instanceId)
{
// 通知抄送人
echo "通知抄送人 (Instance ID: {$instanceId})\n";
}
}
@@ -1,431 +0,0 @@
<?php
namespace app\common\approval\service;
use DateTime;
use support\exception\BusinessException;
use support\Db;
use app\common\approval\model\ApprovalCcs;
use app\common\approval\model\ApprovalFlows;
use app\common\approval\model\ApprovalInstances;
use app\common\approval\model\ApprovalRecords;
class ApprovalFlowService_bak
{
// --- 公共方法 ---
/**
* 启动一个审批实例
* @param string $flowType 流程类型标识 (approval_flows.type)
* @param string $bizType 业务类型标识 (如 'leave_application')
* @param string $bizId 业务数据ID (如 leave_applications 表的 ID)
* @param array $formData 提交的表单数据快照
* @param int $initiatorId 发起人ID
* @return array ['success' => true, 'instance_id' => ...] 或抛出异常
*/
public function start(string $flowType, string $bizType, string $bizId, array $formData, int $initiatorId): array
{
// 1. 获取流程定义
$flow = Db::table('approval_flows')
->where('type', $flowType)
->where('is_active', 1)
->first();
if (!$flow) {
throw new BusinessException("审批流程 '{$flowType}' 不存在或已停用");
}
// 2. 创建审批实例
$instanceId = Db::table('approval_instances')->insertGetId([
'flow_type' => $flowType,
'biz_id' => $bizId,
'biz_type' => $bizType,
'form_data' => json_encode($formData),
'initiator_id' => $initiatorId,
'status' => 'processing',
'current_step' => 0,
'deadline_at' => $flow->timeout_hours > 0 ?
(new DateTime())->modify("+{$flow->timeout_hours} hours")->format('Y-m-d H:i:s') : null,
'created_at' => date('Y-m-d H:i:s'),
'updated_at' => date('Y-m-d H:i:s'),
]);
// 3. 通知下一个审批人
$this->notifyNextApprovers($instanceId);
return ['success' => true, 'instance_id' => $instanceId];
}
/**
* 执行审批操作
* @param int $instanceId 审批实例ID
* @param int $approverId 审批人ID
* @param string $action 操作 (approve, reject, revoke)
* @param string $comment 审批意见
* @return array 操作结果
*/
public function approve(int $instanceId, int $approverId, string $action, string $comment = ''): array
{
// 1. 获取实例和流程
$instance = Db::table('approval_instances')->find($instanceId);
if (!$instance) {
throw new BusinessException("审批实例不存在");
}
if ($instance->status !== 'processing') {
throw new BusinessException("审批实例状态不正确");
}
$flow = Db::table('approval_flows')->where('type', $instance->flow_type)->first();
if (!$flow) {
throw new BusinessException("关联的审批流程不存在");
}
$steps = json_decode($flow->steps, true);
$currentStepIndex = $instance->current_step;
if ($currentStepIndex >= count($steps)) {
throw new BusinessException("审批流程已结束");
}
$currentStep = $steps[$currentStepIndex];
// 2. 验证权限和条件
if (!$this->canUserApproveStep($currentStep, $approverId, $instance)) {
throw new BusinessException("您无权审批此步骤");
}
// 3. 记录审批行为
Db::table('approval_records')->insert([
'instance_id' => $instanceId,
'approver_id' => $approverId,
'step_index' => $currentStepIndex,
'action' => $action,
'comment' => $comment,
'approved_at' => date('Y-m-d H:i:s'),
'created_at' => date('Y-m-d H:i:s'),
'updated_at' => date('Y-m-d H:i:s'),
]);
// 4. 处理审批结果
$updateData = ['updated_at' => date('Y-m-d H:i:s')];
if ($action === 'reject') {
$updateData['status'] = 'rejected';
$updateData['finished_at'] = date('Y-m-d H:i:s');
Db::table('approval_instances')->where('id', $instanceId)->update($updateData);
$this->notifyInitiator($instance, 'rejected');
return ['result' => 'rejected'];
}
if ($action === 'revoke') {
if ($instance->initiator_id !== $approverId || $instance->status !== 'processing') {
throw new BusinessException("无权撤回或状态不允许");
}
$updateData['status'] = 'revoked';
$updateData['finished_at'] = date('Y-m-d H:i:s');
Db::table('approval_instances')->where('id', $instanceId)->update($updateData);
$this->notifyAllApprovers($instance, 'revoked');
return ['result' => 'revoked'];
}
// 5. 处理 approve 逻辑
$nextStepIndex = $this->calculateNextStepIndex($instanceId, $currentStepIndex, $currentStep);
$updateData['current_step'] = $nextStepIndex;
if ($nextStepIndex >= count($steps)) {
$updateData['status'] = 'approved';
$updateData['finished_at'] = date('Y-m-d H:i:s');
Db::table('approval_instances')->where('id', $instanceId)->update($updateData);
$this->notifyInitiator($instance, 'approved');
$this->notifyCc($instanceId);
} else {
Db::table('approval_instances')->where('id', $instanceId)->update($updateData);
$this->notifyNextApprovers($instanceId);
}
return ['result' => 'approved', 'next_step' => $nextStepIndex];
}
/**
* 获取用户待审批列表
* @param int $userId 用户ID
* @return array
*/
public function getPendingApprovals(int $userId): array
{
// 简化查询:找到用户需要审批的实例ID
// 更复杂的逻辑(如并行会签)需要更精细的查询
$instanceIds = Db::table('approval_records')
->where('approver_id', $userId)
->pluck('instance_id')
->unique()
->toArray();
if (empty($instanceIds)) {
return [];
}
return Db::table('approval_instances')
->whereIn('id', $instanceIds)
->where('status', 'processing')
->get()
->toArray();
}
/**
* 获取审批历史记录
* @param int $instanceId
* @return array
*/
public function getApprovalHistory(int $instanceId): array
{
return Db::table('approval_records')
->where('instance_id', $instanceId)
->orderBy('step_index')
->orderBy('approved_at')
->get()
->toArray();
}
/**
* 获取审批流程图数据 (简化版)
* @param int $instanceId
* @return array
*/
public function getFlowDiagramData(int $instanceId): array
{
$instance = Db::table('approval_instances')->find($instanceId);
if (!$instance) return [];
$flow = Db::table('approval_flows')->where('type', $instance->flow_type)->first();
if (!$flow) return [];
$steps = json_decode($flow->steps, true);
$records = Db::table('approval_records')
->where('instance_id', $instanceId)
->orderBy('step_index')
->orderBy('approved_at')
->get()
->groupBy('step_index');
$diagramData = [];
foreach ($steps as $index => $step) {
$status = 'pending';
$actions = $records->get($index, collect())->toArray();
if (!empty($actions)) {
$hasReject = collect($actions)->contains('action', 'reject');
if ($hasReject) {
$status = 'rejected';
} else {
if ($step['type'] === 'parallel') {
// 简化处理
$status = 'approved';
} else {
$status = 'approved';
}
}
} elseif ($index < $instance->current_step) {
$status = 'approved';
} elseif ($index == $instance->current_step) {
$status = 'processing';
}
$diagramData[] = [
'index' => $index,
'name' => $step['name'],
'type' => $step['type'],
'status' => $status,
'actions' => $actions
];
}
return $diagramData;
}
/**
* 处理超时的审批实例 (定时任务调用)
*/
public function handleTimeouts()
{
$now = date('Y-m-d H:i:s');
$expiredInstances = Db::table('approval_instances')
->where('status', 'processing')
->where('deadline_at', '<=', $now)
->get();
foreach ($expiredInstances as $instance) {
$flow = Db::table('approval_flows')->where('type', $instance->flow_type)->first();
if ($flow && $flow->timeout_action) {
$updateData = [
'finished_at' => $now,
'updated_at' => $now
];
if ($flow->timeout_action === 'auto_reject') {
$updateData['status'] = 'rejected';
Db::table('approval_instances')->where('id', $instance->id)->update($updateData);
$this->notifyInitiator($instance, 'rejected_timeout');
} elseif ($flow->timeout_action === 'auto_approve') {
$updateData['status'] = 'approved';
Db::table('approval_instances')->where('id', $instance->id)->update($updateData);
$this->notifyInitiator($instance, 'approved_timeout');
}
}
}
}
// --- 辅助方法 ---
private function canUserApproveStep(array $step, int $userId, object $instance): bool
{
$approvers = $this->resolveApprovers($step, $instance);
return in_array($userId, $approvers);
}
// private function resolveApprovers(array $step, object $instance): array
// {
// $resolved = [];
// foreach ($step['approvers'] as $approver) {
// if ($approver['type'] === 'role') {
// // 示例:根据角色查找用户ID,你需要替换为实际逻辑
// if ($approver['value'] === 'admin') {
// $resolved[] = 1; // 假设 admin 角色的用户ID是1
// } elseif ($approver['value'] === 'manager') {
// $resolved[] = 2; $resolved[] = 3; // 假设有多个manager
// }
// } elseif ($approver['type'] === 'user') {
// $resolved[] = (int)$approver['value'];
// } elseif ($approver['type'] === 'initiator_leader') {
// // 示例:获取发起人的领导
// $resolved[] = 4; // 假设领导ID是4
// }
// }
// return array_unique($resolved);
// }
private function resolveApprovers(array $step, object $instance): array
{
$resolved = [];
foreach ($step['approvers'] as $approverConfig) {
$approverIds = [];
switch ($approverConfig['type']) {
// 1. 按角色查找
case 'role':
$roleCode = $approverConfig['value'];
// 所有拥有该角色的用户ID
$approverIds = $this->getUserIdsByRole($roleCode);
break;
// 2. 按具体用户ID
case 'user':
$approverIds = [(int)$approverConfig['value']];
break;
// 3. 按发起人的直属领导 (业务一般是组长)
case 'initiator_leader':
// 用户表中 report_to 字段
$approverIds = $this->getLeaderIdByUserId($instance->initiator_id);
if ($approverIds) {
$approverIds = [$approverIds];
} else {
$approverIds = []; // 没有领导,跳过
}
break;
// 4. 按业务字段关联的用户 (高级功能)
case 'field_user':
$fieldName = $approverConfig['value']; // 如 'project_manager_id'
$formData = json_decode($instance->form_data, true);
if (isset($formData[$fieldName])) {
$approverIds = [(int)$formData[$fieldName]];
} else {
$approverIds = [];
}
break;
// 5. 按组织架构层级 (如发起人所在部门的负责人)
case 'dept_head':
$deptId = $approverConfig['value']; // 也可以从 instance 或 formData 中获取
$approverIds = $this->getDeptHeadId($deptId);
if ($approverIds) {
$approverIds = [$approverIds];
} else {
$approverIds = [];
}
break;
default:
throw new BusinessException("不支持的审批人类型: {$approverConfig['type']}");
}
$resolved = array_merge($resolved, $approverIds);
}
return array_unique($resolved);
}
// 角色层
private function getUserIdsByRole(string $roleCode): array
{
// user_roles 表和 roles 表
return Db::table('user_roles as ur')
->join('roles as r', 'ur.role_id', '=', 'r.id')
->where('r.code', $roleCode) // 假设 roles 表有 code 字段
->pluck('ur.user_id')
->toArray();
}
// 领导层
private function getLeaderIdByUserId(int $userId): ?int
{
// users 表 report_to 字段
return Db::table('user')->where('id', $userId)->value('report_to');
}
// 组织架构层
private function getDeptHeadId(int $deptId): ?int
{
// departments 表 head_user_id 字段(部门逻辑暂时不用,根据后续业务实际情况开发)
return Db::table('departments')->where('id', $deptId)->value('head_user_id');
}
private function calculateNextStepIndex(int $instanceId, int $currentStepIndex, array $currentStep): int
{
// 对于 sequential 和 any,单次 approve 即进入下一步
if ($currentStep['type'] === 'sequential' || $currentStep['type'] === 'any') {
return $currentStepIndex + 1;
} elseif ($currentStep['type'] === 'parallel') {
// 对于并行会签,需要检查是否所有审批人都已 approve
// 这里简化处理,实际需要精确计算
// 假设只要有一次 approve 就算完成(不准确,仅为示例)
// 正确做法是查询所有 approvers 是否都 approve 了
return $currentStepIndex + 1;
}
return $currentStepIndex + 1;
}
private function notifyNextApprovers(int $instanceId)
{
// 实现你的通知逻辑,如发送站内信、邮件、企业微信等
// 可以查询 instance_id 对应的当前步骤审批人
echo "通知下一个审批人 (Instance ID: {$instanceId})\n";
}
private function notifyInitiator(object $instance, string $result)
{
// 通知发起人审批结果
echo "通知发起人 {$instance->initiator_id} 审批结果: {$result}\n";
}
private function notifyAllApprovers(object $instance, string $status)
{
// 通知所有相关审批人(如撤回时)
echo "通知所有审批人,实例 {$instance->id} 状态变为: {$status}\n";
}
private function notifyCc(int $instanceId)
{
// 通知抄送人
echo "通知抄送人 (Instance ID: {$instanceId})\n";
}
}
+3 -15
View File
@@ -43,21 +43,9 @@ class StockOrderItems extends Model
*/
protected $fillable = [
'id', 'order_id', 'product_code', 'sku_id', 'sku_snapshot',
'model_snapshot', 'quantity', 'color_interior_coide', 'color_interior_snapshot',
'color_exterior_coide', 'color_exterior_snapshot', 'remark',
// 'unit_price',
// 'handling_fee',
// 'fob_transport_fee',
// 'port_misc_fee',
// 'retrofit_fee',
// 'total_price',
'created_at', 'updated_at',
// 'unit_price_value_id',
// 'handling_fee_value_id',
// 'fob_transport_fee_value_id',
// 'port_misc_fee_value_id',
// 'retrofit_fee_value_id',
// 'total_price_value_id'
'model_snapshot', 'quantity', 'color_interior_code', 'color_interior_snapshot',
'color_exterior_code', 'color_exterior_snapshot', 'remark', 'vin', 'created_at', 'updated_at',
'mileage', 'unique_id'
];
/**
+15 -1
View File
@@ -3,6 +3,7 @@
namespace app\common\model;
use support\Model;
use app\common\traits\HasMoneyJournal;
class StockOrders extends Model
{
@@ -49,7 +50,7 @@ class StockOrders extends Model
'trade_term_id', 'trade_term_snapshot', 'clearance_port_id', 'clearance_port_snapshot',
'latest_arrival_date', 'contract_exchange_rate', 'settlement_currency', 'status',
'remark', 'created_at', 'updated_at', 'deleted_at',
'object_id', 'contract', 'is_use', 'is_final', 'approval_status'
'object_id', 'contract', 'is_use', 'is_final', 'approval_status', 'file_url', 'pick_up_date', 'collect_all_date', 'profit_total'
];
/**
@@ -58,6 +59,13 @@ class StockOrders extends Model
protected $guarded = [];
use HasMoneyJournal;
// 实现抽象方法
protected static function moneyObjectScene(): string
{
return 'stock_orders';
}
/**
* 关联订单项
*/
@@ -81,4 +89,10 @@ class StockOrders extends Model
{
return $this->hasMany(VehMoneyValue::class, 'object_id', 'id')->where('object_scene', 'stock_orders');
}
public function salesperson()
{
return $this->hasOne(User::class, 'id', 'salesperson_id');
}
}
@@ -0,0 +1,49 @@
<?php
namespace app\common\model;
use support\Model;
class SysCurrencyTranslation 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_currency_translation';
/**
* 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', 'currency_id', 'locale', 'currency_name', 'created_at', 'updated_at'];
/**
* 不允许自动填充模型字段
*/
protected $guarded = [];
}
+6
View File
@@ -56,4 +56,10 @@ class SysDict extends Model
{
return $this->hasMany(SysDictItem::class, 'dict_id', 'id');
}
public function category()
{
return $this->belongsTo(SysDictCategory::class, 'category_id', 'id');
}
}
+9
View File
@@ -46,4 +46,13 @@ class SysDictCategory extends Model
* 不允许自动填充模型字段
*/
protected $guarded = [];
public function dicts()
{
return $this->hasMany(SysDict::class, 'category_id', 'id')
->where('status', 1)
->whereNull('deleted_at')
->orderBy('id', 'asc');
}
}
@@ -0,0 +1,49 @@
<?php
namespace app\common\model;
use support\Model;
class SysDictItemTranslation 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_dict_translation';
/**
* 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', 'dict_id', 'remark', 'locale', 'dict_name', 'description', 'created_at', 'updated_at'];
/**
* 不允许自动填充模型字段
*/
protected $guarded = [];
}
+49
View File
@@ -0,0 +1,49 @@
<?php
namespace app\common\model;
use support\Model;
class SysDictTranslation 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_dict_translation';
/**
* 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', 'dict_id', 'locale', 'dict_name', 'description', 'created_at', 'updated_at'];
/**
* 不允许自动填充模型字段
*/
protected $guarded = [];
}
+49
View File
@@ -0,0 +1,49 @@
<?php
namespace app\common\model;
use support\Model;
class SysPortTranslation 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_port_translation';
/**
* 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', 'port_id', 'locale', 'port_name', 'description', 'created_at', 'updated_at'];
/**
* 不允许自动填充模型字段
*/
protected $guarded = [];
}
+49
View File
@@ -0,0 +1,49 @@
<?php
namespace app\common\model;
use support\Model;
class VehColorTranslation extends Model
{
/**
* The connection name for the model.
*
* @var string|null
*/
protected $connection = 'mysql';
/**
* The table associated with the model.
*
* @var string
*/
protected $table = 'veh_color_translation';
/**
* 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', 'color_id', 'locale', 'color_text', 'created_at', 'updated_at'];
/**
* 不允许自动填充模型字段
*/
protected $guarded = [];
}
+650 -53
View File
@@ -8,10 +8,13 @@ use app\common\model\StockOrderItems;
use app\common\validate\StockOrdersValidate;
use app\common\traits\crud\CrudServiceTrait;
use app\common\model\StockOrders;
use app\common\model\SysCurrency;
use support\Request;
use app\common\model\User;
use support\exception\BusinessException;
use support\Db;
use support\Model;
use app\admin\model\ApprovalInstances;
use app\admin\model\ApprovalRecords;
class StockOrdersService
{
@@ -45,8 +48,31 @@ class StockOrdersService
$config['model'] = $this->modelClass;
switch ($scene) {
case 'index':
// ====== ====== 筛选 ====== ======
// 业务id
if (isset($p['salesperson_id']) && $p['salesperson_id']) {
$config['where']['salesperson_id'] = $p['salesperson_id'];
}
// 初、终版
$config['where']['is_final'] = $p['is_final'] ?? 0;
// 关键字
if (isset($p['keyword']) && $p['keyword']) {
$k = '%' . $p['keyword'] . '%';
foreach (['pi_no', 'order_no', 'customer_snapshot'] as $col) {
$config['orWhere'][] = [$col, 'like', $k];
}
}
$config['order']['id'] = 'desc';
$config['where']['salesperson_id'] = $request->user->uid;
break;
case 'print':
case 'approval':
$config['where']['id'] = $p['id'];
break;
case 'detail':
case 'show':
$config['where']['id'] = $p['id'];
@@ -55,6 +81,7 @@ class StockOrdersService
$config['with'][] = 'items.moneyValues';
$config['with'][] = 'accounts';
$config['with'][] = 'moneyValues';
$config['with'][] = 'salesperson';
break;
case 'store':
break;
@@ -69,52 +96,6 @@ class StockOrdersService
return $config;
}
public function serviceStore(Request $request, array $data = [])
{
// 关联表较多,增加事务处理
return Db::transaction(function () use ($request, $data) {
$data = $this->preProcess('Store', $data, $request);
$dl = $this->getDataLimitFromRequest($request);
$created = $this->logicCreate($data, $dl); // 表1
// 车辆
foreach ($data['veh_list'] as $veh) {
$veh['order_id'] = $created->id;
$item = StockOrderItems::create($veh); // 表2
// 金额 // 表3
$item->addMoney('official_price', $veh['official_price']['currency'], $veh['official_price']['amount'], $veh['official_price']['rate'], '官方指导价');
$item->addMoney('discount_price', $veh['discount_price']['currency'], $veh['discount_price']['amount'], $veh['discount_price']['rate'], '优惠价');
$item->addMoney('unit_price', $veh['unit_price']['currency'], $veh['unit_price']['amount'], $veh['unit_price']['rate'], '单价');
$item->addMoney('total_price', $veh['total_price']['currency'], $veh['total_price']['amount'], $veh['total_price']['rate'], '总价');
$item->addMoney('handling_fee', $veh['handling_fee']['currency'], $veh['handling_fee']['amount'], $veh['handling_fee']['rate'], '手续费');
$item->addMoney('fob_transport_fee', $veh['fob_transport_fee']['currency'], $veh['fob_transport_fee']['amount'], $veh['fob_transport_fee']['rate'], 'FOB运输费');
$item->addMoney('port_misc_fee', $veh['port_misc_fee']['currency'], $veh['port_misc_fee']['amount'], $veh['port_misc_fee']['rate'], '港杂');
$item->addMoney('retrofit_fee', $veh['retrofit_fee']['currency'], $veh['retrofit_fee']['amount'], $veh['retrofit_fee']['rate'], '改装费');
// 改装
if (count($veh['modify_list']) > 0) {
foreach ($veh['modify_list'] as $modify) {
$modify['order_item_id'] = $item->id;
StockOrderItemOptions::create($modify); // 表4
}
}
}
// 账户
if (count($data['account_list']) > 0) {
foreach ($data['account_list'] as $account) {
$account['order_id'] = $created->id;
$account['remit_date'] = strtotime($account['remit_date']);
StockOrderAccounts::create($account); // 表5
}
}
return $this->postProcess('Store', $created);
});
}
public function beforeServiceStore($data, Request $request)
{
$userId = $request->user->uid;
@@ -125,21 +106,637 @@ class StockOrdersService
$data['latest_arrival_date'] = strtotime($data['latest_arrival_date']);
$data['salesperson_id'] = $userId;
$data['salesperson_snapshot'] = User::where('id', $userId)->value('name') ?? '';
$data['approval_status'] = $data['approval_status'] == 1 ? 1 : 0;
// if ($data['is_final'] == 1) {
$data['pick_up_date'] = $data['pick_up_date'] ? strtotime($data['pick_up_date']) : 0;
$data['collect_all_date'] = $data['collect_all_date'] ? strtotime($data['collect_all_date']) : 0;
// }
return $data;
}
// protected function afterServiceStore($res)
// {
protected function afterServiceStore($res)
{
// 处理原单
if ($res->is_final == 1 && $res->object_id > 0) {
$order = StockOrders::find($res->object_id);
$order->is_use = 1;
$order->object_id = $res->id;
if (!$order->save()) {
flg('备货单【'.$order->order_no.'】创建终版成功,但更新原单失败');
}
}
// 启动审批
if ($res->approval_status == 1) {
$this->startApproval($res);
}
return $res;
}
/**
* 启动审批
*/
public function startApproval($item)
{
// 启动审批
$appro = new \app\common\utils\approval\service\ApprovalFlowService();
$appro->start('stock_order_flow', 'stock_orders', $item->id, $item->toArray(), $item->salesperson_id);
// 异步生成pdf文件
\Webman\RedisQueue\Redis::send('stock-order-pdf', ['id'=>$item->id]);
}
/**
* 创建PDF文件
*/
public function createdPdfFile($id)
{
$config = [];
$config['model'] = $this->modelClass;
$config['where']['id'] = $id;
$config['with'][] = 'items';
$config['with'][] = 'items.options';
$config['with'][] = 'items.moneyValues';
$config['with'][] = 'accounts';
$config['with'][] = 'moneyValues';
$config['with'][] = 'salesperson';
$item = $this->logicShow($config);
$pdfData = $this->pdfData($item);
// 渲染模板为最终HTML
$tplPath = app_path('common/utils/pdf/template/stockOrder.stub');
$html = \app\common\utils\pdf\service\PdfHelper::render($tplPath, ['data' => $pdfData]);
// 生成PDF
$driver = new \app\common\utils\pdf\service\MpdfDriver();
$pdfBin = $driver->htmlToPdf($html);
// 存储
$dir = public_path('pdf').'/';
$fileName = ($pdfData['order_no'] ?? date('YmdHis')) . '.pdf';
// 本地保存
\app\common\utils\pdf\service\PdfStorage::saveBinary(
$pdfBin,
$dir,
$fileName
);
// 上传云端
$param = [
'path' => $dir . $fileName,
'scene' => 'stock_orders',
'category' => 'pdf',
'original_name' => $fileName
];
$up = new \app\common\service\UploadService();
$res = $up->uploadLocalFile($param);
@unlink($dir . $fileName);
// 更新文件地址
$item->file_url = $res['file_name'];
$item->save();
return $res;
}
public function pdfData($item)
{
$vehList = [];
$index = 0;
$totalProfit = 0; // 总利润
// $priceFields = [
// 'official_price', // '官方指导价',
// 'discount_price', // '优惠',
// 'listing_price', // '二手挂牌价价',
// 'reduction_price', // '优惠',
// 'handling_fee', // '手续费',
// 'retrofit_fee', // '改装费',
// 'fob_transport_fee', // 'FOB运输费',
// 'port_misc_fee', // '港杂',
// 'ocean_freight', // '海运费',
// 'unit_price', // '单价',
// 'total_price', // '总价',
// // 'profit', // '利润'
// ];
// return $res;
// }
foreach ($item->items as $k => $v) {
$currencyInfo = SysCurrency::where('currency_code', $item->settlement_currency)->first(['currency_symbol', 'currency_name'])->toArray();
// 存在则数量加1
if (count($vehList) > 0 && isset($vehList[$v->unique_id])) {
$vehList[$v->unique_id]['num'] += 1;
$vehList[$v->unique_id]['total_price']['currency_info'] = $currencyInfo['currency_symbol'].number_format($vehList[$v->unique_id]['total_price']['amount']*$vehList[$v->unique_id]['num'], 2, '.', ',');
$vehList[$v->unique_id]['profit']['currency_info'] = $currencyInfo['currency_symbol'].number_format($vehList[$v->unique_id]['profit']['amount']*$vehList[$v->unique_id]['num'], 2, '.', ',');
continue;
}
$index += 1;
// 处理金额 (每次循环重新初始化)
$mv = [];
foreach ($v->moneyValues as $money) {
$mv[$money->field] = [
'currency' => $money->currency,
'amount' => $money->amount,
'rate' => $money->exchange_rate,
'currency_info' => SysCurrency::where('currency_code', $money->currency)->value('currency_symbol').number_format($money->amount, 2, '.', ',')
];
}
// 处理改装 (每次循环重新初始化)
$mfy = '';
foreach ($v->options as $my) {
$mfy .= $my->option_name_snapshot.' ';
}
// 计算单项利润并累加到总利润
$itemProfit = $this->calculateProfit($mv, $item);
$totalProfit += $itemProfit;
// 处理结果集
$vehListItem = [
'index' => $index,
'model_snapshot' => $v->model_snapshot,
'color_interior_snapshot' => $v->color_interior_snapshot,
'color_exterior_snapshot' => $v->color_exterior_snapshot,
'num' => 1,
'modify' => $mfy,
'mileage' => $v->mileage
];
$vehList[$v->unique_id] = $vehListItem + $mv;
}
$mvs = [];
foreach ($item->moneyValues as $moneys) {
$mvs[$moneys->field] = [
'currency' => $moneys->currency,
'amount' => $moneys->amount,
'rate' => $moneys->exchange_rate,
'currency_info' => SysCurrency::where('currency_code', $moneys->currency)->value('currency_symbol').number_format($moneys->amount, 2, '.', ','),
];
}
$accList = [];
foreach ($item->accounts as $k => $v) {
$accList[] = [
'index' => '收款'.($k + 1),
'remit_country_name_snapshot' => $v->remit_country_name_snapshot,
'remit_date' => date('Y-m-d', $v->remit_date),
'currency_name_snapshot' => $v->currency_name_snapshot,
'account_name_snapshot' => $v->account_name_snapshot,
'remitter_name' => $v->remitter_name,
'amount' => $v->amount,
];
}
$veh = [
'order_no' => $item->order_no,
'salesperson_snapshot' => $item->salesperson_snapshot,
'sign_img_url' => (!empty($item->salesperson->sign_img)) ?
(new \app\common\service\OssService)->generatePrivateUrl($item->salesperson->sign_img) : '',
'created_at' => date('Y-m-d', strtotime($item->created_at)),
'pi_no' => $item->pi_no,
'biz_date' => date('Y-m-d', $item->biz_date),
'clearance_country_snapshot' => $item->clearance_country_snapshot,
'customer_source_snapshot' => $item->customer_source_snapshot,
'customer_snapshot' => $item->customer_snapshot,
'customer_country_snapshot' => $item->customer_country_snapshot,
// 订单项
'veh_list' => array_values($vehList), // 重新索引数组
'contract_exchange_rate' => $item->contract_exchange_rate,
'cif_total_amount' => $mvs['cif_total_amount'],
'profit_total' => $mvs['profit_total'],
'declarant_name' => $item->declarant_name,
'eta_ship_date' => date('Y-m-d', $item->eta_ship_date),
'shipping_method_snapshot' => $item->shipping_method_snapshot,
'domestic_port_snapshot' => $item->domestic_port_snapshot,
'trade_term_snapshot' => $item->trade_term_snapshot,
'clearance_port_snapshot' => $item->clearance_port_snapshot,
'latest_arrival_date' => date('Y-m-d', $item->latest_arrival_date),
'pick_up_date' => date('Y-m-d', $item->pick_up_date),
'collect_all_date' => date('Y-m-d', $item->collect_all_date),
'is_final' => $item->is_final,
// 账户
'account_list' => $accList,
'acc_index' => count($accList)+1,
'currency_symbol' => $item->settlement_currency,
'sign_imgs' => $this->queryAppro($item->id),
'contract_url' => (!empty($item->contract)) ? (new \app\common\service\OssService)->generatePrivateUrl($item->contract) : ''
];
return $veh;
}
public function serviceShow(Request $request)
public function queryAppro($bizId)
{
$instances = ApprovalInstances::where('biz_id', $bizId)->where('biz_type', 'stock_orders')->first();
if (!$instances) {
return [];
}
$records = ApprovalRecords::where('instance_id', $instances->id)->where('action', 'approve')->orderBy('step_index', 'asc')->pluck('approver_id')->toArray();
if (count($records) == 0) {
return [];
}
$signImgs = [];
foreach ($records as $k => $v) {
$signImg = User::where('id', $v)->value('sign_img');
$signImgs[$k] = (!empty($signImg))?(new \app\common\service\OssService)->generatePrivateUrl($signImg):'';
}
return $signImgs;
}
/**
* 打印数据
*/
public function servicePrint(Request $request)
{
$config = $this->buildQueryConfig($request, 'show');
$item = $this->logicShow($config);
return $this->postProcess('Show', $item);
return $this->postProcess('Print', $item);
}
public function serviceDetail(Request $request)
{
$config = $this->buildQueryConfig($request, 'detail');
$item = $this->logicShow($config);
$pdfData = $this->pdfData($item);
return $this->postProcess('Detail', $pdfData);
}
/**
* 审批
*/
public function serviceApproval(Request $request)
{
$config = $this->buildQueryConfig($request, 'approval');
$this->logicUpdateBy($config, [
'approval_status' => 1
]);
$item = $this->logicShow($config);
$this->startApproval($item);
return $this->postProcess('Approval', $item);
}
/**
* 创建
*
* @param Request $request
* @param array $data
* @return mixed
*/
public function serviceStore(Request $request, array $data = [])
{
// 关联表较多,增加事务处理
return Db::transaction(function () use ($request, $data) {
$data = $this->preProcess('Store', $data, $request);
$dl = $this->getDataLimitFromRequest($request);
$created = $this->logicCreate($data, $dl);
// 处理车辆和账户信息
$this->processOrderItems($created, $data);
return $this->postProcess('Store', $created);
});
}
protected function onlyFillableData(Model|string $model, array $data, array $extraWhitelist = []): array
{
// 兼容传类名或实例
$m = \is_string($model) ? new $model : $model;
// 1) 优先使用 fillable
$keys = $m->getFillable();
// 2) 若没配置 fillable(常见:用 guarded),回退到完整表字段,避免误伤
if (empty($keys)) {
$keys = $m->getConnection()
->getSchemaBuilder()
->getColumnListing($m->getTable());
}
// 3) 允许额外白名单(如虚拟字段、JSON映射字段)
if ($extraWhitelist) {
$keys = array_merge($keys, $extraWhitelist);
}
return array_intersect_key($data, array_flip($keys));
}
/**
* 更新前处理
*
* @param array $data
* @param Request $request
* @return array
*/
public function beforeServiceUpdate($data, Request $request)
{
$data['biz_date'] = strtotime($data['biz_date']);
$data['eta_ship_date'] = strtotime($data['eta_ship_date']);
$data['latest_arrival_date'] = strtotime($data['latest_arrival_date']);
$data['approval_status'] = $data['approval_status'] == 1 ? 1 : 0;
$data['pick_up_date'] = $data['pick_up_date'] ? strtotime($data['pick_up_date']) : 0;
$data['collect_all_date'] = $data['collect_all_date'] ? strtotime($data['collect_all_date']) : 0;
return $data;
}
/**
* 更新后处理
*
* @param mixed $res
* @return mixed
*/
protected function afterServiceUpdate($res)
{
// 启动审批
if ($res->approval_status == 1) {
$this->startApproval($res);
}
return $res;
}
/**
* 更新
*
* @param Request $request
* @param array $data
* @return mixed
*/
public function serviceUpdate(Request $request, array $data = [])
{
// 关联表较多,增加事务处理
return Db::transaction(function () use ($request, $data) {
$data = $this->preProcess('Update', $data, $request);
$config = $this->buildQueryConfig($request, 'update');
// 过滤掉非库字段
$dbData = $this->onlyFillableData($config['model'], $data);
// 更新主表
$updated = $this->logicUpdateBy($config, $dbData);
// 获取更新后的数据
$order = $this->logicShow($config);
// 处理车辆和账户信息
$this->processOrderItems($order, $data, true);
return $this->postProcess('Update', $order);
});
}
/**
* 处理订单项(车辆和账户)
* @param StockOrders $order 订单对象
* @param array $data 数据
* @param bool $isUpdate 是否为更新操作
*/
private function processOrderItems($order, array $data, bool $isUpdate = false): void
{
// 如果是更新操作,先删除原有的关联数据
if ($isUpdate) {
$this->clearExistingOrderItems($order->id);
}
// 处理车辆项
$this->processVehicleItems($order, $data['veh_list'] ?? []);
// 处理账户项
$this->processAccountItems($order, $data['account_list'] ?? []);
}
/**
* (更新时)清除已存在的订单项
* @param int $orderId 订单ID
*/
private function clearExistingOrderItems(int $orderId): void
{
// 删除车辆改装项
StockOrderItemOptions::whereHas('item', function ($query) use ($orderId) {
$query->where('order_id', $orderId);
})->delete();
// 删除车辆项及其相关金额记录
$orderItems = StockOrderItems::where('order_id', $orderId)->get();
foreach ($orderItems as $item) {
// 删除车辆项的金额记录
Db::table('veh_money_value')
->where('object_scene', 'stock_order_items')
->where('object_id', $item->id)
->delete();
}
// 删除车辆项
StockOrderItems::where('order_id', $orderId)->delete();
// 删除订单本身的金额记录
Db::table('veh_money_value')
->where('object_scene', 'stock_orders')
->where('object_id', $orderId)
->delete();
// 删除账户项
StockOrderAccounts::where('order_id', $orderId)->delete();
}
/**
* 处理车辆项
*/
private function processVehicleItems($order, array $vehList): void
{
$cifTotalAmount = 0.00;
// $cifTotalAmountRate = 0;
// $cifTotalAmountCurrency = 'CNY';
$profitTotalAmount = 0.00;
foreach ($vehList as $veh) {
$veh['order_id'] = $order->id;
$veh['unique_id'] = $this->generateVehicleUniqueId($veh);
$item = StockOrderItems::create($veh);
// 处理金额信息
$this->processVehiclePrices($item, $veh);
// 计算利润
$profit = $this->calculateProfit($veh, $order);
$item->addMoney('profit', $order->settlement_currency, $profit, $order->contract_exchange_rate, '利润');
// 累计到岸价
$cifTotalAmount += $veh['total_price']['amount'] ?? 0;
$profitTotalAmount += $profit;
// if (isset($veh['retrofit_fee']['rate'])) {
// $cifTotalAmountRate = $veh['retrofit_fee']['rate'];
// }
// if (isset($veh['retrofit_fee']['currency'])) {
// $cifTotalAmountCurrency = $veh['retrofit_fee']['currency'];
// }
// 处理改装项
$this->processModificationItems($item->id, $veh['modify_list'] ?? []);
}
// 保存合计到岸价
$order->addMoney('cif_total_amount', $order->settlement_currency, $cifTotalAmount, $order->contract_exchange_rate, '合计到岸价');
// 利润
$order->addMoney('profit_total', $order->settlement_currency, $profitTotalAmount, $order->contract_exchange_rate, '总利润');
}
/**
* 生成车辆唯一标识
*/
private function generateVehicleUniqueId(array $veh): string
{
$uniqueIdStr = "";
$uniqueIdStr .= $veh['sku_id'] ?? '';
$uniqueIdStr .= $veh['model_snapshot'] ?? '';
$uniqueIdStr .= $veh['color_interior_code'] ?? '';
$uniqueIdStr .= $veh['color_exterior_code'] ?? '';
$uniqueIdStr .= $veh['mileage'] ?? '';
$uniqueIdStr .= json_encode($veh['modify_list'] ?? []);
return md5($uniqueIdStr);
}
/**
* 处理车辆价格信息
*/
private function processVehiclePrices($item, array $veh): void
{
// 基础费用
$priceFields = [
'unit_price' => '单价',
'total_price' => '总价',
'handling_fee' => '手续费',
'fob_transport_fee' => 'FOB运输费',
'port_misc_fee' => '港杂',
'retrofit_fee' => '改装费',
'ocean_freight' => '海运费'
];
foreach ($priceFields as $field => $description) {
if (!empty($veh[$field]['amount'])) {
$item->addMoney(
$field,
$veh[$field]['currency'],
$veh[$field]['amount'],
$veh[$field]['rate'],
$description
);
}
}
// 根据里程处理新车或二手车价格
if (!empty($veh['mileage']) && $veh['mileage'] > 0) {
// 二手车
if (!empty($veh['listing_price']['amount'])) {
$item->addMoney(
'listing_price',
$veh['listing_price']['currency'],
$veh['listing_price']['amount'],
$veh['listing_price']['rate'],
'二手挂牌价'
);
}
if (!empty($veh['reduction_price']['amount'])) {
$item->addMoney(
'reduction_price',
$veh['reduction_price']['currency'],
$veh['reduction_price']['amount'],
$veh['reduction_price']['rate'],
'二手减价'
);
}
} else {
// 新车
if (!empty($veh['official_price']['amount'])) {
$item->addMoney(
'official_price',
$veh['official_price']['currency'],
$veh['official_price']['amount'],
$veh['official_price']['rate'],
'官方指导价'
);
}
if (!empty($veh['discount_price']['amount'])) {
$item->addMoney(
'discount_price',
$veh['discount_price']['currency'],
$veh['discount_price']['amount'],
$veh['discount_price']['rate'],
'优惠价'
);
}
}
}
/**
* 计算利润
*/
private function calculateProfit(array $veh, $order): float
{
$unitPrice = (float)($veh['unit_price']['amount'] ?? 0);
if (!empty($veh['mileage']) && $veh['mileage'] > 0) {
// 二手车
$ttPrice = ($veh['listing_price']['amount'] ?? 0) / ($order->contract_exchange_rate ?: 1);
$ddPrice = $veh['reduction_price']['amount'] ?? 0;
} else {
// 新车
$ttPrice = ($veh['official_price']['amount'] ?? 0) / ($order->contract_exchange_rate ?: 1);
$ddPrice = $veh['discount_price']['amount'] ?? 0;
}
$handlingFee = (float)($veh['handling_fee']['amount'] ?? 0);
$fobTransportFee = (float)($veh['fob_transport_fee']['amount'] ?? 0);
$portMiscFee = (float)($veh['port_misc_fee']['amount'] ?? 0);
$retrofitFee = (float)($veh['retrofit_fee']['amount'] ?? 0);
$oceanFreight = (float)($veh['ocean_freight']['amount'] ?? 0);
return $unitPrice - $ttPrice - $ddPrice - $handlingFee - $fobTransportFee - $portMiscFee - $retrofitFee - $oceanFreight;
}
/**
* 处理改装项
*/
private function processModificationItems(int $orderItemId, array $modifyList): void
{
if (empty($modifyList)) {
return;
}
foreach ($modifyList as $modify) {
$modify['order_item_id'] = $orderItemId;
StockOrderItemOptions::create($modify);
}
}
/**
* 处理账户项
*/
private function processAccountItems($order, array $accountList): void
{
if (empty($accountList)) {
return;
}
foreach ($accountList as $account) {
$account['order_id'] = $order->id;
if (!empty($account['remit_date'])) {
$account['remit_date'] = strtotime($account['remit_date']);
}
StockOrderAccounts::create($account);
}
}
}
@@ -0,0 +1,435 @@
<?php
namespace app\common\service;
use app\common\model\StockOrderAccounts;
use app\common\model\StockOrderItemOptions;
use app\common\model\StockOrderItems;
use app\common\validate\StockOrdersValidate;
use app\common\traits\crud\CrudServiceTrait;
use app\common\model\StockOrders;
use app\common\model\SysCurrency;
use support\Request;
use app\common\model\User;
use support\Db;
class StockOrdersService_bak
{
use CrudServiceTrait;
/**
* 指定当前要操作的模型
*/
protected string $modelClass = StockOrders::class;
/**
* 验证器
*/
protected $validator = StockOrdersValidate::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':
// ====== ====== 筛选 ====== ======
// 业务id
if (isset($p['salesperson_id']) && $p['salesperson_id']) {
$config['where']['salesperson_id'] = $p['salesperson_id'];
}
// 初、终版
$config['where']['is_final'] = $p['is_final'] ?? 0;
// 关键字
if (isset($p['keyword']) && $p['keyword']) {
$k = '%' . $p['keyword'] . '%';
foreach (['pi_no', 'order_no', 'customer_snapshot'] as $col) {
$config['orWhere'][] = [$col, 'like', $k];
}
}
$config['order']['id'] = 'desc';
// $config['dataLimit']['type'] = 'relation';
// $config['dataLimit']['userId'] = $request->user->uid;
// $config['dataLimit']['userField'] = 'salesperson_id';
break;
case 'print':
$config['where']['id'] = $p['id'];
break;
case 'show':
$config['where']['id'] = $p['id'];
$config['with'][] = 'items';
$config['with'][] = 'items.options';
$config['with'][] = 'items.moneyValues';
$config['with'][] = 'accounts';
$config['with'][] = 'moneyValues';
$config['with'][] = 'salesperson';
break;
case 'store':
break;
case 'update':
$config['where']['id'] = $p['id'];
break;
case 'destroy':
$config['where']['id'] = $p['id'];
break;
}
return $config;
}
public function serviceStore(Request $request, array $data = [])
{
// 关联表较多,增加事务处理
return Db::transaction(function () use ($request, $data) {
$data = $this->preProcess('Store', $data, $request);
$dl = $this->getDataLimitFromRequest($request);
$created = $this->logicCreate($data, $dl); // 表1
// 车辆
$cifTotalAmount = 0;
$cifTotalAmountRate = 0;
$cifTotalAmountCurrency = 'CNY';
// $profit = 0;
foreach ($data['veh_list'] as $veh) {
$veh['order_id'] = $created->id;
// 唯一标识
$uniqueIdStr = "";
$uniqueIdStr .= $veh['sku_id'];
$uniqueIdStr .= $veh['model_snapshot'];
$uniqueIdStr .= $veh['color_interior_code'];
$uniqueIdStr .= $veh['color_exterior_code'];
$uniqueIdStr .= $veh['mileage'];
$uniqueIdStr .= json($veh['modify_list']);
$veh['unique_id'] = md5($uniqueIdStr);
$item = StockOrderItems::create($veh); // 表2
// 金额 // 表3
// $item->addMoney('official_price', $veh['official_price']['currency'], $veh['official_price']['amount'], $veh['official_price']['rate'], '官方指导价');
// $item->addMoney('discount_price', $veh['discount_price']['currency'], $veh['discount_price']['amount'], $veh['discount_price']['rate'], '优惠价');
if ($veh['unit_price']['amount']) {
$item->addMoney('unit_price', $veh['unit_price']['currency'], $veh['unit_price']['amount'], $veh['unit_price']['rate'], '单价');
}
if ($veh['total_price']['amount']) {
$item->addMoney('total_price', $veh['total_price']['currency'], $veh['total_price']['amount'], $veh['total_price']['rate'], '总价');
}
if ($veh['handling_fee']['amount']) {
$item->addMoney('handling_fee', $veh['handling_fee']['currency'], $veh['handling_fee']['amount'], $veh['handling_fee']['rate'], '手续费');
}
if ($veh['fob_transport_fee']['amount']) {
$item->addMoney('fob_transport_fee', $veh['fob_transport_fee']['currency'], $veh['fob_transport_fee']['amount'], $veh['fob_transport_fee']['rate'], 'FOB运输费');
}
if ($veh['port_misc_fee']['amount']) {
$item->addMoney('port_misc_fee', $veh['port_misc_fee']['currency'], $veh['port_misc_fee']['amount'], $veh['port_misc_fee']['rate'], '港杂');
}
if ($veh['retrofit_fee']['amount']) {
$item->addMoney('retrofit_fee', $veh['retrofit_fee']['currency'], $veh['retrofit_fee']['amount'], $veh['retrofit_fee']['rate'], '改装费');
}
// 二手车 or 新车
if ($veh['mileage'] > 0) {
if ($veh['listing_price']['amount']) {
$item->addMoney('listing_price', $veh['listing_price']['currency'], $veh['listing_price']['amount'], $veh['listing_price']['rate'], '二手挂牌价');
}
if ($veh['reduction_price']['amount']) {
$item->addMoney('reduction_price', $veh['reduction_price']['currency'], $veh['reduction_price']['amount'], $veh['reduction_price']['rate'], '二手减价');
}
$ttPrice = ($veh['listing_price']['amount'] / $created->contract_exchange_rate);
$ddPrice = $veh['reduction_price']['amount'];
} else {
if ($veh['official_price']['amount']) {
$item->addMoney('official_price', $veh['official_price']['currency'], $veh['official_price']['amount'], $veh['official_price']['rate'], '官方指导价');
}
if ($veh['discount_price']['amount'] ) {
$item->addMoney('discount_price', $veh['discount_price']['currency'], $veh['discount_price']['amount'], $veh['discount_price']['rate'], '优惠价');
}
$ttPrice = ($veh['official_price']['amount'] / $created->contract_exchange_rate);
$ddPrice = $veh['discount_price']['amount'];
}
// 利润
$profit = (float)$veh['unit_price']['amount'] - (float)$ttPrice - (float)$ddPrice - (float)$veh['handling_fee']['amount'] - (float)$veh['fob_transport_fee']['amount'] - (float)$veh['port_misc_fee']['amount'] - (float)$veh['retrofit_fee']['amount'];
$item->addMoney('profit', $veh['unit_price']['currency'], $profit, $veh['unit_price']['rate'], '利润');
// cif_total_amount
$cifTotalAmount += $veh['total_price']['amount'];
$cifTotalAmountRate = $veh['retrofit_fee']['rate'];
$cifTotalAmountCurrency = $veh['retrofit_fee']['currency'];
// 改装
if (count($veh['modify_list']) > 0) {
foreach ($veh['modify_list'] as $modify) {
$modify['order_item_id'] = $item->id;
StockOrderItemOptions::create($modify); // 表4
}
}
}
// 计算&&插入 表1
$created->addMoney('cif_total_amount', $cifTotalAmountCurrency, $cifTotalAmount, $cifTotalAmountRate, '合计到岸价');
// 账户
if (count($data['account_list']) > 0) {
foreach ($data['account_list'] as $account) {
$account['order_id'] = $created->id;
$account['remit_date'] = strtotime($account['remit_date']);
StockOrderAccounts::create($account); // 表5
}
}
return $this->postProcess('Store', $created);
});
}
public function beforeServiceStore($data, Request $request)
{
$userId = $request->user->uid;
$data['order_no'] = generateStockNo();
$data['biz_date'] = strtotime($data['biz_date']);
$data['eta_ship_date'] = strtotime($data['eta_ship_date']);
$data['latest_arrival_date'] = strtotime($data['latest_arrival_date']);
$data['salesperson_id'] = $userId;
$data['salesperson_snapshot'] = User::where('id', $userId)->value('name') ?? '';
return $data;
}
protected function afterServiceStore($res)
{
// 处理原单
if ($res->is_final == 1 && $res->object_id > 0) {
$order = StockOrders::find($res->object_id);
$order->is_use = 1;
$order->object_id = $res->id;
if (!$order->save()) {
flg('备货单【'.$order->order_no.'】创建终版成功,但更新原单失败');
}
}
// 启动审批
$appro = new \app\common\utils\approval\service\ApprovalFlowService();
$result = $appro->start('stock_order_flow', 'stock_orders', $res->id, $res->toArray(), $res->salesperson_id);
var_dump($result);
// 异步生成pdf文件
\Webman\RedisQueue\Redis::send('stock-order-pdf', ['id'=>$res->id]);
return $res;
}
public function createdPdfFile($id)
{
$config = [];
$config['model'] = $this->modelClass;
$config['where']['id'] = $id;
$config['with'][] = 'items';
$config['with'][] = 'items.options';
$config['with'][] = 'items.moneyValues';
$config['with'][] = 'accounts';
$config['with'][] = 'moneyValues';
$config['with'][] = 'salesperson';
$item = $this->logicShow($config);
$pdfData = $this->pdfData($item);
// 渲染模板为最终HTML
$tplPath = app_path('common/utils/pdf/template/stockOrder.stub');
$html = \app\common\utils\pdf\service\PdfHelper::render($tplPath, ['data' => $pdfData]);
// 生成PDF
$driver = new \app\common\utils\pdf\service\MpdfDriver();
$pdfBin = $driver->htmlToPdf($html);
// 存储
$dir = public_path('pdf').'/';
$fileName = ($pdfData['order_no'] ?? date('YmdHis')) . '.pdf';
// 本地保存
\app\common\utils\pdf\service\PdfStorage::saveBinary(
$pdfBin,
$dir,
$fileName
);
// // 在线预览
// return new \support\Response(200, [
// 'Content-Type' => 'application/pdf',
// 'Content-Disposition' => 'inline; filename="stock-order.pdf"',
// ], $pdfBin);
// $url = [
// 'file_name' => $fileName,
// 'file_url' => getenv('MAIN_DOMAIN').'/pdf/'.$fileName
// ];
// 上传云端
$param = [
'path' => $dir . $fileName,
'scene' => 'stock_orders',
'category' => 'pdf',
'original_name' => $fileName
];
$up = new UploadService();
$res = $up->uploadLocalFile($param);
@unlink($dir . $fileName);
// 更新文件地址
$item->file_url = $res['file_name'];
$item->save();
return $res;
}
public function servicePrint(Request $request)
{
$config = $this->buildQueryConfig($request, 'show');
$item = $this->logicShow($config);
return $this->postProcess('Print', $item);
}
public function pdfData($item)
{
$vehList = [];
$index = 0;
foreach ($item->items as $k => $v) {
// 存在则数量加1
if (count($vehList) > 0 && isset($vehList[$v->unique_id])) {
$vehList[$v->unique_id]['num'] += 1;
$vehList[$v->unique_id]['total_price'] += $vehList[$v->unique_id]['unit_price'];
$vehList[$v->unique_id]['total_price_text'] = $vehList[$v->unique_id]['total_price_symbol'].number_format($vehList[$v->unique_id]['total_price'], 2, '.', ',');
continue;
}
$index += 1;
// 处理金额
$mv = [];
foreach ($v->moneyValues as $money) {
$mv[$money->field] = [
'currency' => $money->currency,
'amount' => $money->amount,
'rate' => $money->exchange_rate,
'currency_symbol' => SysCurrency::where('currency_code', $money->currency)->value('currency_symbol'),
];
}
// 处理改装
$mfy = '';
foreach ($v->options as $my) {
$mfy .= $my->option_name_snapshot.' ';
}
// 处理结果集
$vehList[$v->unique_id] = [
'index' => $index,
'model_snapshot' => $v->model_snapshot,
'color_interior_snapshot' => $v->color_interior_snapshot,
'color_exterior_snapshot' => $v->color_exterior_snapshot,
'num' => 1,
'modify' => $mfy,
'mileage' => $v->mileage,
'unit_price' => $mv['unit_price']['amount'],
'total_price' => $mv['total_price']['amount'],
'unit_price_symbol' => $mv['unit_price']['currency_symbol'],
'total_price_symbol' => $mv['total_price']['currency_symbol'],
'unit_price_text' => $mv['unit_price']['currency_symbol'].number_format($mv['unit_price']['amount'], 2, '.', ','),
'total_price_text' => $mv['total_price']['currency_symbol'].number_format($mv['total_price']['amount'], 2, '.', ',')
];
// 根据里程处理 新车or二手车
if ($v->mileage > 0) {
if (isset($mv['reduction_price'])) {
$vehList[$v->unique_id]['reduction_price'] = $mv['reduction_price']['amount'];
$vehList[$v->unique_id]['reduction_price_symbol'] = $mv['reduction_price']['currency_symbol'];
$vehList[$v->unique_id]['reduction_price_text'] = $mv['reduction_price']['currency_symbol'].number_format($mv['reduction_price']['amount'], 2, '.', ',');
}
$vehList[$v->unique_id]['listing_price'] = $mv['listing_price']['amount'];
$vehList[$v->unique_id]['listing_price_symbol'] = $mv['listing_price']['currency_symbol'];
$vehList[$v->unique_id]['listing_price_text'] = $mv['listing_price']['currency_symbol'].number_format($mv['listing_price']['amount'], 2, '.', ',');
} else {
if (isset($mv['discount_price'])) {
$vehList[$v->unique_id]['discount_price'] = $mv['discount_price']['amount'];
$vehList[$v->unique_id]['discount_price_symbol'] = $mv['discount_price']['currency_symbol'];
$vehList[$v->unique_id]['discount_price_text'] = $mv['discount_price']['currency_symbol'].number_format($mv['discount_price']['amount'], 2, '.', ',');
}
$vehList[$v->unique_id]['official_price'] = $mv['official_price']['amount'];
$vehList[$v->unique_id]['official_price_symbol'] = $mv['official_price']['currency_symbol'];
$vehList[$v->unique_id]['official_price_text'] = $mv['official_price']['currency_symbol'].number_format($mv['official_price']['amount'], 2, '.', ',');
}
}
$accList = [];
$accIndex = 1;
foreach ($item->accounts as $k => $v) {
$accList[] = [
'index' => '收款'.$k + 1,
'remit_country_name_snapshot' => $v->remit_country_name_snapshot,
'remit_date' => date('Y-m-d', $v->remit_date),
'currency_name_snapshot' => $v->currency_name_snapshot,
'account_name_snapshot' => $v->account_name_snapshot,
'remitter_name' => $v->remitter_name,
'amount' => $v->amount,
];
$accIndex += 1;
}
$cifTotalAmount = 0.00;
if ($item->moneyValues) {
$cifTotalAmountSysboml = SysCurrency::where('currency_code', $item->moneyValues->currency)->value('currency_symbol');
$cifTotalAmount = $cifTotalAmountSysboml.sprintf("%.2f", $item->moneyValues->amount, $item->moneyValues->exchange_rate);
}
$veh = [
'order_no' => $item->order_no,
'salesperson_snapshot' => $item->salesperson_snapshot,
// 'sign_img' => $item->salesperson->sign_img,
'sign_img_url' => (!empty($item->salesperson->sign_img))?(new \app\common\service\OssService)->generatePrivateUrl($item->salesperson->sign_img):'',
'created_at' => date('Y-m-d', strtotime($item->created_at)),
'pi_no' => $item->pi_no,
'biz_date' => date('Y-m-d', $item->biz_date),
'clearance_country_snapshot' => $item->clearance_country_snapshot,
'customer_source_snapshot' => $item->customer_source_snapshot,
'customer_snapshot' => $item->customer_snapshot,
'customer_country_snapshot' => $item->customer_country_snapshot,
// 订单项
'veh_list' => $vehList,
'contract_exchange_rate' => $item->contract_exchange_rate,
'cif_total_amount' => $item->cif_total_amount,
'declarant_name' => $item->declarant_name,
'eta_ship_date' => date('Y-m-d', $item->eta_ship_date),
'shipping_method_snapshot' => $item->shipping_method_snapshot,
'domestic_port_snapshot' => $item->domestic_port_snapshot,
'trade_term_snapshot' => $item->trade_term_snapshot,
'clearance_port_snapshot' => $item->clearance_port_snapshot,
'latest_arrival_date' => date('Y-m-d', $item->latest_arrival_date),
// 账户
'account_list' => $accList,
'acc_index' => $accIndex,
'cif_total_amount' => $cifTotalAmount
];
return $veh;
}
}
+1 -1
View File
@@ -50,7 +50,7 @@ class UploadService
$size = filesize($filePath) ?: 0;
$sceneUrl = $scene ? ($scene . '/') : '';
$dir = 'vehicle_album/' . $sceneUrl . date('Ymd') . '/';
$dir = 'x_server/' . $sceneUrl . date('Ymd') . '/';
$uniqueName = md5($originalName . microtime(true));
$objectKey = $dir . $uniqueName . ($extension ? ('.' . $extension) : '');
+6 -1
View File
@@ -52,6 +52,9 @@ class UserService
'sn' => $user->sn,
'corp_id' => $user->corp_id,
'corp_name' => $corp_name,
'report_to' => $user->report_to,
'sign_img' => $user->sign_img,
'sign_img_url' => (!empty($user->sign_img))?(new \app\common\service\OssService)->generatePrivateUrl($user->sign_img):'',
],
// 'menu' => $menuTree,
// 'permissions' => $permissionCodes
@@ -84,7 +87,9 @@ class UserService
'email' => $user->email,
'sex' => $user->sex,
'status' => $user->status,
'roles' => $user->roles->pluck('name')
'roles' => $user->roles->pluck('name'),
'sign_img' => $user->sign_img,
'sign_img_url' => (!empty($user->sign_img))?$ossservice->generatePrivateUrl($user->sign_img):'',
];
}
@@ -1,6 +1,6 @@
<?php
namespace app\common\approval\model;
namespace app\common\utils\approval\model;
use support\Model;
@@ -1,6 +1,6 @@
<?php
namespace app\common\approval\model;
namespace app\common\utils\approval\model;
use support\Model;
@@ -1,6 +1,6 @@
<?php
namespace app\common\approval\model;
namespace app\common\utils\approval\model;
use support\Model;
@@ -1,6 +1,6 @@
<?php
namespace app\common\approval\model;
namespace app\common\utils\approval\model;
use support\Model;
File diff suppressed because it is too large Load Diff
+9 -3
View File
@@ -431,12 +431,12 @@ class Emails
$enterUser = User::where('id', $enterUserId)->first();
// 固定渠道
$channel = 8;
$channel = 12;
$channelInfo = InquiryChannel::where('id', $channel)->first();
// 初始化 UID 轮询器
$uidRotator = new \app\common\utils\RedisUidRotator('email_parsed_result_20250804');
$uidRotator->setUids([64, 63, 83, 93, 108, 118, 132, 145, 150, 143, 156, 157, 158], false);
$uidRotator->setUids([64, 63, 83, 93, 108, 132, 145, 150, 143, 156, 157, 158], false);
EmailParsedResult::where('status', 0)
->chunkById($batchSize, function ($records) use ($delaySeconds, $throttleAfter, $throttleSeconds, $enterUser, $channelInfo, $uidRotator) {
@@ -452,7 +452,13 @@ class Emails
$assignedUid = $uidRotator->next();
// 获取分配的用户信息
$recipientInfo = User::where('id', $assignedUid)->first();
$recipientInfo = User::where('id', $assignedUid)->where('status', 1)->first();
if (!$recipientInfo) {
flg("用户ID=" . $assignedUid . " 不存在或已离职跳过分配,需要手动移除 UID");
$assignedUid = $uidRotator->next();
$recipientInfo = User::where('id', $assignedUid)->where('status', 1)->first();
}
// 生成录单
$log = [
@@ -0,0 +1,76 @@
<?php
// app/controller/PdfController.php
namespace app\controller;
use support\Request;
use support\Response;
use support\View;
use app\common\utils\pdf\service\MpdfDriver;
use app\common\utils\pdf\service\PdfHelper;
class PdfController
{
// ① 从“现有 HTML 文件 + 外链CSS”生成并在线预览
public function previewFromFile(Request $request): Response
{
// 你的模板路径(示例)
$tpl = base_path('resource/templates/stock-order.html');
// 可选:把 {{变量}} 替换
$html = PdfHelper::loadTemplate($tpl, [
'title' => '备货单',
'order_no' => 'SO' . date('YmdHis'),
'customer' => 'Magic Bridge Auto FZE',
]);
$driver = new MpdfDriver();
$pdf = $driver->htmlToPdf($html, [
// 若 HTML 里用到相对资源(img/css),给个基准目录
'asset_base' => public_path('assets/pdf'),
// 也可以单独传 CSS 文件(推荐把 CSS 放在 public/assets/pdf)
'css_files' => [
public_path('assets/pdf/stock-order.css'),
],
'header_html' => '<div style="text-align:right;color:#888;font-size:10px">Company • Stock Order</div>',
'footer_html' => '<div style="text-align:center;color:#888;font-size:10px">{PAGENO} / {nbpg}</div>',
'watermark_text' => '', // 'DRAFT'
]);
return new Response(200, [
'Content-Type' => 'application/pdf',
'Content-Disposition' => 'inline; filename="stock-order.pdf"',
], $pdf);
}
// ② 直接下载
public function download(Request $request): Response
{
$html = '<h1>发票</h1><p>客户:永生花</p>';
$pdf = (new MpdfDriver())->htmlToPdf($html);
return new Response(200, [
'Content-Type' => 'application/pdf',
'Content-Disposition' => 'attachment; filename="invoice.pdf"',
], $pdf);
}
// ③ 保存到本地(或上传 OSS 前先落盘)
public function save(Request $request): Response
{
$tpl = base_path('resource/templates/invoice.html');
$html = PdfHelper::loadTemplate($tpl, [
'invoice_no' => 'INV' . date('YmdHis'),
'amount' => '12345.67',
]);
$pdf = (new MpdfDriver())->htmlToPdf($html, [
'css_files' => [ public_path('assets/pdf/invoice.css') ],
'asset_base'=> public_path('assets/pdf'),
]);
$dir = runtime_path('pdf');
if (!is_dir($dir)) mkdir($dir, 0777, true);
$path = $dir . '/invoice-' . date('YmdHis') . '.pdf';
file_put_contents($path, $pdf);
return json(['path' => $path]);
}
}
+118
View File
@@ -0,0 +1,118 @@
<?php
// app/service/pdf/MpdfDriver.php
namespace app\common\utils\pdf\service;
use Mpdf\Mpdf;
use Mpdf\Config\ConfigVariables;
use Mpdf\Config\FontVariables;
use Mpdf\HTMLParserMode;
class MpdfDriver
{
public function htmlToPdf(string $html, array $options = []): string
{
$opts = array_merge([
'format' => 'A4',
'orientation' => 'L',
'margin_top' => 12,
'margin_bottom' => 12,
'margin_left' => 10,
'margin_right' => 10,
'tempDir' => runtime_path('mpdf'),
'header_html' => '',
'footer_html' => '',
'watermark_text'=> '',
'css_files' => [],
'default_font' => 'notosanssc',
'font_dirs' => [public_path('fonts')],
'font_data' => [
'notosanssc' => ['R'=>'NotoSansSC-Regular.ttf','B'=>'NotoSansSC-Bold.ttf'],
],
'asset_base' => '',
'enable_html5_parser' => true,
], $options);
$defaultConfig = (new ConfigVariables())->getDefaults();
$defaultFontConfig = (new FontVariables())->getDefaults();
$fontDirs = array_merge($defaultConfig['fontDir'], $opts['font_dirs']);
$fontData = array_merge($defaultFontConfig['fontdata'], $opts['font_data']);
$mpdf = new Mpdf([
'format' => $opts['format'],
'orientation' => $opts['orientation'],
'margin_left' => $opts['margin_left'],
'margin_right' => $opts['margin_right'],
'margin_top' => $opts['margin_top'],
'margin_bottom' => $opts['margin_bottom'],
'tempDir' => $opts['tempDir'],
'fontDir' => $fontDirs,
'fontdata' => $fontData,
'default_font' => $opts['default_font'],
'enable_html5_parser' => $opts['enable_html5_parser'],
]);
if ($opts['header_html']) $mpdf->SetHTMLHeader($opts['header_html']);
if ($opts['footer_html']) $mpdf->SetHTMLFooter($opts['footer_html']);
if (!empty($opts['watermark_text'])) {
$mpdf->SetWatermarkText($opts['watermark_text']);
$mpdf->showWatermarkText = true;
$mpdf->watermarkTextAlpha = 0.08;
}
// 资源绝对化(如果你有相对路径的图片等)
$html = PdfHelper::absolutizeAssetUrls($html, $opts['asset_base']);
// === 关键改动:把 <style>…</style> 抽出来,作为 HEADER_CSS 写入 ===
[$inlineCss, $cleanHtml] = $this->extractCssAndCleanHtml($html);
// 先写外部 css_files
if (!empty($opts['css_files'])) {
foreach ($opts['css_files'] as $cssFile) {
if (is_file($cssFile)) {
$mpdf->WriteHTML(file_get_contents($cssFile), HTMLParserMode::HEADER_CSS);
}
}
}
// 再写内联 <style> 的 CSS
if ($inlineCss !== '') {
$mpdf->WriteHTML($inlineCss, HTMLParserMode::HEADER_CSS);
}
// 最后写 HTML 正文(已剔除 <style>,避免任何情况下把 CSS 当文字打印)
$mpdf->WriteHTML($cleanHtml, HTMLParserMode::HTML_BODY);
return $mpdf->Output('', \Mpdf\Output\Destination::STRING_RETURN);
}
/**
* 把所有 <style>...</style> 的内容拼成一段 CSS,并把这些 <style> 标签从 HTML 中移除
* @return array{0:string,1:string} [css, htmlWithoutStyle]
*/
private function extractCssAndCleanHtml(string $html): array
{
$cssParts = [];
// 抓取所有 <style ...>...</style>,DOTALL 支持跨行
if (preg_match_all('/<style\b[^>]*>(.*?)<\/style>/is', $html, $m)) {
foreach ($m[1] as $block) {
// 去掉 HTML 注释,以防注释把 CSS 包起来
$block = preg_replace('/<!--|-->/', '', $block);
$cssParts[] = trim($block);
}
// 从 HTML 删除所有 <style> 块
$html = preg_replace('/<style\b[^>]*>.*?<\/style>/is', '', $html);
}
$css = implode("\n\n", $cssParts);
// 额外保险:把 <head> 也清理掉,防止某些解析器把 head 里残余文本当正文
$html = preg_replace('/<head\b[^>]*>.*?<\/head>/is', '', $html);
// 确保剩余 HTML 有 <body>,没有就包一层
if (!preg_match('/<body\b/i', $html)) {
$html = '<body>' . $html . '</body>';
}
return [$css, $html];
}
}
@@ -0,0 +1,80 @@
<?php
// app/service/pdf/PdfHelper.php
namespace app\common\utils\pdf\service;
class PdfHelper
{
/** 从文件加载 HTML(支持占位符 {{key}} 简单替换) */
public static function loadTemplate(string $file, array $vars = []): string
{
if (!is_file($file)) {
throw new \RuntimeException("Template not found: {$file}");
}
$html = file_get_contents($file);
if ($vars) {
$search = [];
$replace = [];
foreach ($vars as $k => $v) {
$search[] = '{{' . $k . '}}';
$replace[] = htmlspecialchars((string)$v, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
}
$html = str_replace($search, $replace, $html);
}
return $html;
}
/**
* 把 HTML 里的相对资源路径(img/link/script href/src)改成绝对路径
* 仅处理以 ./ ../ 或非 http(s) 的相对量;base 传 public/assets/pdf 等目录
*/
public static function absolutizeAssetUrls(string $html, string $baseDir): string
{
$baseDir = rtrim($baseDir, DIRECTORY_SEPARATOR);
// img src
$html = preg_replace_callback('#(<img[^>]+src=["\'])([^"\']+)(["\'])#i', function($m) use ($baseDir){
$u = $m[2];
if (preg_match('#^https?://#i', $u) || strpos($u, 'data:') === 0) return $m[0];
$abs = realpath($baseDir . DIRECTORY_SEPARATOR . ltrim($u, '/')) ?: ($baseDir . '/' . ltrim($u, '/'));
return $m[1] . $abs . $m[3];
}, $html);
// link href(如果你在 HTML 里写了 <link>,通常建议走 css_files 方式,这里也兼容)
$html = preg_replace_callback('#(<link[^>]+href=["\'])([^"\']+)(["\'])#i', function($m) use ($baseDir){
$u = $m[2];
if (preg_match('#^https?://#i', $u)) return $m[0];
$abs = realpath($baseDir . DIRECTORY_SEPARATOR . ltrim($u, '/')) ?: ($baseDir . '/' . ltrim($u, '/'));
return $m[1] . $abs . $m[3];
}, $html);
return $html;
}
/**
* 渲染一个原生 PHP 模板为字符串
* @param string $file 模板路径(如 resource/templates/stock-order.php)
* @param array $vars 将在模板中以变量名存在(如 $data)
* @return string 完整 HTML
*/
public static function render(string $file, array $vars = []): string
{
if (!is_file($file)) {
throw new \RuntimeException("Template not found: {$file}");
}
// 将键名导出为同名变量,例如 'data' => [...] 变为 $data
extract($vars, EXTR_OVERWRITE);
// 开启缓冲,将 include 的输出抓取为字符串
ob_start();
ob_implicit_flush(false);
try {
/** @noinspection PhpIncludeInspection */
include $file;
} catch (\Throwable $e) {
// 清理缓冲并抛出更清晰的异常
ob_end_clean();
throw $e;
}
return ob_get_clean();
}
}
@@ -0,0 +1,38 @@
<?php
namespace app\common\utils\pdf\service;
final class PdfStorage
{
/**
* @param string $pdfBin PDF 二进制
* @param string|null $dir 保存目录,默认 runtime/pdf
* @param string|null $name 文件名(不含目录),默认:stock-order-YYYYmmdd-His-uniqid.pdf
* @return string 绝对路径
*/
public static function saveBinary(string $pdfBin, ?string $dir = null, ?string $name = null): string
{
$dir = $dir ?: runtime_path('pdf');
if (!is_dir($dir)) {
if (!@mkdir($dir, 0775, true) && !is_dir($dir)) {
throw new \RuntimeException("Failed to create dir: {$dir}");
}
}
// 生成安全文件名
$name = $name ?: ('stock-order-' . date('Ymd-His') . '-' . substr(uniqid('', true), -6) . '.pdf');
$name = preg_replace('/[^a-zA-Z0-9_\-.]/', '_', $name); // 简易净化
$path = rtrim($dir, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . $name;
// 先写入临时文件,再原子重命名,避免并发/写入中被读取
$tmp = $path . '.part';
if (file_put_contents($tmp, $pdfBin, LOCK_EX) === false) {
throw new \RuntimeException("Failed to write temp file: {$tmp}");
}
if (!@rename($tmp, $path)) {
@unlink($tmp);
throw new \RuntimeException("Failed to move temp to: {$path}");
}
@chmod($path, 0664);
return $path;
}
}
@@ -0,0 +1,319 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>合塑实业-汽车事业部-备货单(客户下定)</title>
<style>
/* 横版:A4 297mm,左右各 12mm => 可用宽度 273mm */
@page { margin: 12mm; }
body {
font-family: 'notosanssc', 'DejaVu Sans', Arial, sans-serif;
font-size: 10pt;
line-height: 1.3;
color: #000;
background: #fff;
}
.container {
width: 273mm; /* 横向版心 */
margin: 0 auto;
background: #fff;
}
.header {
text-align: center;
margin-bottom: 10pt;
padding-bottom: 6pt;
}
.title {
font-size: 14pt;
font-weight: bold;
margin-bottom: 6pt;
}
.sub-title {
font-size: 9pt;
color: #000;
margin: 0 6pt;
}
table {
width: 100%;
border-collapse: collapse;
margin-bottom: 10pt;
font-size: 9pt;
table-layout: fixed; /* 横向固定布局,配合 colgroup 更稳 */
}
th, td {
border: 0.5pt solid #000;
padding: 6pt 6pt;
text-align: center;
vertical-align: middle;
word-break: break-all;
}
th {
background-color: #f0f0f0;
font-weight: bold;
color: #000;
}
/* 跨页友好 */
thead { display: table-header-group; }
tfoot { display: table-row-group; }
tr, img { page-break-inside: avoid; }
/* 信息类表格左对齐 */
.info-table td,
.logistics-table td { text-align: left; }
.info-label { font-weight: bold; color: #000; }
.info-value { color: #000; }
.summary-row {
background-color: #f0f0f0;
font-weight: bold;
}
.payment-title {
background-color: #f0f0f0;
font-weight: bold;
text-align: center;
vertical-align: middle;
white-space: nowrap;
}
.nowrap { white-space: nowrap; }
</style>
</head>
<body>
<div class="container">
<!-- 头部 -->
<div class="header">
<h1 class="title">合塑实业-汽车事业部-备货单(客户下定)</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>
</div>
</div>
<!-- 基本信息 -->
<table class="info-table">
<colgroup>
<col style="width:18%">
<col style="width:15%">
<col style="width:14%">
<col style="width:14%">
<col style="width:19%">
<col style="width:20%">
</colgroup>
<tbody>
<tr>
<td><span class="info-label">阿里订单号/PI号:</span> <span class="info-value"><?php echo htmlspecialchars($data['pi_no'] ?? ' - '); ?></span></td>
<td><span class="info-label">日期:</span> <span class="info-value nowrap"><?php echo htmlspecialchars($data['biz_date'] ?? ' - '); ?></span></td>
<td><span class="info-label">关目的国:</span> <span class="info-value"><?php echo htmlspecialchars($data['clearance_country_snapshot'] ?? ' - '); ?></span></td>
<td><span class="info-label">客户来源:</span> <span class="info-value"><?php echo htmlspecialchars($data['customer_source_snapshot'] ?? ' - '); ?></span></td>
<td><span class="info-label">客户名称:</span> <span class="info-value"><?php echo htmlspecialchars($data['customer_snapshot'] ?? ' - '); ?></span></td>
<td><span class="info-label">客户所在国:</span> <span class="info-value"><?php echo htmlspecialchars($data['customer_country_snapshot'] ?? ' - '); ?></span></td>
</tr>
</tbody>
</table>
<!-- 产品列表 -->
<table class="product-table">
<colgroup>
<col style="width:6%">
<col style="width:34%">
<col style="width:6%">
<col style="width:6%">
<col style="width:6%">
<col style="width:14%">
<col style="width:8%">
<col style="width:8%">
<col style="width:6%">
<col style="width:6%">
</colgroup>
<thead>
<tr>
<th>编号</th>
<th>车型</th>
<th>数量/台</th>
<th>颜色/外</th>
<th>内饰</th>
<th>改装</th>
<th class="nowrap">官方指导价/二手挂牌价</th>
<th class="nowrap">优惠</th>
<th class="nowrap">单价</th>
<th class="nowrap">总价</th>
<th class="nowrap">利润</th>
</tr>
</thead>
<tbody>
<?php foreach (($data['veh_list'] ?? []) as $vehicle): ?>
<tr>
<td><?php echo htmlspecialchars($vehicle['index'] ?? ' - '); ?></td>
<td>
<?php echo htmlspecialchars($vehicle['model_snapshot'] ?? ' - '); ?>
<?php if ($vehicle['mileage'] > 0): ?>
<span class="nowrap">(<?php echo htmlspecialchars($vehicle['mileage']).'km'; ?>)</span>
<?php endif; ?>
</td>
<td><?php echo htmlspecialchars($vehicle['num'] ?? ' - '); ?></td>
<td><?php echo htmlspecialchars($vehicle['color_exterior_snapshot'] ?? ' - '); ?></td>
<td><?php echo htmlspecialchars($vehicle['color_interior_snapshot'] ?? ' - '); ?></td>
<td><?php echo htmlspecialchars($vehicle['modify'] ?? ' - '); ?></td>
<?php if ($vehicle['mileage'] > 0): ?>
<td class="nowrap"><?php echo htmlspecialchars($vehicle['listing_price_text'] ?? ' - '); ?></td>
<td class="nowrap"><?php echo htmlspecialchars($vehicle['reduction_price_text'] ?? ' - '); ?></td>
<?php else: ?>
<td class="nowrap"><?php echo htmlspecialchars($vehicle['official_price_text'] ?? ' - '); ?></td>
<td class="nowrap"><?php echo htmlspecialchars($vehicle['discount_price_text'] ?? ' - '); ?></td>
<?php endif; ?>
<td class="nowrap"><?php echo htmlspecialchars($vehicle['unit_price_text'] ?? ' - '); ?></td>
<td class="nowrap"><?php echo htmlspecialchars($vehicle['total_price_text'] ?? ' - '); ?></td>
<td class="nowrap"><?php echo htmlspecialchars($vehicle['profit_text'] ?? ' - '); ?></td>
</tr>
<?php endforeach; ?>
<tr class="summary-row">
<td colspan="6" style="text-align:left;">
<span class="info-label">合同汇率:</span> <span class="info-value"><?php echo htmlspecialchars($data['contract_exchange_rate'] ?? ' - '); ?></span>
</td>
<td colspan="5" style="text-align:left;">
<span class="info-label">合计到岸价:</span> <span class="info-value"><?php echo htmlspecialchars($data['cif_total_amount'] ?? ' - '); ?></span>
</td>
</tr>
</tbody>
</table>
<!-- 物流信息 -->
<table class="logistics-table">
<colgroup>
<col style="width:16%">
<col style="width:18%">
<col style="width:12%">
<col style="width:18%">
<col style="width:22%">
<col style="width:14%">
</colgroup>
<tbody>
<tr>
<td><span class="info-label">清关人:</span> <span class="info-value"><?php echo htmlspecialchars($data['declarant_name'] ?? ' - '); ?></span></td>
<?php if ($data['is_final'] == 1): ?>
<td><span class="info-label">全款收齐日:</span> <span class="info-value nowrap"><?php echo htmlspecialchars($data['collect_all_date'] ?? ' - '); ?></span></td>
<?php else: ?>
<td><span class="info-label">预计发运日期:</span> <span class="info-value nowrap"><?php echo htmlspecialchars($data['eta_ship_date'] ?? ' - '); ?></span></td>
<?php endif; ?>
<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>
<?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>
<?php else: ?>
<td><span class="info-label">最迟到港时间:</span> <span class="info-value nowrap"><?php echo htmlspecialchars($data['latest_arrival_date'] ?? ' - '); ?></span></td>
<?php endif; ?>
</tr>
</tbody>
</table>
<!-- 计划收款信息 -->
<table class="payment-table">
<colgroup>
<col style="width:12%">
<col style="width:10%">
<col style="width:14%">
<col style="width:12%">
<col style="width:12%">
<col style="width:20%">
<col style="width:10%">
</colgroup>
<thead>
<tr>
<th style="width:24mm" rowspan="<?php echo htmlspecialchars($data['acc_index']); ?>">计划收款信息</th>
<th>序号</th>
<th>汇款国</th>
<th>日期</th>
<th>币种</th>
<th>账户</th>
<th>汇款人</th>
<th>金额</th>
</tr>
</thead>
<tbody>
<?php foreach (($data['account_list'] ?? []) as $account): ?>
<tr>
<td class="nowrap"><?php echo htmlspecialchars($account['index'] ?? ' - '); ?></td>
<td><?php echo htmlspecialchars($account['remit_country_name_snapshot'] ?? ' - '); ?></td>
<td class="nowrap"><?php echo htmlspecialchars($account['remit_date'] ?? ' - '); ?></td>
<td class="nowrap"><?php echo htmlspecialchars($account['currency_name_snapshot'] ?? ' - '); ?></td>
<td class="nowrap"><?php echo htmlspecialchars($account['account_name_snapshot'] ?? ' - '); ?></td>
<td style="text-align:left;"><?php echo htmlspecialchars($account['remitter_name'] ?? ' - '); ?></td>
<td class="nowrap"><?php echo htmlspecialchars($account['amount'] ?? ' - '); ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<!-- PDF 底部签名区 -->
<div style="margin-top: 6mm; font-size: 12px;">
<!-- 左侧:业务员签字 / 备发货负责人签字 -->
<div style="width: 50%; float: left; text-align: left;">
<!-- 行距通过 margin-bottom 控制;行内元素通过固定行高和 vertical-align: middle 垂直居中 -->
<div style="height: 14mm; line-height: 14mm; margin-bottom: 5mm;">
<span>业务员签字:</span>
<?php if (!empty($data['sign_img_url'])): ?>
<img src="<?php echo htmlspecialchars($data['sign_img_url']); ?>" style="height: 12mm; vertical-align: middle;">
<?php else: ?>
<span style="display:inline-block; width:21mm; height:12mm; vertical-align: middle; border-bottom:0.2mm solid #000;"></span>
<?php endif; ?>
</div>
<div style="height: 14mm; line-height: 14mm;">
<span>备发货负责人签字:</span>
<?php if (!empty($data['sign_img_url'])): ?>
<img src="<?php echo htmlspecialchars($data['sign_img_url']); ?>" style="height: 12mm; vertical-align: middle;">
<?php else: ?>
<span style="display:inline-block; width:21mm; height:12mm; vertical-align: middle; border-bottom:0.2mm solid #000;"></span>
<?php endif; ?>
</div>
</div>
<!-- 右侧:组长签字 / 日期 -->
<div style="width: 50%; float: right; text-align: left;">
<div style="height: 14mm; line-height: 14mm; margin-bottom: 5mm;">
<span>组长签字:</span>
<?php if (!empty($data['sign_img_url'])): ?>
<img src="<?php echo htmlspecialchars($data['sign_img_url']); ?>" style="height: 12mm; vertical-align: middle;">
<?php else: ?>
<span style="display:inline-block; width:21mm; height:12mm; vertical-align: middle; border-bottom:0.2mm solid #000;"></span>
<?php endif; ?>
</div>
<div style="height: 14mm; line-height: 14mm;">
<span>日期:</span>
<?php
$_dt = $data['signed_date'] ?? ($data['created_at'] ?? '');
if (is_numeric($_dt)) {
echo htmlspecialchars(date('Y-m-d', (int)$_dt));
} elseif (!empty($_dt)) {
$t = strtotime($_dt);
echo $t ? htmlspecialchars(date('Y-m-d', $t)) : ' - ';
} else {
echo ' - ';
}
?>
</div>
</div>
<div style="clear: both;"></div>
</div>
</div>
</body>
</html>
@@ -0,0 +1,319 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>合塑实业-汽车事业部-备货单(客户下定)</title>
<style>
/* 横版:A4 297mm,左右各 12mm => 可用宽度 273mm */
@page { margin: 12mm; }
body {
font-family: 'notosanssc', 'DejaVu Sans', Arial, sans-serif;
font-size: 10pt;
line-height: 1.3;
color: #000;
background: #fff;
}
.container {
width: 273mm; /* 横向版心 */
margin: 0 auto;
background: #fff;
}
.header {
text-align: center;
margin-bottom: 10pt;
padding-bottom: 6pt;
}
.title {
font-size: 14pt;
font-weight: bold;
margin-bottom: 6pt;
}
.sub-title {
font-size: 9pt;
color: #000;
margin: 0 6pt;
}
table {
width: 100%;
border-collapse: collapse;
margin-bottom: 10pt;
font-size: 9pt;
table-layout: fixed; /* 横向固定布局,配合 colgroup 更稳 */
}
th, td {
border: 0.5pt solid #000;
padding: 6pt 6pt;
text-align: center;
vertical-align: middle;
word-break: break-all;
}
th {
background-color: #f0f0f0;
font-weight: bold;
color: #000;
}
/* 跨页友好 */
thead { display: table-header-group; }
tfoot { display: table-row-group; }
tr, img { page-break-inside: avoid; }
/* 信息类表格左对齐 */
.info-table td,
.logistics-table td { text-align: left; }
.info-label { font-weight: bold; color: #000; }
.info-value { color: #000; }
.summary-row {
background-color: #f0f0f0;
font-weight: bold;
}
.payment-title {
background-color: #f0f0f0;
font-weight: bold;
text-align: center;
vertical-align: middle;
white-space: nowrap;
}
.nowrap { white-space: nowrap; }
</style>
</head>
<body>
<div class="container">
<!-- 头部 -->
<div class="header">
<h1 class="title">合塑实业-汽车事业部-备货单(客户下定)</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>
</div>
</div>
<!-- 基本信息 -->
<table class="info-table">
<colgroup>
<col style="width:18%">
<col style="width:15%">
<col style="width:14%">
<col style="width:14%">
<col style="width:19%">
<col style="width:20%">
</colgroup>
<tbody>
<tr>
<td><span class="info-label">阿里订单号/PI号:</span> <span class="info-value"><?php echo htmlspecialchars($data['pi_no'] ?? ' - '); ?></span></td>
<td><span class="info-label">日期:</span> <span class="info-value nowrap"><?php echo htmlspecialchars($data['biz_date'] ?? ' - '); ?></span></td>
<td><span class="info-label">关目的国:</span> <span class="info-value"><?php echo htmlspecialchars($data['clearance_country_snapshot'] ?? ' - '); ?></span></td>
<td><span class="info-label">客户来源:</span> <span class="info-value"><?php echo htmlspecialchars($data['customer_source_snapshot'] ?? ' - '); ?></span></td>
<td><span class="info-label">客户名称:</span> <span class="info-value"><?php echo htmlspecialchars($data['customer_snapshot'] ?? ' - '); ?></span></td>
<td><span class="info-label">客户所在国:</span> <span class="info-value"><?php echo htmlspecialchars($data['customer_country_snapshot'] ?? ' - '); ?></span></td>
</tr>
</tbody>
</table>
<!-- 产品列表 -->
<table class="product-table">
<colgroup>
<col style="width:6%">
<col style="width:34%">
<col style="width:6%">
<col style="width:6%">
<col style="width:6%">
<col style="width:14%">
<col style="width:8%">
<col style="width:8%">
<col style="width:6%">
<col style="width:6%">
</colgroup>
<thead>
<tr>
<th>编号</th>
<th>车型</th>
<th>数量/台</th>
<th>颜色/外</th>
<th>内饰</th>
<th>改装</th>
<th class="nowrap">官方指导价/二手挂牌价</th>
<th class="nowrap">优惠</th>
<th class="nowrap">单价</th>
<th class="nowrap">总价</th>
<th class="nowrap">利润</th>
</tr>
</thead>
<tbody>
<?php foreach (($data['veh_list'] ?? []) as $vehicle): ?>
<tr>
<td><?php echo htmlspecialchars($vehicle['index'] ?? ' - '); ?></td>
<td>
<?php echo htmlspecialchars($vehicle['model_snapshot'] ?? ' - '); ?>
<?php if ($vehicle['mileage'] > 0): ?>
<span class="nowrap">(<?php echo htmlspecialchars($vehicle['mileage']).'km'; ?>)</span>
<?php endif; ?>
</td>
<td><?php echo htmlspecialchars($vehicle['num'] ?? ' - '); ?></td>
<td><?php echo htmlspecialchars($vehicle['color_exterior_snapshot'] ?? ' - '); ?></td>
<td><?php echo htmlspecialchars($vehicle['color_interior_snapshot'] ?? ' - '); ?></td>
<td><?php echo htmlspecialchars($vehicle['modify'] ?? ' - '); ?></td>
<?php if ($vehicle['mileage'] > 0): ?>
<td class="nowrap"><?php echo htmlspecialchars($vehicle['listing_price']['currency_info'] ?? ' - '); ?></td>
<td class="nowrap"><?php echo htmlspecialchars($vehicle['reduction_price']['currency_info'] ?? ' - '); ?></td>
<?php else: ?>
<td class="nowrap"><?php echo htmlspecialchars($vehicle['official_price']['currency_info'] ?? ' - '); ?></td>
<td class="nowrap"><?php echo htmlspecialchars($vehicle['discount_price']['currency_info'] ?? ' - '); ?></td>
<?php endif; ?>
<td class="nowrap"><?php echo htmlspecialchars($vehicle['unit_price']['currency_info'] ?? ' - '); ?></td>
<td class="nowrap"><?php echo htmlspecialchars($vehicle['total_price']['currency_info'] ?? ' - '); ?></td>
<td class="nowrap"><?php echo htmlspecialchars($vehicle['profit']['currency_info'] ?? ' - '); ?></td>
</tr>
<?php endforeach; ?>
<tr class="summary-row">
<td colspan="6" style="text-align:left;">
<span class="info-label">合同汇率:</span> <span class="info-value"><?php echo htmlspecialchars($data['contract_exchange_rate'] ?? ' - '); ?></span>
</td>
<td colspan="5" style="text-align:left;">
<span class="info-label">合计到岸价:</span> <span class="info-value"><?php echo htmlspecialchars($data['cif_total_amount']['currency_info'] ?? ' - '); ?></span>
</td>
</tr>
</tbody>
</table>
<!-- 物流信息 -->
<table class="logistics-table">
<colgroup>
<col style="width:16%">
<col style="width:18%">
<col style="width:12%">
<col style="width:18%">
<col style="width:22%">
<col style="width:14%">
</colgroup>
<tbody>
<tr>
<td><span class="info-label">清关人:</span> <span class="info-value"><?php echo htmlspecialchars($data['declarant_name'] ?? ' - '); ?></span></td>
<?php if ($data['is_final'] == 1): ?>
<td><span class="info-label">全款收齐日:</span> <span class="info-value nowrap"><?php echo htmlspecialchars($data['collect_all_date'] ?? ' - '); ?></span></td>
<?php else: ?>
<td><span class="info-label">预计发运日期:</span> <span class="info-value nowrap"><?php echo htmlspecialchars($data['eta_ship_date'] ?? ' - '); ?></span></td>
<?php endif; ?>
<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>
<?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>
<?php else: ?>
<td><span class="info-label">最迟到港时间:</span> <span class="info-value nowrap"><?php echo htmlspecialchars($data['latest_arrival_date'] ?? ' - '); ?></span></td>
<?php endif; ?>
</tr>
</tbody>
</table>
<!-- 计划收款信息 -->
<table class="payment-table">
<colgroup>
<col style="width:12%">
<col style="width:10%">
<col style="width:14%">
<col style="width:12%">
<col style="width:12%">
<col style="width:20%">
<col style="width:10%">
</colgroup>
<thead>
<tr>
<th style="width:24mm" rowspan="<?php echo htmlspecialchars($data['acc_index']); ?>">计划收款信息</th>
<th>序号</th>
<th>汇款国</th>
<th>日期</th>
<th>币种</th>
<th>账户</th>
<th>汇款人</th>
<th>金额</th>
</tr>
</thead>
<tbody>
<?php foreach (($data['account_list'] ?? []) as $account): ?>
<tr>
<td class="nowrap"><?php echo htmlspecialchars($account['index'] ?? ' - '); ?></td>
<td><?php echo htmlspecialchars($account['remit_country_name_snapshot'] ?? ' - '); ?></td>
<td class="nowrap"><?php echo htmlspecialchars($account['remit_date'] ?? ' - '); ?></td>
<td class="nowrap"><?php echo htmlspecialchars($account['currency_name_snapshot'] ?? ' - '); ?></td>
<td class="nowrap"><?php echo htmlspecialchars($account['account_name_snapshot'] ?? ' - '); ?></td>
<td style="text-align:left;"><?php echo htmlspecialchars($account['remitter_name'] ?? ' - '); ?></td>
<td class="nowrap"><?php echo htmlspecialchars($account['amount'] ?? ' - '); ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<!-- PDF 底部签名区 -->
<div style="margin-top: 6mm; font-size: 12px;">
<!-- 左侧:业务员签字 / 备发货负责人签字 -->
<div style="width: 50%; float: left; text-align: left;">
<!-- 行距通过 margin-bottom 控制;行内元素通过固定行高和 vertical-align: middle 垂直居中 -->
<div style="height: 14mm; line-height: 14mm; margin-bottom: 5mm;">
<span>业务员签字:</span>
<?php if (!empty($data['sign_img_url'])): ?>
<img src="<?php echo htmlspecialchars($data['sign_img_url']); ?>" style="height: 12mm; vertical-align: middle;">
<?php else: ?>
<span style="display:inline-block; width:21mm; height:12mm; vertical-align: middle; border-bottom:0.2mm solid #000;"></span>
<?php endif; ?>
</div>
<div style="height: 14mm; line-height: 14mm;">
<span>备发货负责人签字:</span>
<?php if (!empty($data['sign_imgs'][1])): ?>
<img src="<?php echo htmlspecialchars($data['sign_imgs'][1]); ?>" style="height: 12mm; vertical-align: middle;">
<?php else: ?>
<span style="display:inline-block; width:21mm; height:12mm; vertical-align: middle; border-bottom:0.2mm solid #000;"></span>
<?php endif; ?>
</div>
</div>
<!-- 右侧:组长签字 / 日期 -->
<div style="width: 50%; float: right; text-align: left;">
<div style="height: 14mm; line-height: 14mm; margin-bottom: 5mm;">
<span>组长签字:</span>
<?php if (!empty($data['sign_imgs'][0])): ?>
<img src="<?php echo htmlspecialchars($data['sign_imgs'][0]); ?>" style="height: 12mm; vertical-align: middle;">
<?php else: ?>
<span style="display:inline-block; width:21mm; height:12mm; vertical-align: middle; border-bottom:0.2mm solid #000;"></span>
<?php endif; ?>
</div>
<div style="height: 14mm; line-height: 14mm;">
<span>日期:</span>
<?php
$_dt = $data['signed_date'] ?? ($data['created_at'] ?? '');
if (is_numeric($_dt)) {
echo htmlspecialchars(date('Y-m-d', (int)$_dt));
} elseif (!empty($_dt)) {
$t = strtotime($_dt);
echo $t ? htmlspecialchars(date('Y-m-d', $t)) : ' - ';
} else {
echo ' - ';
}
?>
</div>
</div>
<div style="clear: both;"></div>
</div>
</div>
</body>
</html>
@@ -0,0 +1,255 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>合塑实业-汽车事业部-备货单(客户下定)</title>
<style>
/* 横版:A4 297mm,左右各 12mm => 可用宽度 273mm */
@page { margin: 12mm; }
body {
font-family: 'notosanssc', 'DejaVu Sans', Arial, sans-serif;
font-size: 10pt;
line-height: 1.3;
color: #000;
background: #fff;
}
.container {
width: 273mm; /* 横向版心 */
margin: 0 auto;
background: #fff;
}
.header {
text-align: center;
margin-bottom: 10pt;
padding-bottom: 6pt;
}
.title {
font-size: 14pt;
font-weight: bold;
margin-bottom: 6pt;
}
.sub-title {
font-size: 9pt;
color: #000;
margin: 0 6pt;
}
table {
width: 100%;
border-collapse: collapse;
margin-bottom: 10pt;
font-size: 9pt;
table-layout: fixed; /* 横向固定布局,配合 colgroup 更稳 */
}
th, td {
border: 0.5pt solid #000;
padding: 6pt 6pt;
text-align: center;
vertical-align: middle;
word-break: break-all;
}
th {
background-color: #f0f0f0;
font-weight: bold;
color: #000;
}
/* 跨页友好 */
thead { display: table-header-group; }
tfoot { display: table-row-group; }
tr, img { page-break-inside: avoid; }
/* 信息类表格左对齐 */
.info-table td,
.logistics-table td { text-align: left; }
.info-label { font-weight: bold; color: #000; }
.info-value { color: #000; }
.summary-row {
background-color: #f0f0f0;
font-weight: bold;
}
.payment-title {
background-color: #f0f0f0;
font-weight: bold;
text-align: center;
vertical-align: middle;
white-space: nowrap;
}
.nowrap { white-space: nowrap; }
</style>
</head>
<body>
<div class="container">
<!-- 头部 -->
<div class="header">
<h1 class="title">合塑实业-汽车事业部-备货单(客户下定)</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>
</div>
</div>
<!-- 基本信息 -->
<table class="info-table">
<colgroup>
<col style="width:18%">
<col style="width:15%">
<col style="width:14%">
<col style="width:14%">
<col style="width:19%">
<col style="width:20%">
</colgroup>
<tbody>
<tr>
<td><span class="info-label">阿里订单号/PI号:</span> <span class="info-value"><?php echo htmlspecialchars($data['pi_no'] ?? ' - '); ?></span></td>
<td><span class="info-label">日期:</span> <span class="info-value nowrap"><?php echo htmlspecialchars($data['biz_date'] ?? ' - '); ?></span></td>
<td><span class="info-label">关目的国:</span> <span class="info-value"><?php echo htmlspecialchars($data['clearance_country_snapshot'] ?? ' - '); ?></span></td>
<td><span class="info-label">客户来源:</span> <span class="info-value"><?php echo htmlspecialchars($data['customer_source_snapshot'] ?? ' - '); ?></span></td>
<td><span class="info-label">客户名称:</span> <span class="info-value"><?php echo htmlspecialchars($data['customer_snapshot'] ?? ' - '); ?></span></td>
<td><span class="info-label">客户所在国:</span> <span class="info-value"><?php echo htmlspecialchars($data['customer_country_snapshot'] ?? ' - '); ?></span></td>
</tr>
</tbody>
</table>
<!-- 产品列表 -->
<table class="product-table">
<colgroup>
<col style="width:6%">
<col style="width:34%">
<col style="width:6%">
<col style="width:6%">
<col style="width:6%">
<col style="width:14%">
<col style="width:8%">
<col style="width:8%">
<col style="width:6%">
<col style="width:6%">
</colgroup>
<thead>
<tr>
<th>编号</th>
<th>车型</th>
<th>数量/台</th>
<th>颜色/外</th>
<th>内饰</th>
<th>改装</th>
<th class="nowrap">单价</th>
<th class="nowrap">总价</th>
<th class="nowrap">官方指导价/二手挂牌价</th>
<th class="nowrap">优惠/二手减价</th>
</tr>
</thead>
<tbody>
<?php foreach (($data['veh_list'] ?? []) as $vehicle): ?>
<tr>
<td><?php echo htmlspecialchars($vehicle['index'] ?? ' - '); ?></td>
<td><?php echo htmlspecialchars($vehicle['model_snapshot'] ?? ' - '); ?></td>
<td><?php echo htmlspecialchars($vehicle['num'] ?? ' - '); ?></td>
<td><?php echo htmlspecialchars($vehicle['color_exterior_snapshot'] ?? ' - '); ?></td>
<td><?php echo htmlspecialchars($vehicle['color_interior_snapshot'] ?? ' - '); ?></td>
<td><?php echo htmlspecialchars($vehicle['modify'] ?? ' - '); ?></td>
<td class="nowrap"><?php echo htmlspecialchars($vehicle['unit_price_text'] ?? ' - '); ?></td>
<td class="nowrap"><?php echo htmlspecialchars($vehicle['total_price_text'] ?? ' - '); ?></td>
<?php if ($vehicle['mileage'] > 0): ?>
<td class="nowrap"><?php echo htmlspecialchars($vehicle['listing_price'] ?? ' - '); ?></td>
<td class="nowrap"><?php echo htmlspecialchars($vehicle['reduction_price'] ?? ' - '); ?></td>
<?php else: ?>
<td class="nowrap"><?php echo htmlspecialchars($vehicle['official_price'] ?? ' - '); ?></td>
<td class="nowrap"><?php echo htmlspecialchars($vehicle['discount_price'] ?? ' - '); ?></td>
<?php endif; ?>
</tr>
<?php endforeach; ?>
<tr class="summary-row">
<td colspan="6" style="text-align:left;">
<span class="info-label">合同汇率:</span> <span class="info-value"><?php echo htmlspecialchars($data['contract_exchange_rate'] ?? ' - '); ?></span>
</td>
<td colspan="4" style="text-align:left;">
<span class="info-label">合计到岸价:</span> <span class="info-value"><?php echo htmlspecialchars($data['cif_total_amount'] ?? ' - '); ?></span>
</td>
</tr>
</tbody>
</table>
<!-- 物流信息 -->
<table class="logistics-table">
<colgroup>
<col style="width:16%">
<col style="width:18%">
<col style="width:12%">
<col style="width:18%">
<col style="width:22%">
<col style="width:14%">
</colgroup>
<tbody>
<tr>
<td><span class="info-label">清关人:</span> <span class="info-value"><?php echo htmlspecialchars($data['declarant_name'] ?? ' - '); ?></span></td>
<td><span class="info-label">预计发运日期:</span> <span class="info-value nowrap"><?php echo htmlspecialchars($data['eta_ship_date'] ?? ' - '); ?></span></td>
<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 nowrap"><?php echo htmlspecialchars($data['latest_arrival_date'] ?? ' - '); ?></span></td>
</tr>
</tbody>
</table>
<!-- 计划收款信息 -->
<table class="payment-table">
<colgroup>
<col style="width:12%">
<col style="width:10%">
<col style="width:14%">
<col style="width:12%">
<col style="width:12%">
<col style="width:20%">
<col style="width:10%">
</colgroup>
<thead>
<tr>
<th style="width:24mm" rowspan="<?php echo htmlspecialchars($data['acc_index']); ?>">计划收款信息</th>
<th>序号</th>
<th>汇款国</th>
<th>日期</th>
<th>币种</th>
<th>账户</th>
<th>汇款人</th>
<th>金额</th>
</tr>
</thead>
<tbody>
<?php foreach (($data['account_list'] ?? []) as $account): ?>
<tr>
<td class="nowrap"><?php echo htmlspecialchars($account['index'] ?? ' - '); ?></td>
<td><?php echo htmlspecialchars($account['remit_country_name_snapshot'] ?? ' - '); ?></td>
<td class="nowrap"><?php echo htmlspecialchars($account['remit_date'] ?? ' - '); ?></td>
<td class="nowrap"><?php echo htmlspecialchars($account['currency_name_snapshot'] ?? ' - '); ?></td>
<td class="nowrap"><?php echo htmlspecialchars($account['account_name_snapshot'] ?? ' - '); ?></td>
<td style="text-align:left;"><?php echo htmlspecialchars($account['remitter_name'] ?? ' - '); ?></td>
<td class="nowrap"><?php echo htmlspecialchars($account['amount'] ?? ' - '); ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<div class="" style="text-align:right;">
<div style="padding-top: 6mm; padding-bottom: 6mm; display: table; margin: 0 0 0 auto;">
<div style="display: table-cell; vertical-align: middle;">
<span>下单人:</span>
<img src="<?php echo htmlspecialchars($data['sign_img_url'] ?? ' - '); ?>" style="width: 21mm; height: 12mm; vertical-align: middle;">
</div>
</div>
<div>下单时间:<?php echo htmlspecialchars($data['created_at'] ?? ' - '); ?></div>
</div>
</div>
</body>
</html>
@@ -7,6 +7,26 @@ return [
'translation_model' => \app\ticket\model\CategorieTranslation::class,
'foreign_key' => 'categorie_id',
],
'sys_currency' => [
'translation_model' => app\common\model\SysCurrencyTranslation::class,
'foreign_key' => 'currency_id',
],
'sys_dict' => [
'translation_model' => app\common\model\SysDictTranslation::class,
'foreign_key' => 'dict_id',
],
'sys_dict_item' => [
'translation_model' => app\common\model\SysDictItemTranslation::class,
'foreign_key' => 'dict_item_id',
],
'sys_port_translation' => [
'translation_model' => app\common\model\SysPortTranslation::class,
'foreign_key' => 'port_id',
],
'veh_color_translation' => [
'translation_model' => app\common\model\VehColorTranslation::class,
'foreign_key' => 'color_id',
],
],
// 默认支持的目标语种列表
+59 -68
View File
@@ -2,20 +2,11 @@
namespace app\controller;
use app\common\model\InquiryLog;
use app\common\model\User;
use app\common\model\UserRoles;
use app\common\model\UserSystems;
use app\common\service\MessageNotificationsService;
use app\common\traits\DataPermission;
use app\common\utils\email\MailSend;
use app\common\service\UploadService;
use app\common\utils\scaffold\Generate;
use support\Request;
use support\Db;
use support\Cache;
use app\inquiry\service\InquiryLogService;
use Illuminate\Support\Facades\Mail;
use support\Redis;
class IndexController
{
@@ -77,27 +68,27 @@ class IndexController
public function createResource()
{
$options = [
// 'controller' => [
// 'module' => 'common',
// 'name' => 'stock_orders'
// ],
'model' => [
'module' => 'approval',
'name' => 'approval_records'
'controller' => [
'module' => 'crm',
'name' => 'approval_flows'
],
// 'service' => [
// 'module' => 'common',
// 'name' => 'stock_orders'
// ],
// 'logic' => [
// 'module' => 'common',
// 'name' => 'stock_orders'
// ],
// 'validate' => [
// 'module' => 'common',
// 'name' => 'stock_orders'
// ],
'table' => 'approval_records'
'model' => [
'module' => 'crm',
'name' => 'approval_flows'
],
'service' => [
'module' => 'crm',
'name' => 'approval_flows'
],
'logic' => [
'module' => 'crm',
'name' => 'approval_flows'
],
'validate' => [
'module' => 'crm',
'name' => 'approval_flows'
],
'table' => 'approval_flows'
];
$result = (new Generate($options))->doGenerate();
@@ -105,48 +96,48 @@ class IndexController
}
public function test()
public function test(Request $request)
{
$data = [
'id' => 1,
'user_id' => 1,
'leave_type_id' => 1,
'start_date' => '2019-01-01',
'end_date' => '2019-01-02',
'reason' => 'test',
'status' => 1
];
$code = $this->submitLeaveApplication(1, $data);
return $code;
}
// 假设这是你的业务逻辑
public function submitLeaveApplication(int $bizId, $leaveData) {
// 1. 保存业务数据
// $bizId = DB::table('leave_applications')->insertGetId($leaveData);
// 2. 启动审批流程
$service = new \app\common\approval\service\ApprovalFlowService();
// $flowType = 'stock_order_flow';
// $bizType = 'stock_orders';
// $bizId = 32;
// $formData = [];
// $initiatorId = 74;
// $appro = new \app\common\utils\approval\service\ApprovalFlowService();
// // $sp = $appro->start($flowType, $bizType, $bizId, $formData, $initiatorId);
// $sp = $appro->approve(11, 1, 'reject');
// var_dump($sp);
// return json([
// 'code' => 0,
// 'msg' => 'success'
// ]);
// $param = [
// 'path' => public_path().'/pdf/BH20250908014.pdf',
// 'scene' => 'stock_orders',
// 'category' => 'pdf',
// 'original_name' => 'BH20250908014.pdf'
// ];
// $up = new UploadService();
// $res = $up->uploadLocalFile($param);
// var_dump($res);
$param = $request->all();
try {
$result = $service->start(
'leave', // 流程类型,对应 approval_flows.type
'leave_application', // 业务类型标识
$bizId, // 业务ID
$leaveData, // 表单数据快照
$_SESSION['user_id'] ?? 1 // 发起人ID
);
// 3. 将审批实例ID关联回业务表
DB::table('leave_applications')
->where('id', $bizId)
->update(['approval_instance_id' => $result['instance_id']]);
return ['status' => 'success', 'message' => '申请已提交,等待审批'];
} catch (\Exception $e) {
// 处理错误,可能需要回滚业务数据插入
return ['status' => 'error', 'message' => $e->getMessage()];
// $service = new \app\crm\service\StockOrdersService();
// $result = $service->createdPdfFile($param['id']);
// return json($result);
\Webman\RedisQueue\Redis::send('stock-order-pdf', ['id'=>82]);
} catch (\Throwable $th) {
//throw $th;
var_dump($th->getMessage().$th->getLine().$th->getFile());
}
}
}
@@ -0,0 +1,33 @@
<?php
namespace app\crm\controller;
use app\common\controller\BaseController;
use app\common\traits\crud\CrudControllerTrait;
use app\crm\service\ApprovalFlowsService;
class ApprovalFlowsController extends BaseController
{
use CrudControllerTrait;
protected $service;
public function __construct()
{
$this->service = new ApprovalFlowsService();
}
/**
* 列表页数据格式化
*
* @param array $data
* @return array
*/
// protected function indexFormat($data)
// {
// if (isset($data['created_at']))
// $data['created_at'] = date('Y-m-d H:i:s', $data['created_at']);
// if (isset($data['updated_at']))
// $data['updated_at'] = date('Y-m-d H:i:s', $data['updated_at']);
// return $data;
// }
}
@@ -0,0 +1,33 @@
<?php
namespace app\crm\controller;
use app\common\controller\BaseController;
use app\common\traits\crud\CrudControllerTrait;
use app\crm\service\ApprovalInstancesService;
class ApprovalInstancesController extends BaseController
{
use CrudControllerTrait;
protected $service;
public function __construct()
{
$this->service = new ApprovalInstancesService();
}
/**
* 列表页数据格式化
*
* @param array $data
* @return array
*/
// protected function indexFormat($data)
// {
// if (isset($data['created_at']))
// $data['created_at'] = date('Y-m-d H:i:s', $data['created_at']);
// if (isset($data['updated_at']))
// $data['updated_at'] = date('Y-m-d H:i:s', $data['updated_at']);
// return $data;
// }
}
@@ -0,0 +1,89 @@
<?php
namespace app\crm\controller;
use app\common\controller\BaseController;
use app\common\traits\crud\CrudControllerTrait;
use app\crm\service\ApprovalRecordsService;
use app\common\model\SysDictItem;
use support\Request;
class ApprovalRecordsController extends BaseController
{
use CrudControllerTrait;
protected $service;
public function __construct()
{
$this->service = new ApprovalRecordsService();
}
/**
* 列表页数据格式化
*
* @param array $data
* @return array
*/
protected function indexFormat($data)
{
if (isset($data['records']) && count($data['records']) > 0) {
foreach ($data['records'] as $key => &$record) {
$result = [
'id' => $record['id'],
'flows_name' => $record['approvalInstance']['flows']['name'],
'instance_id' => $record['instance_id'],
'biz_id' => $record['approvalInstance']['biz_id'],
'biz_desc' => '备货单【'.$record['approvalInstance']['bizInfo']['order_no'].'】',
'biz_type' => $record['approvalInstance']['flow_type'],
'initiator_name' => $record['approvalInstance']['initiator']['name'],
'initiator_avatar' => $record['approvalInstance']['initiator']['avatar'],
'approver_name' => $record['approver']['name'],
'approver_avatar' => $record['approver']['avatar'],
'step_index' => $record['step_index'],
'action' => $record['action'],
'action_desc' => SysDictItem::where('item_value', $record['action'])->where('status', 1)->value('item_label'),
'status' => $record['status'],
'status_desc' => SysDictItem::where('item_value', $record['status'])->where('status', 1)->value('item_label'),
'comment' => $record['comment'],
'approved_at' => $record['approved_at'],
'finished_at' => $record['approvalInstance']['finished_at'],
'deadline_at' => $record['approvalInstance']['deadline_at'],
'created_at' => $record['created_at']
];
$record = $result;
}
}
return $data;
}
public function approve(Request $request)
{
try {
$res = $this->service->serviceApprove($request);
return $this->success([], $res['msg']);
} catch (\Throwable $e) {
return $this->error($e->getMessage());
}
}
public function logs(Request $request)
{
try {
$instanceId = $request->get('instance_id');
$appro = new \app\common\utils\approval\service\ApprovalFlowService();
$res = $appro->getSimpleTimeline($instanceId, 'name', 'prepend');
return $this->success($res);
} catch (\Throwable $e) {
return $this->error($e->getMessage());
}
}
}
+3 -7
View File
@@ -314,14 +314,10 @@ class CustomerController extends BaseController
if (Util::isEmpty($channel_id)) {
return $this->error(trans('channel_not_null'));
}
$country_id = Util::getParam($request, 'country_id', 0);
if (Util::isEmpty($country_id)) {
// return $this->error('country_id'.trans('not_null'));
$country_id = Util::getParam($request, 'country_id', 245);
if(empty($country_id)){
$country_id = 245;
}
// $car_type_id = Util::getParam($request, 'car_type_id');
// if (Util::isEmpty($car_type_id)) {
// return $this->error(trans('car_type_not_null'));
// }
$contacts = Util::getParam($request, 'contacts');
$email = Util::getParam($request, 'email');
$phone = Util::getParam($request, 'phone');
+156 -10
View File
@@ -3,11 +3,15 @@ namespace app\crm\controller;
use app\common\controller\BaseController;
use app\common\traits\crud\CrudControllerTrait;
use app\common\service\StockOrdersService;
use app\crm\service\StockOrdersService;
use support\Request;
class StockOrdersController extends BaseController
{
protected $noNeedLogin = ['print'];
protected $noNeedHeaderData = ['print'];
use CrudControllerTrait;
protected $service;
@@ -23,20 +27,162 @@ class StockOrdersController extends BaseController
* @param array $data
* @return array
*/
// protected function indexFormat($data)
// {
// if (isset($data['created_at']))
// $data['created_at'] = date('Y-m-d H:i:s', $data['created_at']);
// if (isset($data['updated_at']))
// $data['updated_at'] = date('Y-m-d H:i:s', $data['updated_at']);
// return $data;
// }
protected function indexFormat($data)
{
if (isset($data['created_at'])) $data['created_at'] = date('Y-m-d H:i:s', $data['created_at']);
if (isset($data['updated_at'])) $data['updated_at'] = date('Y-m-d H:i:s', $data['updated_at']);
if (isset($data['eta_ship_date'])) $data['eta_ship_date'] = date('Y-m-d', $data['eta_ship_date']);
if (isset($data['biz_date'])) $data['biz_date'] = date('Y-m-d', $data['biz_date']);
if (isset($data['latest_arrival_date'])) $data['latest_arrival_date'] = date('Y-m-d', $data['latest_arrival_date']);
if (isset($data['id'])) {
$appro = new \app\common\utils\approval\service\ApprovalFlowService();
$appro = $appro->getBriefStatusByBiz($data['id'], null, 'name');
$map = ['processing'=>'审批中','pending'=>'审批中','approved'=>'已通过','rejected'=>'已拒绝','revoked'=>'已撤回'];
if ($appro) {
$data['appro_status'] = [
'status' => $appro['status'],
'status_desc' => $map[$appro['status']],
'name' => isset($appro['step'])?$appro['step']['name']:''
];
}
}
return $data;
}
protected function showFormat($data)
{
if (isset($data['created_at'])) $data['created_at'] = date('Y-m-d H:i:s', $data['created_at']);
if (isset($data['updated_at'])) $data['updated_at'] = date('Y-m-d H:i:s', $data['updated_at']);
if (isset($data['eta_ship_date'])) $data['eta_ship_date'] = date('Y-m-d', $data['eta_ship_date']);
if (isset($data['biz_date']) && $data['biz_date'] > 0) $data['biz_date'] = date('Y-m-d', $data['biz_date']);
if (isset($data['latest_arrival_date']) && $data['latest_arrival_date'] > 0) $data['latest_arrival_date'] = date('Y-m-d', $data['latest_arrival_date']);
if (isset($data['pick_up_date']) && $data['pick_up_date'] > 0) $data['pick_up_date'] = date('Y-m-d', $data['pick_up_date']);
if (isset($data['collect_all_date']) && $data['collect_all_date'] > 0) $data['collect_all_date'] = date('Y-m-d', $data['collect_all_date']);
if (!empty($data['contract'])) {
$data['contract_url'] = (new \app\common\service\OssService)->generatePrivateUrl($data['contract']);
}
if (count($data['moneyValues']) > 0) {
foreach ($data['moneyValues'] as $mvs) {
$data[$mvs['field']] = [
'exchange_rate' => $mvs['exchange_rate'],
'amount' => $mvs['amount'],
'currency' => $mvs['currency'],
'id' => $mvs['id']
];
}
}
foreach ($data['items'] as &$item) {
foreach ($item['moneyValues'] as $mv) {
$item[$mv['field']] = [
'exchange_rate' => $mv['exchange_rate'],
'amount' => $mv['amount'],
'currency' => $mv['currency'],
'id' => $mv['id']
];
}
unset($item['moneyValues']);
foreach ($item['options'] as $opt) {
$item['modify_list'][] = [
"id" => $opt['id'],
"order_item_id" => $opt['order_item_id'],
"option_id" => $opt['option_id'],
"option_name_snapshot" => $opt['option_name_snapshot'],
"option_price" => $opt['option_price']
];
}
unset($item['options']);
}
unset($item);
foreach ($data['accounts'] as $acc) {
$data['account_list'][] = [
"id" => $acc['id'],
"order_id" => $acc['order_id'],
"seq_no" => $acc['seq_no'],
"remit_country_id" => $acc['remit_country_id'],
"remit_country_name_snapshot" => $acc['remit_country_name_snapshot'],
"remit_date" => date('Y-m-d', $acc['remit_date']),
"currency_code" => $acc['currency_code'],
"currency_name_snapshot" => $acc['currency_name_snapshot'],
"account_id" => $acc['account_id'],
"account_name_snapshot" => $acc['account_name_snapshot'],
"remitter_name" => $acc['remitter_name'],
"amount" => $acc['amount']
];
}
unset($data['accounts']);
$data['veh_list'] = $data['items'];
unset($data['items']);
return $data;
}
protected function printFormat($data)
{
$data['file_url_full'] = (!empty($data['file_url'])) ? (new \app\common\service\OssService)->generatePrivateUrl($data['file_url']) : '';
return $data;
}
/**
* 获取打印数据
*/
public function print(Request $request)
{
try {
$raw = $this->service->servicePrint($request);
$data = $this->transformData($raw, method_exists($this, 'printFormat')
? [$this, 'printFormat']
: null);
return $this->success($data);
} catch (\Throwable $e) {
return $this->error($e->getMessage());
}
}
public function index(Request $request)
public function detail(Request $request)
{
try {
$raw = $this->service->serviceDetail($request);
$data = $this->transformData($raw, method_exists($this, 'detailFormat')
? [$this, 'detailFormat']
: null);
return $this->success($data);
} catch (\Throwable $e) {
return $this->error($e->getMessage());
}
}
protected function detailFormat($data)
{
// if (!isset($data['profit_total'])) $data['profit_total'] = 0.00;
return $data;
}
/**
* 启动审批
*/
public function approval(Request $request)
{
try {
$raw = $this->service->serviceApproval($request);
$data = $this->transformData($raw, method_exists($this, 'approvalFormat')
? [$this, 'approvalFormat']
: null);
return $this->success($data);
} catch (\Throwable $e) {
return $this->error($e->getMessage());
}
}
public function desc(Request $request)
{
$data = [
+17
View File
@@ -0,0 +1,17 @@
<?php
namespace app\crm\logic;
use app\common\traits\crud\CrudLogicTrait;
use app\crm\model\ApprovalFlows;
class ApprovalFlowsLogic
{
use CrudLogicTrait;
/**
* 指定当前要操作的模型
*/
protected string $modelClass = ApprovalFlows::class;
}
+17
View File
@@ -0,0 +1,17 @@
<?php
namespace app\crm\logic;
use app\common\traits\crud\CrudLogicTrait;
use app\crm\model\ApprovalInstances;
class ApprovalInstancesLogic
{
use CrudLogicTrait;
/**
* 指定当前要操作的模型
*/
protected string $modelClass = ApprovalInstances::class;
}
+17
View File
@@ -0,0 +1,17 @@
<?php
namespace app\crm\logic;
use app\common\traits\crud\CrudLogicTrait;
use app\crm\model\ApprovalRecords;
class ApprovalRecordsLogic
{
use CrudLogicTrait;
/**
* 指定当前要操作的模型
*/
protected string $modelClass = ApprovalRecords::class;
}
+54
View File
@@ -0,0 +1,54 @@
<?php
namespace app\crm\model;
use support\Model;
class ApprovalFlows extends Model
{
/**
* The connection name for the model.
*
* @var string|null
*/
protected $connection = 'mysql';
/**
* The table associated with the model.
*
* @var string
*/
protected $table = 'approval_flows';
/**
* 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', 'name', 'type', 'steps', 'is_active', 'timeout_hours', 'timeout_action', 'created_at', 'updated_at'];
/**
* 不允许自动填充模型字段
*/
protected $guarded = [];
protected $casts = [
'steps' => 'array',
];
}
+107
View File
@@ -0,0 +1,107 @@
<?php
namespace app\crm\model;
use app\common\model\StockOrders;
use app\common\model\User;
use support\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
class ApprovalInstances extends Model
{
/**
* The connection name for the model.
*
* @var string|null
*/
protected $connection = 'mysql';
/**
* The table associated with the model.
*
* @var string
*/
protected $table = 'approval_instances';
/**
* 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', 'flow_type', 'biz_id', 'biz_type', 'form_data', 'initiator_id', 'status', 'current_step', 'finished_at', 'deadline_at', 'created_at', 'updated_at'];
/**
* 不允许自动填充模型字段
*/
protected $guarded = [];
protected $fillable = [
'flow_type',
'biz_id',
'biz_type',
'form_data',
'initiator_id',
'status',
'current_step',
'finished_at',
'deadline_at',
];
protected $casts = [
'form_data' => 'array', // 自动转换 JSON 字段为数组
'finished_at' => 'datetime',
'deadline_at' => 'datetime',
'created_at' => 'datetime',
'updated_at' => 'datetime',
];
/**
* 一个审批实例可以有多个审批记录
*/
public function approvalRecords(): HasMany
{
return $this->hasMany(ApprovalRecords::class, 'instance_id');
}
/**
* 获取当前步骤的所有审批记录(pending 状态)
*/
public function currentStepRecords()
{
return $this->approvalRecords()
->where('step_index', $this->current_step)
->where('status', 'pending');
}
public function flows()
{
return $this->belongsTo(ApprovalFlows::class, 'flow_type', 'type');
}
public function initiator()
{
return $this->belongsTo(User::class, 'initiator_id', 'id');
}
public function bizInfo()
{
return $this->hasOne(StockOrders::class, 'id', 'biz_id');
}
}
+84
View File
@@ -0,0 +1,84 @@
<?php
namespace app\crm\model;
use app\common\model\User;
use support\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class ApprovalRecords extends Model
{
/**
* The connection name for the model.
*
* @var string|null
*/
protected $connection = 'mysql';
/**
* The table associated with the model.
*
* @var string
*/
protected $table = 'approval_records';
/**
* 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', 'instance_id', 'approver_id', 'step_index', 'action', 'status', 'comment', 'approved_at', 'created_at', 'updated_at'];
/**
* 不允许自动填充模型字段
*/
protected $guarded = [];
protected $fillable = [
'instance_id',
'approver_id',
'step_index',
'action',
'status',
'comment',
'approved_at',
];
protected $casts = [
'approved_at' => 'datetime',
'created_at' => 'datetime',
'updated_at' => 'datetime',
];
/**
* 一条审批记录属于一个审批实例
*/
public function approvalInstance(): BelongsTo
{
return $this->belongsTo(ApprovalInstances::class, 'instance_id');
}
/**
* 关联审批人
*/
public function approver()
{
return $this->belongsTo(User::class, 'approver_id');
}
}
+31
View File
@@ -0,0 +1,31 @@
<?php
namespace app\crm\service;
use app\crm\validate\ApprovalFlowsValidate;
use app\common\traits\crud\CrudServiceTrait;
use app\crm\model\ApprovalFlows;
class ApprovalFlowsService
{
use CrudServiceTrait;
/**
* 指定当前要操作的模型
*/
protected string $modelClass = ApprovalFlows::class;
/**
* 验证器
*/
protected $validator = ApprovalFlowsValidate::class;
/**
* 获取验证器
*/
public function getValidator()
{
return new $this->validator;
}
}
@@ -0,0 +1,31 @@
<?php
namespace app\crm\service;
use app\crm\validate\ApprovalInstancesValidate;
use app\common\traits\crud\CrudServiceTrait;
use app\crm\model\ApprovalInstances;
class ApprovalInstancesService
{
use CrudServiceTrait;
/**
* 指定当前要操作的模型
*/
protected string $modelClass = ApprovalInstances::class;
/**
* 验证器
*/
protected $validator = ApprovalInstancesValidate::class;
/**
* 获取验证器
*/
public function getValidator()
{
return new $this->validator;
}
}
@@ -0,0 +1,91 @@
<?php
namespace app\crm\service;
use app\crm\validate\ApprovalRecordsValidate;
use app\common\traits\crud\CrudServiceTrait;
use app\crm\model\ApprovalRecords;
use support\Request;
class ApprovalRecordsService
{
use CrudServiceTrait;
/**
* 指定当前要操作的模型
*/
protected string $modelClass = ApprovalRecords::class;
/**
* 验证器
*/
protected $validator = ApprovalRecordsValidate::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';
$config['with'] = [
'approvalInstance',
'approver',
'approvalInstance.flows',
'approvalInstance.initiator',
'approvalInstance.bizInfo'
];
$config['dataLimit']['type'] = 'relation';
$config['dataLimit']['userId'] = $request->user->uid;
$config['dataLimit']['userField'] = 'approver_id';
break;
case 'show':
$config['where']['id'] = $p['id'];
break;
case 'store':
break;
case 'update':
$config['where']['id'] = $p['id'];
break;
case 'destroy':
$config['where']['id'] = $p['id'];
break;
case 'logs':
$config['where']['instance_id'] = $p['instance_id'];
$config['order']['id'] = 'desc';
break;
}
return $config;
}
public function serviceApprove(Request $request)
{
$param = $request->all();
$appro = new \app\common\utils\approval\service\ApprovalFlowService();
return $appro->approve($param['instance_id'], $request->user->uid, $param['action'], $param['comment']);
}
public function serviceLogs(Request $request)
{
$config = $this->buildQueryConfig($request, 'logs');
$item = $this->logicListAll($config);
return $this->postProcess('Logs', $item);
}
}
+742
View File
@@ -0,0 +1,742 @@
<?php
namespace app\crm\service;
use app\common\model\StockOrderAccounts;
use app\common\model\StockOrderItemOptions;
use app\common\model\StockOrderItems;
use app\common\validate\StockOrdersValidate;
use app\common\traits\crud\CrudServiceTrait;
use app\common\model\StockOrders;
use app\common\model\SysCurrency;
use support\Request;
use app\common\model\User;
use support\Db;
use support\Model;
use app\admin\model\ApprovalInstances;
use app\admin\model\ApprovalRecords;
class StockOrdersService
{
use CrudServiceTrait;
/**
* 指定当前要操作的模型
*/
protected string $modelClass = StockOrders::class;
/**
* 验证器
*/
protected $validator = StockOrdersValidate::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':
// ====== ====== 筛选 ====== ======
// 业务id
if (isset($p['salesperson_id']) && $p['salesperson_id']) {
$config['where']['salesperson_id'] = $p['salesperson_id'];
}
// 初、终版
$config['where']['is_final'] = $p['is_final'] ?? 0;
// 关键字
if (isset($p['keyword']) && $p['keyword']) {
$k = '%' . $p['keyword'] . '%';
foreach (['pi_no', 'order_no', 'customer_snapshot'] as $col) {
$config['orWhere'][] = [$col, 'like', $k];
}
}
$config['order']['id'] = 'desc';
$config['where']['salesperson_id'] = $request->user->uid;
break;
case 'print':
case 'approval':
$config['where']['id'] = $p['id'];
break;
case 'detail':
case 'show':
$config['where']['id'] = $p['id'];
$config['with'][] = 'items';
$config['with'][] = 'items.options';
$config['with'][] = 'items.moneyValues';
$config['with'][] = 'accounts';
$config['with'][] = 'moneyValues';
$config['with'][] = 'salesperson';
break;
case 'store':
break;
case 'update':
$config['where']['id'] = $p['id'];
break;
case 'destroy':
$config['where']['id'] = $p['id'];
break;
}
return $config;
}
public function beforeServiceStore($data, Request $request)
{
$userId = $request->user->uid;
$data['order_no'] = generateStockNo();
$data['biz_date'] = strtotime($data['biz_date']);
$data['eta_ship_date'] = strtotime($data['eta_ship_date']);
$data['latest_arrival_date'] = strtotime($data['latest_arrival_date']);
$data['salesperson_id'] = $userId;
$data['salesperson_snapshot'] = User::where('id', $userId)->value('name') ?? '';
$data['approval_status'] = $data['approval_status'] == 1 ? 1 : 0;
// if ($data['is_final'] == 1) {
$data['pick_up_date'] = $data['pick_up_date'] ? strtotime($data['pick_up_date']) : 0;
$data['collect_all_date'] = $data['collect_all_date'] ? strtotime($data['collect_all_date']) : 0;
// }
return $data;
}
protected function afterServiceStore($res)
{
// 处理原单
if ($res->is_final == 1 && $res->object_id > 0) {
$order = StockOrders::find($res->object_id);
$order->is_use = 1;
$order->object_id = $res->id;
if (!$order->save()) {
flg('备货单【'.$order->order_no.'】创建终版成功,但更新原单失败');
}
}
// 启动审批
if ($res->approval_status == 1) {
$this->startApproval($res);
}
return $res;
}
/**
* 启动审批
*/
public function startApproval($item)
{
// 启动审批
$appro = new \app\common\utils\approval\service\ApprovalFlowService();
$appro->start('stock_order_flow', 'stock_orders', $item->id, $item->toArray(), $item->salesperson_id);
// 异步生成pdf文件
\Webman\RedisQueue\Redis::send('stock-order-pdf', ['id'=>$item->id]);
}
/**
* 创建PDF文件
*/
public function createdPdfFile($id)
{
$config = [];
$config['model'] = $this->modelClass;
$config['where']['id'] = $id;
$config['with'][] = 'items';
$config['with'][] = 'items.options';
$config['with'][] = 'items.moneyValues';
$config['with'][] = 'accounts';
$config['with'][] = 'moneyValues';
$config['with'][] = 'salesperson';
$item = $this->logicShow($config);
$pdfData = $this->pdfData($item);
// 渲染模板为最终HTML
$tplPath = app_path('common/utils/pdf/template/stockOrder.stub');
$html = \app\common\utils\pdf\service\PdfHelper::render($tplPath, ['data' => $pdfData]);
// 生成PDF
$driver = new \app\common\utils\pdf\service\MpdfDriver();
$pdfBin = $driver->htmlToPdf($html);
// 存储
$dir = public_path('pdf').'/';
$fileName = ($pdfData['order_no'] ?? date('YmdHis')) . '.pdf';
// 本地保存
\app\common\utils\pdf\service\PdfStorage::saveBinary(
$pdfBin,
$dir,
$fileName
);
// 上传云端
$param = [
'path' => $dir . $fileName,
'scene' => 'stock_orders',
'category' => 'pdf',
'original_name' => $fileName
];
$up = new \app\common\service\UploadService();
$res = $up->uploadLocalFile($param);
@unlink($dir . $fileName);
// 更新文件地址
$item->file_url = $res['file_name'];
$item->save();
return $res;
}
public function pdfData($item)
{
$vehList = [];
$index = 0;
$totalProfit = 0; // 总利润
// $priceFields = [
// 'official_price', // '官方指导价',
// 'discount_price', // '优惠',
// 'listing_price', // '二手挂牌价价',
// 'reduction_price', // '优惠',
// 'handling_fee', // '手续费',
// 'retrofit_fee', // '改装费',
// 'fob_transport_fee', // 'FOB运输费',
// 'port_misc_fee', // '港杂',
// 'ocean_freight', // '海运费',
// 'unit_price', // '单价',
// 'total_price', // '总价',
// // 'profit', // '利润'
// ];
foreach ($item->items as $k => $v) {
$currencyInfo = SysCurrency::where('currency_code', $item->settlement_currency)->first(['currency_symbol', 'currency_name'])->toArray();
// 存在则数量加1
if (count($vehList) > 0 && isset($vehList[$v->unique_id])) {
$vehList[$v->unique_id]['num'] += 1;
$vehList[$v->unique_id]['total_price']['currency_info'] = $currencyInfo['currency_symbol'].number_format($vehList[$v->unique_id]['total_price']['amount']*$vehList[$v->unique_id]['num'], 2, '.', ',');
$vehList[$v->unique_id]['profit']['currency_info'] = $currencyInfo['currency_symbol'].number_format($vehList[$v->unique_id]['profit']['amount']*$vehList[$v->unique_id]['num'], 2, '.', ',');
continue;
}
$index += 1;
// 处理金额 (每次循环重新初始化)
$mv = [];
foreach ($v->moneyValues as $money) {
$mv[$money->field] = [
'currency' => $money->currency,
'amount' => $money->amount,
'rate' => $money->exchange_rate,
'currency_info' => SysCurrency::where('currency_code', $money->currency)->value('currency_symbol').number_format($money->amount, 2, '.', ',')
];
}
// 处理改装 (每次循环重新初始化)
$mfy = '';
foreach ($v->options as $my) {
$mfy .= $my->option_name_snapshot.' ';
}
// 计算单项利润并累加到总利润
$itemProfit = $this->calculateProfit($mv, $item);
$totalProfit += $itemProfit;
// 处理结果集
$vehListItem = [
'index' => $index,
'model_snapshot' => $v->model_snapshot,
'color_interior_snapshot' => $v->color_interior_snapshot,
'color_exterior_snapshot' => $v->color_exterior_snapshot,
'num' => 1,
'modify' => $mfy,
'mileage' => $v->mileage
];
$vehList[$v->unique_id] = $vehListItem + $mv;
}
$mvs = [];
foreach ($item->moneyValues as $moneys) {
$mvs[$moneys->field] = [
'currency' => $moneys->currency,
'amount' => $moneys->amount,
'rate' => $moneys->exchange_rate,
'currency_info' => SysCurrency::where('currency_code', $moneys->currency)->value('currency_symbol').number_format($moneys->amount, 2, '.', ','),
];
}
$accList = [];
foreach ($item->accounts as $k => $v) {
$accList[] = [
'index' => '收款'.($k + 1),
'remit_country_name_snapshot' => $v->remit_country_name_snapshot,
'remit_date' => date('Y-m-d', $v->remit_date),
'currency_name_snapshot' => $v->currency_name_snapshot,
'account_name_snapshot' => $v->account_name_snapshot,
'remitter_name' => $v->remitter_name,
'amount' => $v->amount,
];
}
$veh = [
'order_no' => $item->order_no,
'salesperson_snapshot' => $item->salesperson_snapshot,
'sign_img_url' => (!empty($item->salesperson->sign_img)) ?
(new \app\common\service\OssService)->generatePrivateUrl($item->salesperson->sign_img) : '',
'created_at' => date('Y-m-d', strtotime($item->created_at)),
'pi_no' => $item->pi_no,
'biz_date' => date('Y-m-d', $item->biz_date),
'clearance_country_snapshot' => $item->clearance_country_snapshot,
'customer_source_snapshot' => $item->customer_source_snapshot,
'customer_snapshot' => $item->customer_snapshot,
'customer_country_snapshot' => $item->customer_country_snapshot,
// 订单项
'veh_list' => array_values($vehList), // 重新索引数组
'contract_exchange_rate' => $item->contract_exchange_rate,
'cif_total_amount' => $mvs['cif_total_amount'],
'profit_total' => $mvs['profit_total'],
'declarant_name' => $item->declarant_name,
'eta_ship_date' => date('Y-m-d', $item->eta_ship_date),
'shipping_method_snapshot' => $item->shipping_method_snapshot,
'domestic_port_snapshot' => $item->domestic_port_snapshot,
'trade_term_snapshot' => $item->trade_term_snapshot,
'clearance_port_snapshot' => $item->clearance_port_snapshot,
'latest_arrival_date' => date('Y-m-d', $item->latest_arrival_date),
'pick_up_date' => date('Y-m-d', $item->pick_up_date),
'collect_all_date' => date('Y-m-d', $item->collect_all_date),
'is_final' => $item->is_final,
// 账户
'account_list' => $accList,
'acc_index' => count($accList)+1,
'currency_symbol' => $item->settlement_currency,
'sign_imgs' => $this->queryAppro($item->id),
'contract_url' => (!empty($item->contract)) ? (new \app\common\service\OssService)->generatePrivateUrl($item->contract) : ''
];
return $veh;
}
public function queryAppro($bizId)
{
$instances = ApprovalInstances::where('biz_id', $bizId)->where('biz_type', 'stock_orders')->first();
if (!$instances) {
return [];
}
$records = ApprovalRecords::where('instance_id', $instances->id)->where('action', 'approve')->orderBy('step_index', 'asc')->pluck('approver_id')->toArray();
if (count($records) == 0) {
return [];
}
$signImgs = [];
foreach ($records as $k => $v) {
$signImg = User::where('id', $v)->value('sign_img');
$signImgs[$k] = (!empty($signImg))?(new \app\common\service\OssService)->generatePrivateUrl($signImg):'';
}
return $signImgs;
}
/**
* 打印数据
*/
public function servicePrint(Request $request)
{
$config = $this->buildQueryConfig($request, 'show');
$item = $this->logicShow($config);
return $this->postProcess('Print', $item);
}
public function serviceDetail(Request $request)
{
$config = $this->buildQueryConfig($request, 'detail');
$item = $this->logicShow($config);
$pdfData = $this->pdfData($item);
return $this->postProcess('Detail', $pdfData);
}
/**
* 审批
*/
public function serviceApproval(Request $request)
{
$config = $this->buildQueryConfig($request, 'approval');
$this->logicUpdateBy($config, [
'approval_status' => 1
]);
$item = $this->logicShow($config);
$this->startApproval($item);
return $this->postProcess('Approval', $item);
}
/**
* 创建
*
* @param Request $request
* @param array $data
* @return mixed
*/
public function serviceStore(Request $request, array $data = [])
{
// 关联表较多,增加事务处理
return Db::transaction(function () use ($request, $data) {
$data = $this->preProcess('Store', $data, $request);
$dl = $this->getDataLimitFromRequest($request);
$created = $this->logicCreate($data, $dl);
// 处理车辆和账户信息
$this->processOrderItems($created, $data);
return $this->postProcess('Store', $created);
});
}
protected function onlyFillableData(Model|string $model, array $data, array $extraWhitelist = []): array
{
// 兼容传类名或实例
$m = \is_string($model) ? new $model : $model;
// 1) 优先使用 fillable
$keys = $m->getFillable();
// 2) 若没配置 fillable(常见:用 guarded),回退到完整表字段,避免误伤
if (empty($keys)) {
$keys = $m->getConnection()
->getSchemaBuilder()
->getColumnListing($m->getTable());
}
// 3) 允许额外白名单(如虚拟字段、JSON映射字段)
if ($extraWhitelist) {
$keys = array_merge($keys, $extraWhitelist);
}
return array_intersect_key($data, array_flip($keys));
}
/**
* 更新前处理
*
* @param array $data
* @param Request $request
* @return array
*/
public function beforeServiceUpdate($data, Request $request)
{
$data['biz_date'] = strtotime($data['biz_date']);
$data['eta_ship_date'] = strtotime($data['eta_ship_date']);
$data['latest_arrival_date'] = strtotime($data['latest_arrival_date']);
$data['approval_status'] = $data['approval_status'] == 1 ? 1 : 0;
$data['pick_up_date'] = $data['pick_up_date'] ? strtotime($data['pick_up_date']) : 0;
$data['collect_all_date'] = $data['collect_all_date'] ? strtotime($data['collect_all_date']) : 0;
return $data;
}
/**
* 更新后处理
*
* @param mixed $res
* @return mixed
*/
protected function afterServiceUpdate($res)
{
// 启动审批
if ($res->approval_status == 1) {
$this->startApproval($res);
}
return $res;
}
/**
* 更新
*
* @param Request $request
* @param array $data
* @return mixed
*/
public function serviceUpdate(Request $request, array $data = [])
{
// 关联表较多,增加事务处理
return Db::transaction(function () use ($request, $data) {
$data = $this->preProcess('Update', $data, $request);
$config = $this->buildQueryConfig($request, 'update');
// 过滤掉非库字段
$dbData = $this->onlyFillableData($config['model'], $data);
// 更新主表
$updated = $this->logicUpdateBy($config, $dbData);
// 获取更新后的数据
$order = $this->logicShow($config);
// 处理车辆和账户信息
$this->processOrderItems($order, $data, true);
return $this->postProcess('Update', $order);
});
}
/**
* 处理订单项(车辆和账户)
* @param StockOrders $order 订单对象
* @param array $data 数据
* @param bool $isUpdate 是否为更新操作
*/
private function processOrderItems($order, array $data, bool $isUpdate = false): void
{
// 如果是更新操作,先删除原有的关联数据
if ($isUpdate) {
$this->clearExistingOrderItems($order->id);
}
// 处理车辆项
$this->processVehicleItems($order, $data['veh_list'] ?? []);
// 处理账户项
$this->processAccountItems($order, $data['account_list'] ?? []);
}
/**
* (更新时)清除已存在的订单项
* @param int $orderId 订单ID
*/
private function clearExistingOrderItems(int $orderId): void
{
// 删除车辆改装项
StockOrderItemOptions::whereHas('item', function ($query) use ($orderId) {
$query->where('order_id', $orderId);
})->delete();
// 删除车辆项及其相关金额记录
$orderItems = StockOrderItems::where('order_id', $orderId)->get();
foreach ($orderItems as $item) {
// 删除车辆项的金额记录
Db::table('veh_money_value')
->where('object_scene', 'stock_order_items')
->where('object_id', $item->id)
->delete();
}
// 删除车辆项
StockOrderItems::where('order_id', $orderId)->delete();
// 删除订单本身的金额记录
Db::table('veh_money_value')
->where('object_scene', 'stock_orders')
->where('object_id', $orderId)
->delete();
// 删除账户项
StockOrderAccounts::where('order_id', $orderId)->delete();
}
/**
* 处理车辆项
*/
private function processVehicleItems($order, array $vehList): void
{
$cifTotalAmount = 0.00;
// $cifTotalAmountRate = 0;
// $cifTotalAmountCurrency = 'CNY';
$profitTotalAmount = 0.00;
foreach ($vehList as $veh) {
$veh['order_id'] = $order->id;
$veh['unique_id'] = $this->generateVehicleUniqueId($veh);
$item = StockOrderItems::create($veh);
// 处理金额信息
$this->processVehiclePrices($item, $veh);
// 计算利润
$profit = $this->calculateProfit($veh, $order);
$item->addMoney('profit', $order->settlement_currency, $profit, $order->contract_exchange_rate, '利润');
// 累计到岸价
$cifTotalAmount += $veh['total_price']['amount'] ?? 0;
$profitTotalAmount += $profit;
// if (isset($veh['retrofit_fee']['rate'])) {
// $cifTotalAmountRate = $veh['retrofit_fee']['rate'];
// }
// if (isset($veh['retrofit_fee']['currency'])) {
// $cifTotalAmountCurrency = $veh['retrofit_fee']['currency'];
// }
// 处理改装项
$this->processModificationItems($item->id, $veh['modify_list'] ?? []);
}
// 保存合计到岸价
$order->addMoney('cif_total_amount', $order->settlement_currency, $cifTotalAmount, $order->contract_exchange_rate, '合计到岸价');
// 利润
$order->addMoney('profit_total', $order->settlement_currency, $profitTotalAmount, $order->contract_exchange_rate, '总利润');
}
/**
* 生成车辆唯一标识
*/
private function generateVehicleUniqueId(array $veh): string
{
$uniqueIdStr = "";
$uniqueIdStr .= $veh['sku_id'] ?? '';
$uniqueIdStr .= $veh['model_snapshot'] ?? '';
$uniqueIdStr .= $veh['color_interior_code'] ?? '';
$uniqueIdStr .= $veh['color_exterior_code'] ?? '';
$uniqueIdStr .= $veh['mileage'] ?? '';
$uniqueIdStr .= json_encode($veh['modify_list'] ?? []);
return md5($uniqueIdStr);
}
/**
* 处理车辆价格信息
*/
private function processVehiclePrices($item, array $veh): void
{
// 基础费用
$priceFields = [
'unit_price' => '单价',
'total_price' => '总价',
'handling_fee' => '手续费',
'fob_transport_fee' => 'FOB运输费',
'port_misc_fee' => '港杂',
'retrofit_fee' => '改装费',
'ocean_freight' => '海运费'
];
foreach ($priceFields as $field => $description) {
if (!empty($veh[$field]['amount'])) {
$item->addMoney(
$field,
$veh[$field]['currency'],
$veh[$field]['amount'],
$veh[$field]['rate'],
$description
);
}
}
// 根据里程处理新车或二手车价格
if (!empty($veh['mileage']) && $veh['mileage'] > 0) {
// 二手车
if (!empty($veh['listing_price']['amount'])) {
$item->addMoney(
'listing_price',
$veh['listing_price']['currency'],
$veh['listing_price']['amount'],
$veh['listing_price']['rate'],
'二手挂牌价'
);
}
if (!empty($veh['reduction_price']['amount'])) {
$item->addMoney(
'reduction_price',
$veh['reduction_price']['currency'],
$veh['reduction_price']['amount'],
$veh['reduction_price']['rate'],
'二手减价'
);
}
} else {
// 新车
if (!empty($veh['official_price']['amount'])) {
$item->addMoney(
'official_price',
$veh['official_price']['currency'],
$veh['official_price']['amount'],
$veh['official_price']['rate'],
'官方指导价'
);
}
if (!empty($veh['discount_price']['amount'])) {
$item->addMoney(
'discount_price',
$veh['discount_price']['currency'],
$veh['discount_price']['amount'],
$veh['discount_price']['rate'],
'优惠价'
);
}
}
}
/**
* 计算利润
*/
private function calculateProfit(array $veh, $order): float
{
$unitPrice = (float)($veh['unit_price']['amount'] ?? 0);
if (!empty($veh['mileage']) && $veh['mileage'] > 0) {
// 二手车
$ttPrice = ($veh['listing_price']['amount'] ?? 0) / ($order->contract_exchange_rate ?: 1);
$ddPrice = $veh['reduction_price']['amount'] ?? 0;
} else {
// 新车
$ttPrice = ($veh['official_price']['amount'] ?? 0) / ($order->contract_exchange_rate ?: 1);
$ddPrice = $veh['discount_price']['amount'] ?? 0;
}
$handlingFee = (float)($veh['handling_fee']['amount'] ?? 0);
$fobTransportFee = (float)($veh['fob_transport_fee']['amount'] ?? 0);
$portMiscFee = (float)($veh['port_misc_fee']['amount'] ?? 0);
$retrofitFee = (float)($veh['retrofit_fee']['amount'] ?? 0);
$oceanFreight = (float)($veh['ocean_freight']['amount'] ?? 0);
return $unitPrice - $ttPrice - $ddPrice - $handlingFee - $fobTransportFee - $portMiscFee - $retrofitFee - $oceanFreight;
}
/**
* 处理改装项
*/
private function processModificationItems(int $orderItemId, array $modifyList): void
{
if (empty($modifyList)) {
return;
}
foreach ($modifyList as $modify) {
$modify['order_item_id'] = $orderItemId;
StockOrderItemOptions::create($modify);
}
}
/**
* 处理账户项
*/
private function processAccountItems($order, array $accountList): void
{
if (empty($accountList)) {
return;
}
foreach ($accountList as $account) {
$account['order_id'] = $order->id;
if (!empty($account['remit_date'])) {
$account['remit_date'] = strtotime($account['remit_date']);
}
StockOrderAccounts::create($account);
}
}
}
+444
View File
@@ -0,0 +1,444 @@
<?php
namespace app\crm\service;
use app\common\model\StockOrderAccounts;
use app\common\model\StockOrderItemOptions;
use app\common\model\StockOrderItems;
use app\common\validate\StockOrdersValidate;
use app\common\traits\crud\CrudServiceTrait;
use app\common\model\StockOrders;
use app\common\model\SysCurrency;
use support\Request;
use app\common\model\User;
use support\exception\BusinessException;
use support\Db;
class StockOrdersService_bak
{
use CrudServiceTrait;
/**
* 指定当前要操作的模型
*/
protected string $modelClass = StockOrders::class;
/**
* 验证器
*/
protected $validator = StockOrdersValidate::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':
// ====== ====== 筛选 ====== ======
// 业务id
if (isset($p['salesperson_id']) && $p['salesperson_id']) {
$config['where']['salesperson_id'] = $p['salesperson_id'];
}
// 初、终版
$config['where']['is_final'] = $p['is_final'] ?? 0;
// 关键字
if (isset($p['keyword']) && $p['keyword']) {
$k = '%' . $p['keyword'] . '%';
foreach (['pi_no', 'order_no', 'customer_snapshot'] as $col) {
$config['orWhere'][] = [$col, 'like', $k];
}
}
$config['order']['id'] = 'desc';
$config['where']['salesperson_id'] = $request->user->uid;
break;
case 'print':
$config['where']['id'] = $p['id'];
break;
case 'show':
$config['where']['id'] = $p['id'];
$config['with'][] = 'items';
$config['with'][] = 'items.options';
$config['with'][] = 'items.moneyValues';
$config['with'][] = 'accounts';
$config['with'][] = 'moneyValues';
$config['with'][] = 'salesperson';
break;
case 'store':
break;
case 'update':
$config['where']['id'] = $p['id'];
break;
case 'destroy':
$config['where']['id'] = $p['id'];
break;
}
return $config;
}
public function serviceStore(Request $request, array $data = [])
{
// 关联表较多,增加事务处理
return Db::transaction(function () use ($request, $data) {
$data = $this->preProcess('Store', $data, $request);
$dl = $this->getDataLimitFromRequest($request);
$created = $this->logicCreate($data, $dl); // 表1
// 车辆
$cifTotalAmount = 0;
$cifTotalAmountRate = 0;
$cifTotalAmountCurrency = 'CNY';
// $profit = 0;
foreach ($data['veh_list'] as $veh) {
$veh['order_id'] = $created->id;
// 唯一标识
$uniqueIdStr = "";
$uniqueIdStr .= $veh['sku_id'];
$uniqueIdStr .= $veh['model_snapshot'];
$uniqueIdStr .= $veh['color_interior_code'];
$uniqueIdStr .= $veh['color_exterior_code'];
$uniqueIdStr .= $veh['mileage'];
$uniqueIdStr .= json($veh['modify_list']);
$veh['unique_id'] = md5($uniqueIdStr);
$item = StockOrderItems::create($veh); // 表2
// 金额 // 表3
// $item->addMoney('official_price', $veh['official_price']['currency'], $veh['official_price']['amount'], $veh['official_price']['rate'], '官方指导价');
// $item->addMoney('discount_price', $veh['discount_price']['currency'], $veh['discount_price']['amount'], $veh['discount_price']['rate'], '优惠价');
if ($veh['unit_price']['amount']) {
$item->addMoney('unit_price', $veh['unit_price']['currency'], $veh['unit_price']['amount'], $veh['unit_price']['rate'], '单价');
}
if ($veh['total_price']['amount']) {
$item->addMoney('total_price', $veh['total_price']['currency'], $veh['total_price']['amount'], $veh['total_price']['rate'], '总价');
}
if ($veh['handling_fee']['amount']) {
$item->addMoney('handling_fee', $veh['handling_fee']['currency'], $veh['handling_fee']['amount'], $veh['handling_fee']['rate'], '手续费');
}
if ($veh['fob_transport_fee']['amount']) {
$item->addMoney('fob_transport_fee', $veh['fob_transport_fee']['currency'], $veh['fob_transport_fee']['amount'], $veh['fob_transport_fee']['rate'], 'FOB运输费');
}
if ($veh['port_misc_fee']['amount']) {
$item->addMoney('port_misc_fee', $veh['port_misc_fee']['currency'], $veh['port_misc_fee']['amount'], $veh['port_misc_fee']['rate'], '港杂');
}
if ($veh['retrofit_fee']['amount']) {
$item->addMoney('retrofit_fee', $veh['retrofit_fee']['currency'], $veh['retrofit_fee']['amount'], $veh['retrofit_fee']['rate'], '改装费');
}
if ($veh['ocean_freight']['amount']) {
$item->addMoney('ocean_freight', $veh['ocean_freight']['currency'], $veh['ocean_freight']['amount'], $veh['ocean_freight']['rate'], '海运费');
}
// 二手车 or 新车
if ($veh['mileage'] > 0) {
if ($veh['listing_price']['amount']) {
$item->addMoney('listing_price', $veh['listing_price']['currency'], $veh['listing_price']['amount'], $veh['listing_price']['rate'], '二手挂牌价');
}
if ($veh['reduction_price']['amount']) {
$item->addMoney('reduction_price', $veh['reduction_price']['currency'], $veh['reduction_price']['amount'], $veh['reduction_price']['rate'], '二手减价');
}
$ttPrice = ($veh['listing_price']['amount'] / $created->contract_exchange_rate);
$ddPrice = $veh['reduction_price']['amount'];
} else {
if ($veh['official_price']['amount']) {
$item->addMoney('official_price', $veh['official_price']['currency'], $veh['official_price']['amount'], $veh['official_price']['rate'], '官方指导价');
}
if ($veh['discount_price']['amount'] ) {
$item->addMoney('discount_price', $veh['discount_price']['currency'], $veh['discount_price']['amount'], $veh['discount_price']['rate'], '优惠价');
}
$ttPrice = ($veh['official_price']['amount'] / $created->contract_exchange_rate);
$ddPrice = $veh['discount_price']['amount'];
}
// 利润
$profit = (float)$veh['unit_price']['amount']
- (float)$ttPrice
- (float)$ddPrice
- (float)$veh['handling_fee']['amount']
- (float)$veh['fob_transport_fee']['amount']
- (float)$veh['port_misc_fee']['amount']
- (float)$veh['retrofit_fee']['amount']
- (float)$veh['ocean_freight']['amount'];
$item->addMoney('profit', $veh['unit_price']['currency'], $profit, $veh['unit_price']['rate'], '利润');
// cif_total_amount
$cifTotalAmount += $veh['total_price']['amount'];
$cifTotalAmountRate = $veh['retrofit_fee']['rate'];
$cifTotalAmountCurrency = $veh['retrofit_fee']['currency'];
// 改装
if (count($veh['modify_list']) > 0) {
foreach ($veh['modify_list'] as $modify) {
$modify['order_item_id'] = $item->id;
StockOrderItemOptions::create($modify); // 表4
}
}
}
// 计算&&插入 表1
$created->addMoney('cif_total_amount', $cifTotalAmountCurrency, $cifTotalAmount, $cifTotalAmountRate, '合计到岸价');
// 账户
if (count($data['account_list']) > 0) {
foreach ($data['account_list'] as $account) {
$account['order_id'] = $created->id;
$account['remit_date'] = strtotime($account['remit_date']);
StockOrderAccounts::create($account); // 表5
}
}
return $this->postProcess('Store', $created);
});
}
public function beforeServiceStore($data, Request $request)
{
$userId = $request->user->uid;
$data['order_no'] = generateStockNo();
$data['biz_date'] = strtotime($data['biz_date']);
$data['eta_ship_date'] = strtotime($data['eta_ship_date']);
$data['latest_arrival_date'] = strtotime($data['latest_arrival_date']);
$data['salesperson_id'] = $userId;
$data['salesperson_snapshot'] = User::where('id', $userId)->value('name') ?? '';
return $data;
}
protected function afterServiceStore($res)
{
// 处理原单
if ($res->is_final == 1 && $res->object_id > 0) {
$order = StockOrders::find($res->object_id);
$order->is_use = 1;
$order->object_id = $res->id;
if (!$order->save()) {
flg('备货单【'.$order->order_no.'】创建终版成功,但更新原单失败');
}
}
// 启动审批
$appro = new \app\common\utils\approval\service\ApprovalFlowService();
$result = $appro->start('stock_order_flow', 'stock_orders', $res->id, $res->toArray(), $res->salesperson_id);
var_dump($result);
// 异步生成pdf文件
\Webman\RedisQueue\Redis::send('stock-order-pdf', ['id'=>$res->id]);
return $res;
}
public function createdPdfFile($id)
{
$config = [];
$config['model'] = $this->modelClass;
$config['where']['id'] = $id;
$config['with'][] = 'items';
$config['with'][] = 'items.options';
$config['with'][] = 'items.moneyValues';
$config['with'][] = 'accounts';
$config['with'][] = 'moneyValues';
$config['with'][] = 'salesperson';
$item = $this->logicShow($config);
$pdfData = $this->pdfData($item);
// 渲染模板为最终HTML
$tplPath = app_path('common/utils/pdf/template/stockOrder.stub');
$html = \app\common\utils\pdf\service\PdfHelper::render($tplPath, ['data' => $pdfData]);
// 生成PDF
$driver = new \app\common\utils\pdf\service\MpdfDriver();
$pdfBin = $driver->htmlToPdf($html);
// 存储
$dir = public_path('pdf').'/';
$fileName = ($pdfData['order_no'] ?? date('YmdHis')) . '.pdf';
// 本地保存
\app\common\utils\pdf\service\PdfStorage::saveBinary(
$pdfBin,
$dir,
$fileName
);
// // 在线预览
// return new \support\Response(200, [
// 'Content-Type' => 'application/pdf',
// 'Content-Disposition' => 'inline; filename="stock-order.pdf"',
// ], $pdfBin);
// $url = [
// 'file_name' => $fileName,
// 'file_url' => getenv('MAIN_DOMAIN').'/pdf/'.$fileName
// ];
// 上传云端
$param = [
'path' => $dir . $fileName,
'scene' => 'stock_orders',
'category' => 'pdf',
'original_name' => $fileName
];
$up = new \app\common\service\UploadService();
$res = $up->uploadLocalFile($param);
@unlink($dir . $fileName);
// 更新文件地址
$item->file_url = $res['file_name'];
$item->save();
return $res;
}
public function servicePrint(Request $request)
{
$config = $this->buildQueryConfig($request, 'show');
$item = $this->logicShow($config);
return $this->postProcess('Print', $item);
}
public function pdfData($item)
{
$vehList = [];
$index = 0;
foreach ($item->items as $k => $v) {
// 存在则数量加1
if (count($vehList) > 0 && isset($vehList[$v->unique_id])) {
$vehList[$v->unique_id]['num'] += 1;
$vehList[$v->unique_id]['total_price'] += $vehList[$v->unique_id]['unit_price'];
$vehList[$v->unique_id]['total_price_text'] = $vehList[$v->unique_id]['total_price_symbol'].number_format($vehList[$v->unique_id]['total_price'], 2, '.', ',');
continue;
}
$index += 1;
// 处理金额
$mv = [];
foreach ($v->moneyValues as $money) {
$mv[$money->field] = [
'currency' => $money->currency,
'amount' => $money->amount,
'rate' => $money->exchange_rate,
'currency_symbol' => SysCurrency::where('currency_code', $money->currency)->value('currency_symbol'),
];
}
// 处理改装
$mfy = '';
foreach ($v->options as $my) {
$mfy .= $my->option_name_snapshot.' ';
}
// 处理结果集
$vehList[$v->unique_id] = [
'index' => $index,
'model_snapshot' => $v->model_snapshot,
'color_interior_snapshot' => $v->color_interior_snapshot,
'color_exterior_snapshot' => $v->color_exterior_snapshot,
'num' => 1,
'modify' => $mfy,
'mileage' => $v->mileage,
'unit_price' => $mv['unit_price']['amount'],
'total_price' => $mv['total_price']['amount'],
'unit_price_symbol' => $mv['unit_price']['currency_symbol'],
'total_price_symbol' => $mv['total_price']['currency_symbol'],
'unit_price_text' => $mv['unit_price']['currency_symbol'].number_format($mv['unit_price']['amount'], 2, '.', ','),
'total_price_text' => $mv['total_price']['currency_symbol'].number_format($mv['total_price']['amount'], 2, '.', ',')
];
// 根据里程处理 新车or二手车
if ($v->mileage > 0) {
if (isset($mv['reduction_price'])) {
$vehList[$v->unique_id]['reduction_price'] = $mv['reduction_price']['amount'];
$vehList[$v->unique_id]['reduction_price_symbol'] = $mv['reduction_price']['currency_symbol'];
$vehList[$v->unique_id]['reduction_price_text'] = $mv['reduction_price']['currency_symbol'].number_format($mv['reduction_price']['amount'], 2, '.', ',');
}
$vehList[$v->unique_id]['listing_price'] = $mv['listing_price']['amount'];
$vehList[$v->unique_id]['listing_price_symbol'] = $mv['listing_price']['currency_symbol'];
$vehList[$v->unique_id]['listing_price_text'] = $mv['listing_price']['currency_symbol'].number_format($mv['listing_price']['amount'], 2, '.', ',');
} else {
if (isset($mv['discount_price'])) {
$vehList[$v->unique_id]['discount_price'] = $mv['discount_price']['amount'];
$vehList[$v->unique_id]['discount_price_symbol'] = $mv['discount_price']['currency_symbol'];
$vehList[$v->unique_id]['discount_price_text'] = $mv['discount_price']['currency_symbol'].number_format($mv['discount_price']['amount'], 2, '.', ',');
}
$vehList[$v->unique_id]['official_price'] = $mv['official_price']['amount'];
$vehList[$v->unique_id]['official_price_symbol'] = $mv['official_price']['currency_symbol'];
$vehList[$v->unique_id]['official_price_text'] = $mv['official_price']['currency_symbol'].number_format($mv['official_price']['amount'], 2, '.', ',');
}
}
$accList = [];
$accIndex = 1;
foreach ($item->accounts as $k => $v) {
$accList[] = [
'index' => '收款'.$k + 1,
'remit_country_name_snapshot' => $v->remit_country_name_snapshot,
'remit_date' => date('Y-m-d', $v->remit_date),
'currency_name_snapshot' => $v->currency_name_snapshot,
'account_name_snapshot' => $v->account_name_snapshot,
'remitter_name' => $v->remitter_name,
'amount' => $v->amount,
];
$accIndex += 1;
}
$cifTotalAmount = 0.00;
if ($item->moneyValues) {
$cifTotalAmountSysboml = SysCurrency::where('currency_code', $item->moneyValues->currency)->value('currency_symbol');
$cifTotalAmount = $cifTotalAmountSysboml.sprintf("%.2f", $item->moneyValues->amount, $item->moneyValues->exchange_rate);
}
$veh = [
'order_no' => $item->order_no,
'salesperson_snapshot' => $item->salesperson_snapshot,
// 'sign_img' => $item->salesperson->sign_img,
'sign_img_url' => (!empty($item->salesperson->sign_img))?(new \app\common\service\OssService)->generatePrivateUrl($item->salesperson->sign_img):'',
'created_at' => date('Y-m-d', strtotime($item->created_at)),
'pi_no' => $item->pi_no,
'biz_date' => date('Y-m-d', $item->biz_date),
'clearance_country_snapshot' => $item->clearance_country_snapshot,
'customer_source_snapshot' => $item->customer_source_snapshot,
'customer_snapshot' => $item->customer_snapshot,
'customer_country_snapshot' => $item->customer_country_snapshot,
// 订单项
'veh_list' => $vehList,
'contract_exchange_rate' => $item->contract_exchange_rate,
'cif_total_amount' => $item->cif_total_amount,
'declarant_name' => $item->declarant_name,
'eta_ship_date' => date('Y-m-d', $item->eta_ship_date),
'shipping_method_snapshot' => $item->shipping_method_snapshot,
'domestic_port_snapshot' => $item->domestic_port_snapshot,
'trade_term_snapshot' => $item->trade_term_snapshot,
'clearance_port_snapshot' => $item->clearance_port_snapshot,
'latest_arrival_date' => date('Y-m-d', $item->latest_arrival_date),
// 账户
'account_list' => $accList,
'acc_index' => $accIndex,
'cif_total_amount' => $cifTotalAmount
];
return $veh;
}
}
@@ -0,0 +1,29 @@
<?php
namespace app\crm\validate;
use think\Validate;
class ApprovalFlowsValidate extends Validate
{
// 规则
protected $rule = [
];
// 提示
protected $message = [
];
// 场景
protected $scene = [
'store' => [
],
'update' => [
]
];
}
@@ -0,0 +1,29 @@
<?php
namespace app\crm\validate;
use think\Validate;
class ApprovalInstancesValidate extends Validate
{
// 规则
protected $rule = [
];
// 提示
protected $message = [
];
// 场景
protected $scene = [
'store' => [
],
'update' => [
]
];
}
@@ -0,0 +1,29 @@
<?php
namespace app\crm\validate;
use think\Validate;
class ApprovalRecordsValidate extends Validate
{
// 规则
protected $rule = [
];
// 提示
protected $message = [
];
// 场景
protected $scene = [
'store' => [
],
'update' => [
]
];
}
+8 -8
View File
@@ -29,26 +29,26 @@ class Emails
// });
if (getenv('ENVIRONMENT') === 'master') {
// 拉取新邮件 - 每3小时的第0分钟0秒执行
new Crontab('0 0 */3 * * *', function(){
// 拉取新邮件 - 每小时的第20分钟0秒执行
new Crontab('0 */20 * * * *', function(){
$email = new EmailsUtils();
$email->uids();
});
// 邮件详情 - 每3小时的第10分钟0秒执行
new Crontab('0 10 */3 * * *', function(){
// 邮件详情 - 每小时的第25分钟0秒执行
new Crontab('0 */25 * * * *', function(){
$email = new EmailsUtils();
$email->details();
});
// 邮件处理 - 每3小时的第20分钟0秒执行
new Crontab('0 20 */3 * * *', function(){
// 邮件处理 - 每小时的第30分钟0秒执行
new Crontab('0 */30 * * * *', function(){
$email = new EmailsUtils();
$email->process();
});
// 邮件分派 - 每3小时的第30分钟0秒执行
new Crontab('0 30 */3 * * *', function(){
// 邮件分派 - 每小时的第35分钟0秒执行
new Crontab('0 */35 * * * *', function(){
$email = new EmailsUtils();
$email->fenpai();
});
+51
View File
@@ -0,0 +1,51 @@
<?php
namespace app\queue\redis;
use support\exception\BusinessException;
use Webman\RedisQueue\Consumer;
class StockOrderPdf implements Consumer
{
// 要消费的队列名
public $queue = 'stock-order-pdf';
// 连接名,对应 plugin/webman/redis-queue/redis.php 里的连接`
public $connection = 'default';
// 消费
public function consume($data)
{
try {
$service = new \app\crm\service\StockOrdersService();
$service->createdPdfFile($data['id']);
return true;
} catch (\Throwable $th) {
var_dump('4 :'.$th->getFile()."|".$th->getLine()."|".$th->getMessage());
throw new BusinessException($th->getFile()."|".$th->getLine()."|".$th->getMessage());
}
}
// 消费失败回调
/*
$package = [
'id' => 1357277951, // 消息ID
'time' => 1709170510, // 消息时间
'delay' => 0, // 延迟时间
'attempts' => 2, // 消费次数
'queue' => 'send-mail', // 队列名
'data' => ['to' => 'tom@gmail.com', 'content' => 'hello'], // 消息内容
'max_attempts' => 5, // 最大重试次数
'error' => '错误信息' // 错误信息
]
*/
public function onConsumeFailure(\Throwable $e, $package)
{
echo "consume failure\n";
echo $e->getMessage() . "\n";
// 无需反序列化
var_export($package);
}
}
+2 -1
View File
@@ -46,7 +46,8 @@
"workerman/crontab": "^1.0",
"phpoffice/phpspreadsheet": "^4.5",
"phpmailer/phpmailer": "^6.10",
"webman/gateway-worker": "^1.0"
"webman/gateway-worker": "^1.0",
"mpdf/mpdf": "^8.2"
},
"suggest": {
"ext-event": "For better performance. "
Generated
+306 -1
View File
@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "7c70d8c8bda8a5098b8baed463db8839",
"content-hash": "68e74d2aef3a2f7f8163bfaf65431cb2",
"packages": [
{
"name": "aliyuncs/oss-sdk-php",
@@ -2890,6 +2890,239 @@
],
"time": "2024-11-12T12:43:37+00:00"
},
{
"name": "mpdf/mpdf",
"version": "v8.2.6",
"source": {
"type": "git",
"url": "https://github.com/mpdf/mpdf.git",
"reference": "dd30e3b01061cf8dfe65e7041ab4cc46d8ebdd44"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/mpdf/mpdf/zipball/dd30e3b01061cf8dfe65e7041ab4cc46d8ebdd44",
"reference": "dd30e3b01061cf8dfe65e7041ab4cc46d8ebdd44",
"shasum": ""
},
"require": {
"ext-gd": "*",
"ext-mbstring": "*",
"mpdf/psr-http-message-shim": "^1.0 || ^2.0",
"mpdf/psr-log-aware-trait": "^2.0 || ^3.0",
"myclabs/deep-copy": "^1.7",
"paragonie/random_compat": "^1.4|^2.0|^9.99.99",
"php": "^5.6 || ^7.0 || ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0",
"psr/http-message": "^1.0 || ^2.0",
"psr/log": "^1.0 || ^2.0 || ^3.0",
"setasign/fpdi": "^2.1"
},
"require-dev": {
"mockery/mockery": "^1.3.0",
"mpdf/qrcode": "^1.1.0",
"squizlabs/php_codesniffer": "^3.5.0",
"tracy/tracy": "~2.5",
"yoast/phpunit-polyfills": "^1.0"
},
"suggest": {
"ext-bcmath": "Needed for generation of some types of barcodes",
"ext-xml": "Needed mainly for SVG manipulation",
"ext-zlib": "Needed for compression of embedded resources, such as fonts"
},
"type": "library",
"autoload": {
"files": [
"src/functions.php"
],
"psr-4": {
"Mpdf\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"GPL-2.0-only"
],
"authors": [
{
"name": "Matěj Humpál",
"role": "Developer, maintainer"
},
{
"name": "Ian Back",
"role": "Developer (retired)"
}
],
"description": "PHP library generating PDF files from UTF-8 encoded HTML",
"homepage": "https://mpdf.github.io",
"keywords": [
"pdf",
"php",
"utf-8"
],
"support": {
"docs": "https://mpdf.github.io",
"issues": "https://github.com/mpdf/mpdf/issues",
"source": "https://github.com/mpdf/mpdf"
},
"funding": [
{
"url": "https://www.paypal.me/mpdf",
"type": "custom"
}
],
"time": "2025-08-18T08:51:51+00:00"
},
{
"name": "mpdf/psr-http-message-shim",
"version": "v2.0.1",
"source": {
"type": "git",
"url": "https://github.com/mpdf/psr-http-message-shim.git",
"reference": "f25a0153d645e234f9db42e5433b16d9b113920f"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/mpdf/psr-http-message-shim/zipball/f25a0153d645e234f9db42e5433b16d9b113920f",
"reference": "f25a0153d645e234f9db42e5433b16d9b113920f",
"shasum": ""
},
"require": {
"psr/http-message": "^2.0"
},
"type": "library",
"autoload": {
"psr-4": {
"Mpdf\\PsrHttpMessageShim\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Mark Dorison",
"email": "mark@chromatichq.com"
},
{
"name": "Kristofer Widholm",
"email": "kristofer@chromatichq.com"
},
{
"name": "Nigel Cunningham",
"email": "nigel.cunningham@technocrat.com.au"
}
],
"description": "Shim to allow support of different psr/message versions.",
"support": {
"issues": "https://github.com/mpdf/psr-http-message-shim/issues",
"source": "https://github.com/mpdf/psr-http-message-shim/tree/v2.0.1"
},
"time": "2023-10-02T14:34:03+00:00"
},
{
"name": "mpdf/psr-log-aware-trait",
"version": "v3.0.0",
"source": {
"type": "git",
"url": "https://github.com/mpdf/psr-log-aware-trait.git",
"reference": "a633da6065e946cc491e1c962850344bb0bf3e78"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/mpdf/psr-log-aware-trait/zipball/a633da6065e946cc491e1c962850344bb0bf3e78",
"reference": "a633da6065e946cc491e1c962850344bb0bf3e78",
"shasum": ""
},
"require": {
"psr/log": "^3.0"
},
"type": "library",
"autoload": {
"psr-4": {
"Mpdf\\PsrLogAwareTrait\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Mark Dorison",
"email": "mark@chromatichq.com"
},
{
"name": "Kristofer Widholm",
"email": "kristofer@chromatichq.com"
}
],
"description": "Trait to allow support of different psr/log versions.",
"support": {
"issues": "https://github.com/mpdf/psr-log-aware-trait/issues",
"source": "https://github.com/mpdf/psr-log-aware-trait/tree/v3.0.0"
},
"time": "2023-05-03T06:19:36+00:00"
},
{
"name": "myclabs/deep-copy",
"version": "1.13.4",
"source": {
"type": "git",
"url": "https://github.com/myclabs/DeepCopy.git",
"reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/07d290f0c47959fd5eed98c95ee5602db07e0b6a",
"reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a",
"shasum": ""
},
"require": {
"php": "^7.1 || ^8.0"
},
"conflict": {
"doctrine/collections": "<1.6.8",
"doctrine/common": "<2.13.3 || >=3 <3.2.2"
},
"require-dev": {
"doctrine/collections": "^1.6.8",
"doctrine/common": "^2.13.3 || ^3.2.2",
"phpspec/prophecy": "^1.10",
"phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13"
},
"type": "library",
"autoload": {
"files": [
"src/DeepCopy/deep_copy.php"
],
"psr-4": {
"DeepCopy\\": "src/DeepCopy/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"description": "Create deep copies (clones) of your objects",
"keywords": [
"clone",
"copy",
"duplicate",
"object",
"object graph"
],
"support": {
"issues": "https://github.com/myclabs/DeepCopy/issues",
"source": "https://github.com/myclabs/DeepCopy/tree/1.13.4"
},
"funding": [
{
"url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy",
"type": "tidelift"
}
],
"time": "2025-08-01T08:46:24+00:00"
},
{
"name": "nesbot/carbon",
"version": "3.10.0",
@@ -3975,6 +4208,78 @@
},
"time": "2019-03-08T08:55:37+00:00"
},
{
"name": "setasign/fpdi",
"version": "v2.6.4",
"source": {
"type": "git",
"url": "https://github.com/Setasign/FPDI.git",
"reference": "4b53852fde2734ec6a07e458a085db627c60eada"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/Setasign/FPDI/zipball/4b53852fde2734ec6a07e458a085db627c60eada",
"reference": "4b53852fde2734ec6a07e458a085db627c60eada",
"shasum": ""
},
"require": {
"ext-zlib": "*",
"php": "^7.1 || ^8.0"
},
"conflict": {
"setasign/tfpdf": "<1.31"
},
"require-dev": {
"phpunit/phpunit": "^7",
"setasign/fpdf": "~1.8.6",
"setasign/tfpdf": "~1.33",
"squizlabs/php_codesniffer": "^3.5",
"tecnickcom/tcpdf": "^6.8"
},
"suggest": {
"setasign/fpdf": "FPDI will extend this class but as it is also possible to use TCPDF or tFPDF as an alternative. There's no fixed dependency configured."
},
"type": "library",
"autoload": {
"psr-4": {
"setasign\\Fpdi\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Jan Slabon",
"email": "jan.slabon@setasign.com",
"homepage": "https://www.setasign.com"
},
{
"name": "Maximilian Kresse",
"email": "maximilian.kresse@setasign.com",
"homepage": "https://www.setasign.com"
}
],
"description": "FPDI is a collection of PHP classes facilitating developers to read pages from existing PDF documents and use them as templates in FPDF. Because it is also possible to use FPDI with TCPDF, there are no fixed dependencies defined. Please see suggestions for packages which evaluates the dependencies automatically.",
"homepage": "https://www.setasign.com/fpdi",
"keywords": [
"fpdf",
"fpdi",
"pdf"
],
"support": {
"issues": "https://github.com/Setasign/FPDI/issues",
"source": "https://github.com/Setasign/FPDI/tree/v2.6.4"
},
"funding": [
{
"url": "https://tidelift.com/funding/github/packagist/setasign/fpdi",
"type": "tidelift"
}
],
"time": "2025-08-05T09:57:14+00:00"
},
{
"name": "shopwwi/filesystem-oss",
"version": "v1.0.3",
+35 -2
View File
@@ -165,6 +165,7 @@ Route::group('/admin', function () {
Route::any('/authorize', [app\admin\controller\UserController::class, 'authorize']);
Route::any('/set/resources', [app\admin\controller\UserController::class, 'updateResources']);
Route::any('/update/password', [app\admin\controller\UserController::class, 'savePassword']);
Route::any('/update/sign', [app\admin\controller\UserController::class, 'updateSign']);
});
// 询单
@@ -299,6 +300,29 @@ Route::group('/admin', function () {
Route::any('/destroy', [app\admin\controller\SysPortController::class, 'destroy']);
});
// 审批
Route::group('/approval', function () {
Route::any('/list', [app\admin\controller\ApprovalFlowsController::class, 'index']);
Route::any('/show', [app\admin\controller\ApprovalFlowsController::class, 'show']);
Route::any('/store', [app\admin\controller\ApprovalFlowsController::class, 'store']);
Route::any('/update', [app\admin\controller\ApprovalFlowsController::class, 'update']);
Route::any('/destroy', [app\admin\controller\ApprovalFlowsController::class, 'destroy']);
});
// 审批记录
Route::group('/approval/records', function () {
Route::any('/list', [app\admin\controller\ApprovalRecordsController::class, 'index']);
Route::any('/logs', [app\admin\controller\ApprovalRecordsController::class, 'logs']);
Route::any('/action', [app\admin\controller\ApprovalRecordsController::class, 'approve']);
});
Route::group('/stock/order', function () {
Route::any('/list', [app\admin\controller\StockOrdersController::class, 'index']);
Route::any('/show', [app\admin\controller\StockOrdersController::class, 'show']);
Route::any('/store', [app\admin\controller\StockOrdersController::class, 'store']);
Route::any('/print', [app\admin\controller\StockOrdersController::class, 'print']);
Route::any('/detail', [app\admin\controller\StockOrdersController::class, 'detail']);
});
});
@@ -368,7 +392,16 @@ Route::group('/crm', function () {
Route::any('/list', [app\crm\controller\StockOrdersController::class, 'index']);
Route::any('/show', [app\crm\controller\StockOrdersController::class, 'show']);
Route::any('/store', [app\crm\controller\StockOrdersController::class, 'store']);
// Route::any('/update', [app\crm\controller\StockOrdersController::class, 'update']);
// Route::any('/destroy', [app\crm\controller\StockOrdersController::class, 'destroy']);
Route::any('/update', [app\crm\controller\StockOrdersController::class, 'update']);
Route::any('/print', [app\crm\controller\StockOrdersController::class, 'print']);
Route::any('/approval', [app\crm\controller\StockOrdersController::class, 'approval']);
Route::any('/detail', [app\crm\controller\StockOrdersController::class, 'detail']);
});
// 审批记录
Route::group('/approval/records', function () {
Route::any('/list', [app\crm\controller\ApprovalRecordsController::class, 'index']);
Route::any('/logs', [app\crm\controller\ApprovalRecordsController::class, 'logs']);
Route::any('/action', [app\crm\controller\ApprovalRecordsController::class, 'approve']);
});
});
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.
Binary file not shown.