Commit 37d77b4c by 杨树贤

Merge branch 'ysx-对接审核中心-20251015'

parents b2419022 f16a8e05
......@@ -3,6 +3,7 @@
namespace App\Http\Controllers\Api;
use Cookie;
use Exception;
use App\Model\LogModel;
use App\Model\RedisModel;
use Illuminate\Http\Request;
......@@ -16,6 +17,7 @@ use Illuminate\Support\Facades\Auth;
use App\Http\Services\CompanyService;
use App\Http\Services\SupplierService;
use App\Http\Services\AdminUserService;
use App\Http\Services\AuditCenterService;
use App\Http\Services\SyncSupplierService;
use App\Http\Validators\SupplierValidator;
use App\Http\Services\StandardBrandService;
......@@ -211,8 +213,11 @@ class SupplierApiController extends Controller
$this->response(0, '操作成功');
}
$service = new SupplierService();
$supplierId = $service->saveSupplier($channel);
//dd(123);
try {
$supplierId = $service->saveSupplier($channel);
} catch (\Exception $e) {
$this->response(-1, $e->getMessage());
}
if (!$supplierId) {
$this->response(-1, '操作失败');
}
......@@ -233,7 +238,11 @@ class SupplierApiController extends Controller
$channelMap = array_merge($this->channelMap, config('field.AttachmentFields'));
$channel = $request->only($channelMap);
$service = new SupplierService();
$result = $service->saveSupplier($channel);
try {
$result = $service->saveSupplier($channel);
} catch (\Exception $e) {
$this->response(-1, $e->getMessage());
}
if (!$result) {
$this->response(-1, '修改失败');
}
......@@ -412,10 +421,14 @@ class SupplierApiController extends Controller
$this->response(-1, '不同意时必须填写原因');
}
$supplierId = $request->get('supplier_id');
$service = new SupplierAuditService();
$result = $service->auditSupplier($supplierId, $status, $rejectReason);
if (!$result) {
$this->response(-1, '审核失败');
try {
$service = new SupplierAuditService();
$result = $service->auditSupplier($supplierId, $status, $rejectReason);
if (!$result) {
$this->response(-1, '审核失败');
}
} catch (Exception $e) {
$this->response(-1, $e->getMessage());
}
$this->response(0, '审核成功');
}
......@@ -834,4 +847,13 @@ class SupplierApiController extends Controller
$this->response(-1, '清空线上采购员失败');
}
}
//获取审核流程
public function GetAuditFlow($request)
{
$supplierId = $request->get('supplier_id');
$supplierAuditService = new AuditCenterService();
$result = $supplierAuditService->getAuditInfoByIdAndType($supplierId, AuditCenterService::TYPE_SUPPLIER_AUDIT);
$this->response(0, 'ok', $result);
}
}
......@@ -15,6 +15,7 @@ use App\Model\SupplierContactModel;
use App\Http\Services\RegionService;
use App\Http\Services\SupplierService;
use App\Http\Services\DepartmentService;
use App\Http\Services\AuditCenterService;
use App\Http\Services\SupplierTagService;
use App\Http\Transformers\LogTransformer;
use App\Http\Services\SkuUploadLogService;
......
<?php
namespace App\Http\Services;
use Exception;
use GuzzleHttp\Client;
use Illuminate\Support\Facades\Log;
class AuditCenterService
{
const SYSTEM_ID = 1;
//审批状态 0-待审核 1-审核中 2-审核完成 -1-审核拒绝 -2-审核撤销
const APPROVAL_STATUS_WAIT = 0;
const APPROVAL_STATUS_ING = 1;
const APPROVAL_STATUS_FINISH = 2;
const APPROVAL_STATUS_REFUSE = -1;
const APPROVAL_STATUS_CANCEL = -2;
const TYPE_SUPPLIER_AUDIT = 'supplier_audit';
public static function getAuditInfoByIdAndType($id, $type)
{
$requestData = [
'bill_type' => $type,
'source_id' => $id,
'system_id' => self::SYSTEM_ID,
];
$response = self::requestAudit("/sync/bill/getBillInfo", $requestData);
if ($response['code'] != 0) {
throw new Exception($response['msg']);
}
return $response['data'];
}
public static function getAuditInfoByBillId($billId)
{
$requestData = [
'bill_id' => $billId,
];
$response = self::requestAudit("/sync/bill/getBillInfo", $requestData);
if ($response['code'] != 0) {
throw new Exception($response['msg']);
}
return $response['data'];
}
public static function audit($billId, $approvalUid, $approvalStatus, $remark)
{
$requestData = [
'bill_id' => $billId,
'approval_uid' => $approvalUid,
'approval_status' => $approvalStatus,
'remark' => $remark,
];
$response = self::requestAudit("/sync/bill/audit", $requestData);
if ($response['code'] != 0) {
throw new Exception($response['msg']);
}
return $response['data'];
}
public static function getAuditFlowInfo($type, $userId, $orgId = null)
{
$requestData = [
"system_id" => self::SYSTEM_ID,
"bill_type" => $type,
"apply_uid" => $userId,
"org_id" => $orgId,
];
$response = self::requestAudit("/sync/flow/getFlowInfo", $requestData);
if ($response['code'] != 0) {
throw new Exception($response['msg']);
}
return $response['data'];
}
public static function getAuditList($billType, $params)
{
//page
//limit
//source_id
//source_sn
//org_id
//system_id
//bill_type
//approval_status
//apply_uids
//apply_dept_ids
//apply_time
//update_time
$requestData = $params;
$requestData['system_id'] = self::SYSTEM_ID;
$response = self::requestAudit("/sync/bill/getBillList", $requestData);
if ($response['code'] != 0) {
throw new Exception($response['msg']);
}
return $response['data'];
}
public static function addAudit($params, $applyUid, $createUid)
{
//{
// "flow_id": "6",
// "source_id": "1",
// "source_sn": "1",
// "org_id": "1",
// "system_id": "5",
// "trigger_reason": "测试触发",
// "current_node": "null",
// "apply_uid": "1580",
// "create_uid": "1580",
// "apply_node_ids": "41,42"
//}
$requestData = [
'flow_id' => $params['flow_id'],
'source_id' => $params['source_id'],
'source_sn' => $params['source_sn'],
'org_id' => $params['org_id'],
'system_id' => self::SYSTEM_ID,
'trigger_reason' => array_get($params, 'trigger_reason', ''),
'current_node' => array_get($params, 'current_node', ''),
'apply_uid' => $applyUid,
'create_uid' => $createUid,
'apply_node_ids' => array_get($params, 'apply_node_ids', ''),
];
$response = self::requestAudit("/sync/bill/addBill", $requestData);
if ($response['code'] != 0) {
throw new Exception($response['msg']);
}
return $response['data'];
}
public static function requestAudit($route, $data)
{
$url = config("website.AuditCenterUrl");
$url = $url . $route;
try {
Log::info("---------------同步请求audit接口 {$route}------------------");
Log::info(json_encode($data));
$client = new Client(['headers' => ['Content-Type' => 'application/json']]);
$response = $client->post($url, ['json' => $data]);
$result = json_decode($response->getBody()->getContents(), true);
if ($result['code'] != 0) {
throw new Exception($result['msg']);
}
Log::info("---------------同步请求audit接口 {$route}-----audit返回结果-------------");
return $result;
} catch (\Throwable $e) {
throw $e;
}
}
}
......@@ -6,9 +6,10 @@ namespace App\Http\Services;
use App\Model\LogModel;
use App\Model\SupplierAddressModel;
use App\Model\SupplierAttachmentsModel;
use App\Model\SupplierChannelModel;
use App\Model\SupplierContactModel;
use Illuminate\Support\Facades\Log;
use App\Model\SupplierAttachmentsModel;
use http\Exception\InvalidArgumentException;
class SupplierAddressService
......@@ -70,18 +71,11 @@ class SupplierAddressService
}
//更新一体化的地址库
public function updateUnitedAddress($supplierId, $supplierAddress, $shippingAddress, $returnAddress)
public function updateUnitedAddress($oldSupplier, $supplierAddress, $shippingAddress, $returnAddress)
{
//先找出原始地址数据,然后比对
$supplier = SupplierChannelModel::where('supplier_id', $supplierId)->select([
'supplier_id',
'supplier_code',
'supplier_name',
'supplier_address',
'create_name',
'create_uid',
])->first()->toArray();
$supplier = $oldSupplier;
$oldSupplierAddress = $supplier['supplier_address'];
$supplierId = $supplier['supplier_id'];
//收发货地址
$addressData = SupplierAddressModel::where('supplier_id', $supplierId)->select(['info_id', 'address', 'address_type', 'supplier_id'])
->get()->toArray();
......@@ -135,13 +129,12 @@ class SupplierAddressService
'create_name' => $supplier['create_name'],
'address_data' => $changedAddress,
];
$url = config('website.UnitedDataDomain') . '/sync/Address/updateAddress';
$result = curl($url, $params);
$result = json_decode($result, true);
\Log::error($result);
Log::error($result);
if (array_get($result, 'code') != 0) {
\Log::error('更新一体化地址库失败 : ' . json_encode($result));
Log::error('更新一体化地址库失败 : ' . json_encode($result));
}
}
......
......@@ -12,6 +12,204 @@ class SupplierAuditService
{
public function auditSupplier($supplierId, $status, $rejectReason)
{
// 获取审核单据信息
$auditBillInfo = AuditCenterService::getAuditInfoByIdAndType($supplierId, AuditCenterService::TYPE_SUPPLIER_AUDIT);
if (empty($auditBillInfo)) {
// 如果没有审核单据,说明是旧流程,继续使用原有逻辑
return $this->auditSupplierOld($supplierId, $status, $rejectReason);
}
// 使用审核中心流程
$approvalStatus = $status == SupplierChannelModel::STATUS_PASSED ? 1 : 2;
$result = $this->auditByAuditCenter($auditBillInfo['id'], $approvalStatus, $rejectReason);
return $result ? true : false;
}
/**
* 通过审核中心审核
*/
private function auditByAuditCenter($billId, $approvalStatus, $remark)
{
$billInfo = AuditCenterService::getAuditInfoByBillId($billId);
if (empty($billInfo)) {
throw new \Exception("单据不存在");
}
$approvalUid = request()->user->userId;
// 调用审核中心审核接口
if (in_array($billInfo['approval_status'], [
AuditCenterService::APPROVAL_STATUS_WAIT,
AuditCenterService::APPROVAL_STATUS_ING
])) {
$billInfo = AuditCenterService::audit($billId, $approvalUid, $approvalStatus, $remark);
}
// 处理审核结果
$this->handleAuditResult($billInfo['source_id'], $billInfo, $approvalStatus);
return true;
}
/**
* 处理审核结果
*/
private function handleAuditResult($supplierId, $billInfo, $approvalStatus)
{
// 判断billInfo的状态,如果是完成,则直接完成,如果是拒绝,则走拒绝流程
if ($approvalStatus == 1) { // 通过
if ($billInfo['approval_status'] == AuditCenterService::APPROVAL_STATUS_FINISH) {
$this->auditPass($supplierId);
} else {
//这里要记录一次审核日志
$logService = new LogService();
$logService->AddLog($billInfo['source_id'], LogModel::UPDATE_OPERATE, '审核供应商', '审核通过');
}
} else { // 拒绝
if ($billInfo['approval_status'] == AuditCenterService::APPROVAL_STATUS_REFUSE) {
$this->auditNotPass($supplierId, array_get($billInfo['current_node_info'], 'remark', '审核不通过'));
}
}
}
/**
* 审核通过处理
*/
private function auditPass($supplierId)
{
$supplier = SupplierChannelModel::where('supplier_id', $supplierId)->first();
if (!$supplier) {
return false;
}
$status = SupplierChannelModel::STATUS_PASSED;
// 更新供应商状态
SupplierChannelModel::where('supplier_id', $supplierId)->update([
'status' => $status,
'need_review' => 0,
'update_time' => time(),
]);
// 自动创建芯链账号
SupplierAccountService::autoCreateYunxinAccount($supplierId);
// 添加日志
$logService = new LogService();
$logService->AddLog($supplierId, LogModel::UPDATE_OPERATE, '审核供应商', '审核通过');
// 同步到金蝶
$service = new SyncSupplierService();
$service->syncSupplierToErp($supplierId);
// 同步到一体化
if (!checkPerm('IgnoreCompanyCheck')) {
$regionType = $supplier['region'] == SupplierChannelModel::REGION_CN ? 1 : 2;
if ($regionType == 2) {
(new SyncSupplierService())->syncSupplierToUnited($supplierId);
} else {
$supplier['tax_number'] = $supplier['supplier_name'] ?: $supplier['tax_number'];
$company = (new CompanyService())->getCompanyInfo(
$supplier['supplier_name'],
$supplier['tax_number'],
$regionType
);
if (!empty($company)) {
if (!empty($company['tax_number'])) {
SupplierChannelModel::where('supplier_id', $supplierId)->update([
'tax_number' => $company['tax_number'],
]);
}
(new SyncSupplierService())->syncSupplierToUnited($supplierId);
}
}
} else {
(new SyncSupplierService())->syncSupplierToUnited($supplierId);
}
if (!empty($supplier['group_code'])) {
(new SyncSupplierService())->syncSupplierToErp($supplierId);
}
return true;
}
/**
* 审核不通过处理
*/
private function auditNotPass($supplierId, $rejectReason)
{
$status = SupplierChannelModel::STATUS_REJECT;
// 更新供应商状态
SupplierChannelModel::where('supplier_id', $supplierId)->update([
'status' => $status,
'reject_reason' => $rejectReason,
'update_time' => time(),
]);
// 添加日志
$logService = new LogService();
$logService->AddLog($supplierId, LogModel::UPDATE_OPERATE, '审核供应商', '审核不通过,原因是 : ' . $rejectReason);
return true;
}
/**
* 添加审批到审核中心
*/
public function addApprove($supplierId, $triggerReason)
{
$canIgnoreAudit = (new SupplierAuditService())->checkCanIgnoreSupplierAudit($supplierId);
if ($canIgnoreAudit) {
return true;
}
$supplierInfo = SupplierChannelModel::where('supplier_id', $supplierId)->first();
if (!$supplierInfo) {
throw new \Exception("供应商不存在");
}
$auditInfo = AuditCenterService::getAuditInfoByIdAndType($supplierId, AuditCenterService::TYPE_SUPPLIER_AUDIT);
// 如果存在审核信息,并且状态不是 已完成,已拒绝,已撤销时直接跳过
if (!empty($auditInfo) && !in_array($auditInfo['approval_status'], [
AuditCenterService::APPROVAL_STATUS_REFUSE,
AuditCenterService::APPROVAL_STATUS_CANCEL,
AuditCenterService::APPROVAL_STATUS_FINISH
])) {
return $auditInfo;
}
// 先获取审核流节点数据
$flowInfo = AuditCenterService::getAuditFlowInfo(
AuditCenterService::TYPE_SUPPLIER_AUDIT,
request()->user->userId,
null // 供应商没有公司ID概念,传null
);
// 创建审批单据
$billInfo = AuditCenterService::addAudit([
"flow_id" => $flowInfo['flow_info']['id'],
"source_id" => $supplierId,
"source_sn" => $supplierInfo['supplier_code'],
"org_id" => 1,
"trigger_reason" => $triggerReason,
"current_node" => null,
"apply_node_ids" => implode(",", array_column($flowInfo['node_list'], 'id'))
], request()->user->userId, request()->user->userId);
return $billInfo;
}
/**
* 旧的审核流程(兼容)
*/
private function auditSupplierOld($supplierId, $status, $rejectReason)
{
$model = new SupplierChannelModel();
//先找出原来供应商的状态
......@@ -321,4 +519,18 @@ class SupplierAuditService
return false;
}
}
//获取审核流程
public function getAuditFlow($supplierId)
{
$flowInfo = AuditCenterService::getAuditList(
AuditCenterService::TYPE_SUPPLIER_AUDIT,
[
'id' => 2129,
'page' => 1,
'limit' => 1000
]
);
return $flowInfo;
}
}
......@@ -87,10 +87,10 @@ class SupplierService
$isDirectApply = request()->get('direct_apply');
//走事务
$supplierId = DB::connection('web')->transaction(function () use ($channel, $model, $oldSupplier, &$needAudit) {
$supplierId = DB::connection('web')->transaction(function () use ($channel, $model, $oldSupplier, &$needAudit, $isDirectApply, &$address) {
//是否直接申请审核,如果是直接申请审核,那就变成待审核
$isDirectApply = request()->get('direct_apply');
// $isDirectApply 已经从外部传入
$tagService = new SupplierTagService();
//获取附加税数据
......@@ -234,6 +234,15 @@ class SupplierService
//新增的时候也要去添加地址了
$supplierAddressService = new SupplierAddressService();
$supplierAddressService->saveShippingAddress($supplierId, $shippingAddress);
// 如果是直接申请审核,触发审核中心流程(在事务内,失败会回滚)
if ($isDirectApply) {
$auditService = new SupplierAuditService();
$triggerReason = $channel['supplier_type'] == SupplierChannelModel::SUPPLIER_TYPE_TEMPORARY
? request()->input('apply_audit_reason', '新增临时供应商并申请审核')
: '新增供应商并申请审核';
$auditService->addApprove($supplierId, $triggerReason);
}
} else {
/**这里的是更新供应商的操作**/
$supplierId = $this->newSupplierId = $channel['supplier_id'];
......@@ -273,11 +282,9 @@ class SupplierService
$channel['system_tags'] = trim(implode(',', $channel['system_tags']), ',');
$supplierAddressService = new SupplierAddressService();
//校验是否需要同步修改地址给一体化
$supplierAddressService->updateUnitedAddress($supplierId, $channel['supplier_address'], $address['shipping_address'], $address['return_address']);
$model->where('supplier_id', $supplierId)->update($channel);
$supplierAddressService = new SupplierAddressService();
//保存地址
$supplierAddressService->saveAddress($address);
......@@ -306,6 +313,21 @@ class SupplierService
$channel['customer_tags'],
$oldCustomerTags
);
// 如果更新后需要审核,并且是申请审核,触发审核中心流程(在事务内,失败会回滚)
if ($needAudit || $isDirectApply) {
$auditService = new SupplierAuditService();
$triggerReason = $channel['supplier_type'] == SupplierChannelModel::SUPPLIER_TYPE_TEMPORARY
? request()->input('apply_audit_reason', '修改供应商并申请审核')
: '修改供应商并申请审核';
$auditService->addApprove($supplierId, $triggerReason);
// 更新状态为审核中
$model->where('supplier_id', $supplierId)->update([
'status' => SupplierChannelModel::STATUS_IN_REVIEW,
'apply_audit_reason' => $triggerReason,
'update_time' => time()
]);
}
}
//新增供应商的话,还要去初始化有效期
......@@ -360,6 +382,10 @@ class SupplierService
//但是供应商还没有插入db的时候,可能会提前广播到,导致没法操作供应商的状态(因为供应商都还没有生成)
if (empty($channel['supplier_id'])) {
(new CompanyService())->checkSupplierCompanyAndAddress($supplierId);
} else {
$supplierAddressService = new SupplierAddressService();
//校验是否需要同步修改地址给一体化
$supplierAddressService->updateUnitedAddress($oldSupplier, $channel['supplier_address'], $address['shipping_address'], $address['return_address']);
}
$contentChange = true;
......@@ -405,15 +431,9 @@ class SupplierService
//这里有个申请审核的判断逻辑
//如果是申请审核,并且原来的状态不是已通过,无论什么情况,都要变成审核中,如果是已通过,那么就要走下面的判断是否要忽略审核流程
if ($isAudit && $oldSupplier['status'] == SupplierChannelModel::STATUS_PASSED && !$needAudit && !empty($channel['supplier_id'])) {
//注意: 如果是更新供应商并且 $isDirectApply = true, 已经在事务内处理了审核中心调用,这里不需要再处理
if ($isAudit && array_get($oldSupplier, 'status') == SupplierChannelModel::STATUS_PASSED && !$needAudit && !empty($channel['supplier_id'])) {
//什么都不需要操作
} else if ($isAudit && $newSupplier['is_entity'] != 0) {
$auditData[] = [
'supplier_id' => $supplierId,
//供应商类型为临时供应商,才会去存申请理由
'apply_audit_reason' => $channel['supplier_type'] == 2 ? request()->input('apply_audit_reason') : '',
];
$this->batchApplyInReviewSupplier($auditData);
}
return $supplierId;
}
......@@ -767,18 +787,27 @@ class SupplierService
//批量申请审核供应商
public function batchApplyInReviewSupplier($auditData = [])
{
$model = new SupplierChannelModel();
foreach ($auditData as $data) {
$result = $model->where('supplier_id', $data['supplier_id'])->update([
'update_time' => time(),
'apply_audit_reason' => $data['apply_audit_reason'],
'status' => SupplierChannelModel::STATUS_IN_REVIEW
]);
if (!$result) {
return $result;
return DB::connection('web')->transaction(function () use ($auditData) {
$model = new SupplierChannelModel();
$auditService = new SupplierAuditService();
foreach ($auditData as $data) {
$result = $model->where('supplier_id', $data['supplier_id'])->update([
'update_time' => time(),
'apply_audit_reason' => $data['apply_audit_reason'],
'status' => SupplierChannelModel::STATUS_IN_REVIEW
]);
if (!$result) {
throw new \Exception('更新供应商状态失败');
}
// 触发审核中心流程(在事务内,失败会回滚)
$triggerReason = array_get($data, 'apply_audit_reason', '申请审核');
$auditService->addApprove($data['supplier_id'], $triggerReason);
}
}
return true;
return true;
});
}
//判断是否自动转正,并且还要修改为待提审状态给相关人员进行补充资料
......
......@@ -363,13 +363,19 @@ class SyncSupplierService
if (!$isEntity && $preStatus == SupplierChannelModel::STATUS_DISABLE) {
$preStatus = SupplierChannelModel::STATUS_IN_REVIEW;
}
$status = $isEntity ? SupplierChannelModel::STATUS_DISABLE : ($preStatus == SupplierChannelModel::STATUS_BLOCK ? SupplierChannelModel::STATUS_IN_REVIEW : $preStatus);
if ($isEntityResult == SupplierChannelModel::IS_ENTITY_NEED_CONFIRM) {
$status = SupplierChannelModel::STATUS_DISABLE;
}
//修改供应商的状态
SupplierChannelModel::where('supplier_id', $supplierId)
->update([
'is_entity' => $isEntityResult,
'update_time' => time(),
//这里的逻辑是,因为取消实体名单以后,如果之前的状态是黑名单的话,直接变成待提审
'status' => $isEntity ? SupplierChannelModel::STATUS_DISABLE : ($preStatus == SupplierChannelModel::STATUS_BLOCK ? SupplierChannelModel::STATUS_IN_REVIEW : $preStatus),
'status' =>$status,
'disable_reason' => $reason,
'united_tags' => !empty($tagList) ? implode(',', $tagList) : '',
]);
......
......@@ -15,6 +15,7 @@ return [
'AddSupplierAttachment',
'UpdateSupplierAttachment',
'BatchUpdateSkuStatus',
'GetAuditFlow',
'ViewAccountLog',
]//不用验证权限的方法
],
......
......@@ -46,5 +46,6 @@ return [
'SkipSendEmail' => env('skip_send_email'),
'CubeUrl' => env('CUBE_URL'),
'FootstoneCurlUrl' => 'http://footstone.liexindev.net/open/curl?url=',
'GoodsServerUrl' => 'http://192.168.1.237:60014'
'GoodsServerUrl' => 'http://192.168.1.237:60014',
'AuditCenterUrl' => 'http://audit_center.liexindev.net',
];
# 供应商审核对接审核中心说明
## 概述
参考付款审核系统的对接方式,将供应商审核流程对接到审核中心,实现统一的审核管理。
## 修改的文件
### 1. resources/views/script/SupplierListScript.blade.php
在供应商列表页面添加审核流程时间线展示功能:
#### 功能说明:
- 当供应商状态为"审核中"时,鼠标悬停在状态字段上会显示审核流程时间线
- 时间线横向展示,包含每个审核节点的详细信息
- 使用 layui 的 tips 组件实现浮窗效果
#### 主要修改:
- 修改状态字段的 `templet` 函数,为审核中状态添加 `audit-status-hover` class
-`done` 回调中调用 `bindAuditStatusHover()` 绑定悬停事件
- 新增 `bindAuditStatusHover()` 函数处理鼠标悬停事件
- 新增 `showAuditFlowTips()` 函数显示审核流程浮窗
- 新增 `buildAuditTimeline()` 函数构建横向时间线 HTML
#### 时间线展示内容:
- 审核节点序号(圆形图标)
- 审核状态(通过/拒绝/待审核)
- 审核人
- 审核结果
- 审核时间
- 附加说明(如有)
### 2. app/Http/Services/SupplierAuditService.php
主要修改内容:
#### 新增方法:
- `auditByAuditCenter($billId, $approvalStatus, $remark)` - 通过审核中心审核
- `handleAuditResult($supplierId, $billInfo, $approvalStatus)` - 处理审核结果
- `auditPass($supplierId)` - 审核通过处理
- `auditNotPass($supplierId, $rejectReason)` - 审核不通过处理
- `addApprove($supplierId, $triggerReason)` - 添加审批到审核中心
#### 修改方法:
- `auditSupplier($supplierId, $status, $rejectReason)` - 主审核方法
- 先检查是否有审核中心单据
- 如果有,走审核中心流程
- 如果没有,走旧流程(兼容)
### 3. app/Http/Controllers/Api/SupplierApiController.php
已存在 `GetAuditFlow()` 接口,用于获取供应商的审核流程信息:
- 接口路径:`/api/supplier/GetAuditFlow`
- 参数:`supplier_id`
- 返回:审核单据详细信息,包括节点列表、当前节点、审核状态等
### 4. app/Http/Services/SupplierService.php
修改了两个方法:
#### `saveSupplier()` 方法:
**新增供应商并直接申请审核**(第238-244行)
- 在新增供应商并直接申请审核时(`$isDirectApply = true`
- **在事务内**调用 `SupplierAuditService::addApprove()` 创建审核中心单据
- **如果审核中心调用失败,会抛出异常并回滚整个事务**
- 保证数据一致性:要么供应商和审核单据都创建成功,要么都失败
**更新供应商并直接申请审核**(第319-331行)
- 在更新供应商并直接申请审核时(`$isDirectApply = true``$needAudit = true`
- **在事务内**先更新供应商信息,然后调用 `SupplierAuditService::addApprove()` 创建审核中心单据
- **在事务内**更新供应商状态为 `STATUS_IN_REVIEW`
- **如果审核中心调用失败,会抛出异常并回滚整个事务**
- 保证数据一致性:要么供应商修改、状态更新、审核单据都成功,要么都回滚
**避免重复创建审核单据**(第435行)
- 事务外的 `$isAudit` 逻辑排除了更新供应商并直接申请审核的情况
- 条件:`!($isDirectApply && !empty($channel['supplier_id']))`
- 确保不会重复调用审核中心
#### `batchApplyInReviewSupplier()` 方法:
- **使用 `DB::transaction()` 包裹整个操作**
- 在更新供应商状态为审核中后
- **在事务内**调用 `SupplierAuditService::addApprove()` 创建审核中心单据
- **如果审核中心调用失败,会抛出异常并回滚整个事务**
- 保证数据一致性:要么状态更新和审核单据都成功,要么都失败
## 核心流程
### 申请审核流程
#### 场景1:新增供应商并直接申请审核
```
1. 用户新增供应商,勾选"直接申请审核"
2. SupplierService::saveSupplier()
- 保存供应商信息
- 设置状态为 STATUS_IN_REVIEW
- 调用 SupplierAuditService::addApprove()
3. SupplierAuditService::addApprove()
- 检查是否已有审核单据
- 获取审核流配置
- 调用 AuditCenterService::addAudit() 创建审核单据
4. 审核单据创建成功,等待审核
```
#### 场景2:已有供应商申请审核
```
1. 用户点击"申请审核"
2. SupplierService::batchApplyInReviewSupplier()
- 更新供应商状态为 STATUS_IN_REVIEW
- 调用 SupplierAuditService::addApprove()
3. SupplierAuditService::addApprove()
- 检查是否已有审核单据
- 获取审核流配置
- 调用 AuditCenterService::addAudit() 创建审核单据
4. 审核单据创建成功,等待审核
```
### 审核流程
```
1. 审核人在审核中心点击审核
2. SupplierApiController::AuditSupplier()
- 接收审核参数(supplier_id, status, reject_reason)
3. SupplierAuditService::auditSupplier()
- 检查是否有审核中心单据
- 如果有:调用 auditByAuditCenter()
- 如果没有:调用 auditSupplierOld()(兼容旧流程)
4. auditByAuditCenter()
- 调用 AuditCenterService::audit() 提交审核
- 调用 handleAuditResult() 处理审核结果
5. handleAuditResult()
- 如果审核通过且流程完成:调用 auditPass()
- 如果审核拒绝:调用 auditNotPass()
6. auditPass() / auditNotPass()
- 更新供应商状态
- 创建芯链账号(通过时)
- 同步到金蝶和一体化(通过时)
- 记录操作日志
```
## 审核状态映射
### 供应商状态
- `STATUS_PENDING = 0` - 待提审
- `STATUS_IN_REVIEW = 1` - 审核中
- `STATUS_PASSED = 2` - 已通过
- `STATUS_REJECT = 3` - 未通过
### 审核中心状态
- `APPROVAL_STATUS_WAIT = 0` - 待审核
- `APPROVAL_STATUS_ING = 1` - 审核中
- `APPROVAL_STATUS_FINISH = 2` - 审核完成
- `APPROVAL_STATUS_REFUSE = -1` - 审核拒绝
- `APPROVAL_STATUS_CANCEL = -2` - 审核撤销
### 审核操作状态
- `1` - 通过
- `2` - 拒绝
## 关键配置
### 审核类型
```php
AuditCenterService::TYPE_SUPPLIER_AUDIT = 'supplier_audit';
```
### 系统ID
```php
AuditCenterService::SYSTEM_ID = 1;
```
## 数据一致性保证
### 事务处理机制
#### 1. **新增供应商直接申请审核**
```
开始事务 (saveSupplier 第90行)
保存供应商基本信息
保存联系人、附件、银行信息
保存地址信息
调用审核中心创建单据 (第238-244行)
成功 → 提交事务 ✅
失败 → 抛出异常 → 回滚事务 → 所有供应商数据不保存 ✅
```
#### 2. **更新供应商并直接申请审核**
```
开始事务 (saveSupplier 第90行)
更新供应商基本信息
更新地址、附加费、标签等
调用审核中心创建单据 (第319-331行)
更新状态为 STATUS_IN_REVIEW
成功 → 提交事务 ✅
失败 → 抛出异常 → 回滚事务 → 供应商修改全部回滚 ✅
```
**关键点**
- 供应商的所有修改(基本信息、地址、标签等)都在事务内
- 审核中心调用也在事务内
- 如果审核中心失败,**所有修改都会回滚**,供应商恢复到修改前的状态
#### 3. **批量申请审核**(非直接申请)
```
开始事务 (batchApplyInReviewSupplier 第780行)
更新供应商状态为 STATUS_IN_REVIEW
调用审核中心创建单据
成功 → 提交事务 ✅
失败 → 抛出异常 → 回滚事务 → 状态不更新 ✅
```
#### 4. **异常处理链**
- `AuditCenterService::addAudit()` - 审核中心API调用失败时抛出异常
- `SupplierAuditService::addApprove()` - 供应商不存在或流程配置错误时抛出异常
- `DB::transaction()` - 捕获异常并自动回滚事务
- 前端会收到错误提示,用户可以重试
## 兼容性说明
1. **向后兼容**:保留了旧的审核流程 `auditSupplierOld()`,如果没有审核中心单据,会自动使用旧流程
2. **数据迁移**:现有的供应商审核数据不受影响,新的审核会自动使用审核中心
## 测试要点
1. **新增供应商并直接申请审核**
- 新增供应商时勾选"直接申请审核" → 检查供应商状态是否为审核中
- 检查审核中心是否创建了审核单据
2. **已有供应商申请审核**
- 创建供应商(不勾选直接申请) → 点击申请审核 → 检查审核中心是否有单据
3. **修改供应商后申请审核**
- 修改已通过的供应商信息 → 申请审核 → 检查是否触发审核流程
- 注意:部分字段修改不需要审核(由 `checkNeedAudit()` 判断)
4. **审核通过流程**
- 审核通过 → 检查供应商状态是否为已通过
- 检查是否创建了芯链账号
- 检查是否同步到金蝶和一体化
3. **审核拒绝流程**
- 审核拒绝 → 检查供应商状态是否为未通过
- 检查拒绝原因是否正确记录
4. **兼容性测试**
- 旧数据审核 → 应该走旧流程
- 新数据审核 → 应该走审核中心流程
## 注意事项
1. 审核中心需要配置好供应商审核流程(flow)
2. 审核节点需要配置审核人员
3. 确保 `config/website.php` 中配置了正确的审核中心地址
4. 审核中心服务需要正常运行
## 参考文件
- `app/Http/Services/Api/Audit/AuditPaymentService.php` - 付款审核对接示例
......@@ -140,6 +140,7 @@
}
},
{field: 'group_code', title: '集团编码', align: 'center', width: 90},
{
field: 'supplier_name', title: '供应商名称', align: 'left', width: 180, templet: function (data) {
if (data.status === -3) {
......@@ -233,19 +234,28 @@
return '<span style="color: ' + color + ';" title="' + data.disable_reason + '">' + data.is_entity_name + '</span>';
}
},
{
{
field: 'status_name', title: '状态', align: 'center', width: 80, templet: function (data) {
let statusHtml = '';
let color = '';
let title = '';
switch (data.status) {
case 3:
return "<span style='color: red' title='" + data.reject_reason + "'>" + data.status_name + "</span>"
color = 'red';
title = data.reject_reason;
break;
case -3:
return "<span style='color: red' title='" + data.block_reason + "'>" + data.status_name + "</span>"
color = 'red';
title = data.block_reason;
break;
case -2:
return "<span style='color: red' title='" + data.disable_reason + "'>" + data.status_name + "</span>"
default:
return data.status_name;
color = 'red';
title = data.disable_reason;
break;
}
statusHtml = '<span class="audit-status-hover" data-status="' + data.status + '" data-supplier-id="' + data.supplier_id + '" style="cursor: pointer; color: ' + color + ';" title="' + title + '">' + data.status_name + '</span>';
return statusHtml;
}
},
{field: 'last_update_name', title: '最新修改人', align: 'center', width: 110},
......@@ -325,6 +335,8 @@
}
});
// 绑定审核状态悬停事件
bindAuditStatusHover();
}
});
......@@ -894,5 +906,131 @@
$('.main_filter').attr('class', 'main_filter');
}
// 绑定审核状态悬停事件
function bindAuditStatusHover() {
let auditFlowCache = {}; // 缓存审核流程数据
$('.audit-status-hover').hover(
function() {
let $this = $(this);
let supplierId = $this.data('supplier-id');
let status = $this.data('status');
if (status !== 1) {
return;
}
// 如果已经有缓存,直接显示
if (auditFlowCache[supplierId]) {
showAuditFlowTips($this, auditFlowCache[supplierId]);
return;
}
// 请求审核流程数据
$.ajax({
url: '/api/supplier/GetAuditFlow',
type: 'get',
data: {supplier_id: supplierId},
dataType: 'json',
success: function(res) {
if (res.err_code === 0 && res.data) {
auditFlowCache[supplierId] = res.data;
showAuditFlowTips($this, res.data);
}
}
});
},
function() {
// 鼠标移出时关闭tips
layer.closeAll('tips');
}
);
}
// 显示审核流程时间线
function showAuditFlowTips($element, auditData) {
if (auditData.length === 0) {
return;
}else{
let timelineHtml = buildAuditTimeline(auditData);
layer.tips(timelineHtml, $element, {
tips: [3, '#009688'],
time: 0,
area: ['auto', 'auto'],
maxWidth: 550
});
}
}
// 构建审核时间线HTML - 横向展示
function buildAuditTimeline(auditData) {
let nodeList = auditData.node_list || [];
let currentNodeId = auditData.current_node;
let approvalStatus = auditData.approval_status;
let html = '<div style="padding: 20px; background: #fff; border-radius: 4px; box-shadow: 0 2px 8px rgba(0,0,0,0.15); min-width: 500px;">';
html += '<div style="display: flex; justify-content: space-between; align-items: flex-start;">';
nodeList.forEach(function(node, index) {
let isPassed = node.approval_status == 1;
let isRejected = node.approval_status == 2;
let isWaiting = node.approval_status == 0;
// 确定图标和颜色
let iconNumber = index + 1;
let iconBgColor = '';
let iconTextColor = '#fff';
let nodeTitle = '';
if (isPassed) {
iconBgColor = '#5FB878';
nodeTitle = '审核通过';
} else if (isRejected) {
iconBgColor = '#FF5722';
nodeTitle = '审核拒绝';
} else if (isWaiting) {
iconBgColor = '#D2D2D2';
nodeTitle = '待审核';
}
// 每个节点
html += '<div style="flex: 1; text-align: center; position: relative;">';
// 圆形图标
html += '<div style="width: 40px; height: 40px; border-radius: 50%; background: ' + iconBgColor + '; color: ' + iconTextColor + '; line-height: 40px; font-size: 18px; font-weight: bold; margin: 0 auto 10px;">';
html += iconNumber;
html += '</div>';
// 节点标题
html += '<div style="font-weight: bold; margin-bottom: 8px; font-size: 14px; color: #333;">' + nodeTitle + '</div>';
// 节点详情
html += '<div style="color: #666; font-size: 12px; line-height: 1.8; text-align: left; padding: 0 10px;">';
html += '<div>审核人:' + (node.approval_names || '-') + '</div>';
html += '<div>审核结果:' + (node.approval_status_text || '-') + '</div>';
html += '<div>审核时间:' + (node.update_time || '-') + '</div>';
if (node.remark) {
html += '<div>附加说明:' + node.remark + '</div>';
}
html += '</div>';
html += '</div>';
// 连接线(最后一个节点不需要)
if (index < nodeList.length - 1) {
let lineColor = isPassed ? '#5FB878' : '#D2D2D2';
html += '<div style="flex: 0 0 50px; padding-top: 20px;">';
html += '<div style="height: 2px; background: ' + lineColor + ';"></div>';
html += '</div>';
}
});
html += '</div>';
html += '</div>';
return html;
}
</script>
......@@ -140,6 +140,7 @@
}
},
{field: 'group_code', title: '集团编码', align: 'center', width: 90},
{
field: 'supplier_name', title: '供应商名称', align: 'left', width: 180, templet: function (data) {
if (data.status === -3) {
......@@ -233,19 +234,28 @@
return '<span style="color: ' + color + ';" title="' + data.disable_reason + '">' + data.is_entity_name + '</span>';
}
},
{
{
field: 'status_name', title: '状态', align: 'center', width: 80, templet: function (data) {
let statusHtml = '';
let color = '';
let title = '';
switch (data.status) {
case 3:
return "<span style='color: red' title='" + data.reject_reason + "'>" + data.status_name + "</span>"
color = 'red';
title = data.reject_reason;
break;
case -3:
return "<span style='color: red' title='" + data.block_reason + "'>" + data.status_name + "</span>"
color = 'red';
title = data.block_reason;
break;
case -2:
return "<span style='color: red' title='" + data.disable_reason + "'>" + data.status_name + "</span>"
default:
return data.status_name;
color = 'red';
title = data.disable_reason;
break;
}
statusHtml = '<span class="audit-status-hover" data-status="' + data.status + '" data-supplier-id="' + data.supplier_id + '" style="cursor: pointer; color: ' + color + ';" title="' + title + '">' + data.status_name + '</span>';
return statusHtml;
}
},
{field: 'last_update_name', title: '最新修改人', align: 'center', width: 110},
......@@ -325,6 +335,8 @@
}
});
// 绑定审核状态悬停事件
bindAuditStatusHover();
}
});
......@@ -894,5 +906,131 @@
$('.main_filter').attr('class', 'main_filter');
}
// 绑定审核状态悬停事件
function bindAuditStatusHover() {
let auditFlowCache = {}; // 缓存审核流程数据
$('.audit-status-hover').hover(
function() {
let $this = $(this);
let supplierId = $this.data('supplier-id');
let status = $this.data('status');
if (status !== 1) {
return;
}
// 如果已经有缓存,直接显示
if (auditFlowCache[supplierId]) {
showAuditFlowTips($this, auditFlowCache[supplierId]);
return;
}
// 请求审核流程数据
$.ajax({
url: '/api/supplier/GetAuditFlow',
type: 'get',
data: {supplier_id: supplierId},
dataType: 'json',
success: function(res) {
if (res.err_code === 0 && res.data) {
auditFlowCache[supplierId] = res.data;
showAuditFlowTips($this, res.data);
}
}
});
},
function() {
// 鼠标移出时关闭tips
layer.closeAll('tips');
}
);
}
// 显示审核流程时间线
function showAuditFlowTips($element, auditData) {
if (auditData.length === 0) {
return;
}else{
let timelineHtml = buildAuditTimeline(auditData);
layer.tips(timelineHtml, $element, {
tips: [3, '#009688'],
time: 0,
area: ['auto', 'auto'],
maxWidth: 550
});
}
}
// 构建审核时间线HTML - 横向展示
function buildAuditTimeline(auditData) {
let nodeList = auditData.node_list || [];
let currentNodeId = auditData.current_node;
let approvalStatus = auditData.approval_status;
let html = '<div style="padding: 20px; background: #fff; border-radius: 4px; box-shadow: 0 2px 8px rgba(0,0,0,0.15); min-width: 500px;">';
html += '<div style="display: flex; justify-content: space-between; align-items: flex-start;">';
nodeList.forEach(function(node, index) {
let isPassed = node.approval_status == 1;
let isRejected = node.approval_status == 2;
let isWaiting = node.approval_status == 0;
// 确定图标和颜色
let iconNumber = index + 1;
let iconBgColor = '';
let iconTextColor = '#fff';
let nodeTitle = '';
if (isPassed) {
iconBgColor = '#5FB878';
nodeTitle = '审核通过';
} else if (isRejected) {
iconBgColor = '#FF5722';
nodeTitle = '审核拒绝';
} else if (isWaiting) {
iconBgColor = '#D2D2D2';
nodeTitle = '待审核';
}
// 每个节点
html += '<div style="flex: 1; text-align: center; position: relative;">';
// 圆形图标
html += '<div style="width: 40px; height: 40px; border-radius: 50%; background: ' + iconBgColor + '; color: ' + iconTextColor + '; line-height: 40px; font-size: 18px; font-weight: bold; margin: 0 auto 10px;">';
html += iconNumber;
html += '</div>';
// 节点标题
html += '<div style="font-weight: bold; margin-bottom: 8px; font-size: 14px; color: #333;">' + nodeTitle + '</div>';
// 节点详情
html += '<div style="color: #666; font-size: 12px; line-height: 1.8; text-align: left; padding: 0 10px;">';
html += '<div>审核人:' + (node.approval_names || '-') + '</div>';
html += '<div>审核结果:' + (node.approval_status_text || '-') + '</div>';
html += '<div>审核时间:' + (node.update_time || '-') + '</div>';
if (node.remark) {
html += '<div>附加说明:' + node.remark + '</div>';
}
html += '</div>';
html += '</div>';
// 连接线(最后一个节点不需要)
if (index < nodeList.length - 1) {
let lineColor = isPassed ? '#5FB878' : '#D2D2D2';
html += '<div style="flex: 0 0 50px; padding-top: 20px;">';
html += '<div style="height: 2px; background: ' + lineColor + ';"></div>';
html += '</div>';
}
});
html += '</div>';
html += '</div>';
return html;
}
</script>
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment