2295 lines
89 KiB
PHP
2295 lines
89 KiB
PHP
<?php
|
|
|
|
namespace app\controller\admin;
|
|
|
|
use app\support\AppraisalEvidenceService;
|
|
use app\support\ContentService;
|
|
use app\support\EnterpriseWebhookService;
|
|
use app\support\FulfillmentFlowService;
|
|
use app\support\MessageDispatcher;
|
|
use app\support\MaterialTagService;
|
|
use app\support\PublicAssetUrlService;
|
|
use support\Request;
|
|
use support\think\Db;
|
|
|
|
class AppraisalTasksController
|
|
{
|
|
public function index(Request $request)
|
|
{
|
|
$keyword = trim((string)$request->input('keyword', ''));
|
|
$taskStage = trim((string)$request->input('task_stage', ''));
|
|
$status = trim((string)$request->input('status', ''));
|
|
$serviceProvider = trim((string)$request->input('service_provider', ''));
|
|
$scope = trim((string)$request->input('scope', ''));
|
|
$paginationEnabled = $request->input('page', null) !== null || $request->input('page_size', null) !== null;
|
|
$page = max(1, (int)$request->input('page', 1));
|
|
$pageSize = max(1, min(100, (int)$request->input('page_size', 20)));
|
|
|
|
$query = $this->buildTaskBaseQuery()
|
|
->whereRaw($this->workbenchVisibleOrderStatusSql());
|
|
$this->applyTaskScopeFilter($query, $request, $scope);
|
|
|
|
if ($keyword !== '') {
|
|
$query->where(function ($builder) use ($keyword) {
|
|
$builder->whereRaw(
|
|
'(o.order_no LIKE :keyword_order OR o.appraisal_no LIKE :keyword_appraisal OR ecr.external_order_no LIKE :keyword_external OR p.product_name LIKE :keyword_product)',
|
|
[
|
|
'keyword_order' => "%{$keyword}%",
|
|
'keyword_appraisal' => "%{$keyword}%",
|
|
'keyword_external' => "%{$keyword}%",
|
|
'keyword_product' => "%{$keyword}%",
|
|
]
|
|
);
|
|
});
|
|
}
|
|
|
|
if ($taskStage !== '') {
|
|
$query->where('t.task_stage', $taskStage);
|
|
}
|
|
if ($status !== '') {
|
|
$query->where('t.status', $status);
|
|
}
|
|
if ($serviceProvider !== '') {
|
|
$query->where('o.service_provider', $serviceProvider);
|
|
}
|
|
|
|
$matchedRows = $query->select()->toArray();
|
|
if (!$matchedRows) {
|
|
return api_success($paginationEnabled ? [
|
|
'list' => [],
|
|
'total' => 0,
|
|
'page' => $page,
|
|
'page_size' => $pageSize,
|
|
] : ['list' => []]);
|
|
}
|
|
|
|
$orderIds = array_values(array_unique(array_map(fn (array $item) => (int)$item['order_id'], $matchedRows)));
|
|
$reportMap = $this->buildAppraisalReportMap($orderIds);
|
|
|
|
$allRows = $this->buildTaskBaseQuery()
|
|
->whereRaw($this->workbenchVisibleOrderStatusSql())
|
|
->whereIn('t.order_id', $orderIds)
|
|
->group('t.id')
|
|
->order('t.order_id', 'desc')
|
|
->order('t.id', 'desc')
|
|
->select()
|
|
->toArray();
|
|
$this->applyTaskScopeFilterRows($allRows, $request, $scope);
|
|
$this->attachTransferFlowToRows($allRows);
|
|
|
|
$list = $this->buildGroupedTaskList($allRows, $reportMap);
|
|
$total = count($list);
|
|
if ($paginationEnabled) {
|
|
$offset = ($page - 1) * $pageSize;
|
|
$list = array_slice($list, $offset, $pageSize);
|
|
|
|
return api_success([
|
|
'list' => $list,
|
|
'total' => $total,
|
|
'page' => $page,
|
|
'page_size' => $pageSize,
|
|
]);
|
|
}
|
|
|
|
return api_success(['list' => $list]);
|
|
}
|
|
|
|
public function detail(Request $request)
|
|
{
|
|
$id = (int)$request->input('id', 0);
|
|
if (!$id) {
|
|
return api_error('任务 ID 不能为空', 422);
|
|
}
|
|
|
|
$task = Db::name('appraisal_tasks')
|
|
->alias('t')
|
|
->leftJoin('orders o', 'o.id = t.order_id')
|
|
->leftJoin('order_products p', 'p.order_id = t.order_id')
|
|
->leftJoin('order_extras e', 'e.order_id = t.order_id')
|
|
->leftJoin('appraisal_task_results r', 'r.task_id = t.id')
|
|
->leftJoin('reports rp', 'rp.order_id = t.order_id AND rp.report_type = "appraisal"')
|
|
->leftJoin('report_contents rc', 'rc.report_id = rp.id')
|
|
->leftJoin('enterprise_customer_order_refs ecr', 'ecr.order_id = t.order_id')
|
|
->order('rp.id', 'desc')
|
|
->field([
|
|
't.id',
|
|
't.order_id',
|
|
't.task_stage',
|
|
't.status',
|
|
't.assignee_id',
|
|
't.assignee_name',
|
|
't.started_at',
|
|
't.submitted_at',
|
|
't.sla_deadline',
|
|
't.is_overtime',
|
|
'o.order_no',
|
|
'o.appraisal_no',
|
|
'ecr.external_order_no',
|
|
'o.service_provider',
|
|
'o.order_status',
|
|
'o.display_status',
|
|
'p.product_name',
|
|
'p.category_id',
|
|
'p.category_name',
|
|
'p.brand_id',
|
|
'p.brand_name',
|
|
'p.color',
|
|
'p.size_spec',
|
|
'p.serial_no',
|
|
'e.purchase_channel',
|
|
'e.purchase_price',
|
|
'e.usage_status',
|
|
'e.condition_desc as extra_condition_desc',
|
|
'e.remark',
|
|
'r.id as result_id',
|
|
'r.result_text',
|
|
'r.result_desc',
|
|
'r.condition_grade',
|
|
'r.condition_desc as result_condition_desc',
|
|
'r.valuation_min',
|
|
'r.valuation_max',
|
|
'r.valuation_desc as result_valuation_desc',
|
|
'r.attachments_json as result_attachments_json',
|
|
'r.external_remark',
|
|
'r.internal_remark',
|
|
'rp.zhongjian_report_no',
|
|
'rp.report_entry_admin_id',
|
|
'rp.report_entry_admin_name',
|
|
'rp.report_entered_at',
|
|
'rc.zhongjian_report_files_json',
|
|
])
|
|
->where('t.id', $id)
|
|
->find();
|
|
|
|
if (!$task) {
|
|
return api_error('任务不存在', 404);
|
|
}
|
|
|
|
$report = $this->findLatestAppraisalReport((int)$task['order_id']);
|
|
$materialTag = $report ? (new MaterialTagService())->findBoundTagForReport((int)$report['id']) : null;
|
|
$transferFlow = $this->latestTransferFlowForOrder((int)$task['order_id']);
|
|
$effectiveStatus = $this->effectiveTaskStatus($task, $report);
|
|
if ($effectiveStatus !== $task['status']) {
|
|
Db::name('appraisal_tasks')->where('id', (int)$task['id'])->update([
|
|
'status' => $effectiveStatus,
|
|
'updated_at' => date('Y-m-d H:i:s'),
|
|
]);
|
|
$task['status'] = $effectiveStatus;
|
|
}
|
|
|
|
$timeline = Db::name('order_timelines')
|
|
->where('order_id', $task['order_id'])
|
|
->order('occurred_at', 'asc')
|
|
->select()
|
|
->toArray();
|
|
|
|
$materials = Db::name('order_upload_items')
|
|
->where('order_id', $task['order_id'])
|
|
->select()
|
|
->toArray();
|
|
|
|
$materials = array_values(array_filter(array_map(function (array $item) use ($request) {
|
|
$files = Db::name('order_upload_files')
|
|
->where('order_upload_item_id', $item['id'])
|
|
->order('id', 'asc')
|
|
->select()
|
|
->toArray();
|
|
|
|
if (!$files) {
|
|
return null;
|
|
}
|
|
|
|
return [
|
|
'item_name' => $item['item_name'],
|
|
'status' => $item['status'],
|
|
'source_type' => $item['source_type'],
|
|
'files' => array_map(fn (array $file) => [
|
|
'file_id' => $file['file_id'],
|
|
'file_url' => $this->assetUrlService()->normalizeUrl((string)$file['file_url'], $request),
|
|
'thumbnail_url' => $this->assetUrlService()->normalizeUrl((string)$file['thumbnail_url'], $request),
|
|
], $files),
|
|
];
|
|
}, $materials)));
|
|
|
|
$supplementTask = Db::name('order_supplement_tasks')
|
|
->where('order_id', $task['order_id'])
|
|
->where('status', 'pending')
|
|
->order('id', 'desc')
|
|
->find();
|
|
|
|
$supplementItems = [];
|
|
if ($supplementTask) {
|
|
$supplementItems = Db::name('order_supplement_task_items')
|
|
->where('task_id', $supplementTask['id'])
|
|
->order('id', 'asc')
|
|
->select()
|
|
->toArray();
|
|
}
|
|
|
|
$stageReportMap = $this->buildAppraisalReportMap([(int)$task['order_id']]);
|
|
|
|
$stageTaskRows = $this->buildTaskBaseQuery()
|
|
->where('t.order_id', (int)$task['order_id'])
|
|
->group('t.id')
|
|
->order('t.id', 'asc')
|
|
->select()
|
|
->toArray();
|
|
$this->attachTransferFlowToRows($stageTaskRows);
|
|
|
|
$stageTasks = array_map(function (array $item) use ($id, $stageReportMap) {
|
|
$row = $this->normalizeTaskListRow($item, $stageReportMap[(int)$item['order_id']] ?? null);
|
|
$row['is_current'] = (int)$row['id'] === $id;
|
|
return $row;
|
|
}, $stageTaskRows);
|
|
|
|
usort($stageTasks, fn (array $a, array $b) => $this->stagePriority($a['task_stage']) <=> $this->stagePriority($b['task_stage']));
|
|
|
|
$currentResultInfo = $this->formatResultInfo($task, $request);
|
|
$prefillResultInfo = null;
|
|
|
|
if ($task['task_stage'] === 'final_review') {
|
|
$prefillTask = Db::name('appraisal_tasks')
|
|
->alias('t')
|
|
->leftJoin('appraisal_task_results r', 'r.task_id = t.id')
|
|
->field([
|
|
't.id',
|
|
't.task_stage',
|
|
'r.id as result_id',
|
|
'r.result_text',
|
|
'r.result_desc',
|
|
'r.condition_grade',
|
|
'r.condition_desc',
|
|
'r.valuation_min',
|
|
'r.valuation_max',
|
|
'r.valuation_desc',
|
|
'r.attachments_json',
|
|
'r.external_remark',
|
|
'r.internal_remark',
|
|
])
|
|
->where('t.order_id', (int)$task['order_id'])
|
|
->where('t.task_stage', 'first_review')
|
|
->order('t.id', 'desc')
|
|
->find();
|
|
|
|
if ($prefillTask) {
|
|
$prefillPayload = $this->formatResultInfo($prefillTask, $request);
|
|
if ($this->hasResultData($prefillPayload)) {
|
|
$prefillResultInfo = array_merge($prefillPayload, [
|
|
'source_task_id' => (int)$prefillTask['id'],
|
|
'source_stage' => 'first_review',
|
|
'source_stage_text' => '鉴定',
|
|
]);
|
|
}
|
|
}
|
|
}
|
|
|
|
$appraisalTemplate = $this->resolveAppraisalTemplate(
|
|
(int)($task['category_id'] ?? 0),
|
|
(string)($task['service_provider'] ?? 'anxinyan'),
|
|
$currentResultInfo['key_points'] ?? []
|
|
);
|
|
|
|
return api_success([
|
|
'task_info' => [
|
|
'id' => (int)$task['id'],
|
|
'order_id' => (int)$task['order_id'],
|
|
'order_no' => $task['order_no'],
|
|
'appraisal_no' => $task['appraisal_no'],
|
|
'external_order_no' => $task['external_order_no'] ?: '',
|
|
'service_provider' => $task['service_provider'],
|
|
'service_provider_text' => $task['service_provider'] === 'zhongjian' ? '中检鉴定' : '实物鉴定',
|
|
'task_stage' => $task['task_stage'],
|
|
'task_stage_text' => '鉴定',
|
|
'status' => $task['status'],
|
|
'status_text' => $this->taskStatusText($task['status']),
|
|
'assignee_id' => (int)($task['assignee_id'] ?? 0),
|
|
'assignee_name' => $task['assignee_name'] ?: '未分配',
|
|
'started_at' => $task['started_at'],
|
|
'submitted_at' => $task['submitted_at'],
|
|
'sla_deadline' => $task['sla_deadline'],
|
|
'is_overtime' => (bool)$task['is_overtime'],
|
|
'internal_tag_no' => (string)($transferFlow['internal_tag_no'] ?? ''),
|
|
],
|
|
'report_summary' => $report ? [
|
|
'id' => (int)$report['id'],
|
|
'report_no' => $report['report_no'],
|
|
'report_status' => $report['report_status'],
|
|
'report_status_text' => $this->reportStatusText($report['report_status']),
|
|
] : null,
|
|
'material_tag' => $materialTag,
|
|
'zhongjian_report' => [
|
|
'report_no' => (string)($task['zhongjian_report_no'] ?? ''),
|
|
'report_entry_admin_id' => (int)($task['report_entry_admin_id'] ?? 0),
|
|
'report_entry_admin_name' => (string)($task['report_entry_admin_name'] ?? ''),
|
|
'report_entered_at' => (string)($task['report_entered_at'] ?? ''),
|
|
'files' => $this->evidenceService()->normalize($task['zhongjian_report_files_json'] ?? null, $request),
|
|
],
|
|
'product_info' => [
|
|
'product_name' => $task['product_name'] ?: '',
|
|
'category_id' => (int)($task['category_id'] ?? 0),
|
|
'category_name' => $task['category_name'] ?: '',
|
|
'brand_id' => (int)($task['brand_id'] ?? 0),
|
|
'brand_name' => $task['brand_name'] ?: '',
|
|
'color' => $task['color'] ?: '',
|
|
'size_spec' => $task['size_spec'] ?: '',
|
|
'serial_no' => $task['serial_no'] ?: '',
|
|
],
|
|
'extra_info' => [
|
|
'purchase_channel' => $task['purchase_channel'],
|
|
'purchase_price' => (float)$task['purchase_price'],
|
|
'usage_status' => $task['usage_status'],
|
|
'condition_desc' => $task['extra_condition_desc'],
|
|
'remark' => $task['remark'],
|
|
],
|
|
'result_info' => $currentResultInfo,
|
|
'prefill_result_info' => $prefillResultInfo,
|
|
'appraisal_template' => $appraisalTemplate,
|
|
'timeline' => array_map(fn (array $item) => [
|
|
'node_text' => $item['node_text'],
|
|
'node_desc' => $item['node_desc'],
|
|
'occurred_at' => $item['occurred_at'],
|
|
], $timeline),
|
|
'stage_tasks' => $stageTasks,
|
|
'materials' => $materials,
|
|
'supplement_task' => $supplementTask ? [
|
|
'id' => (int)$supplementTask['id'],
|
|
'reason' => $supplementTask['reason'],
|
|
'deadline' => $supplementTask['deadline'],
|
|
'status' => $supplementTask['status'],
|
|
'items' => array_map(fn (array $item) => [
|
|
'item_name' => $item['item_name'],
|
|
'guide_text' => $item['guide_text'],
|
|
'is_required' => (bool)$item['is_required'],
|
|
], $supplementItems),
|
|
] : null,
|
|
]);
|
|
}
|
|
|
|
public function bindMaterialTag(Request $request)
|
|
{
|
|
$id = (int)$request->input('id', 0);
|
|
$qrInput = trim((string)$request->input('qr_input', ''));
|
|
if ($id <= 0 || $qrInput === '') {
|
|
return api_error('任务 ID 和吊牌二维码不能为空', 422);
|
|
}
|
|
|
|
$task = Db::name('appraisal_tasks')->where('id', $id)->find();
|
|
if (!$task) {
|
|
return api_error('任务不存在', 404);
|
|
}
|
|
$operatorGuard = $this->guardTaskOperator($request, $task);
|
|
if ($operatorGuard['error']) {
|
|
return $operatorGuard['error'];
|
|
}
|
|
|
|
try {
|
|
$tag = (new MaterialTagService())->bindTagToReportByTask($id, $qrInput, $request);
|
|
} catch (\InvalidArgumentException $e) {
|
|
return api_error($e->getMessage(), 422);
|
|
} catch (\RuntimeException $e) {
|
|
return api_error($e->getMessage(), $e->getCode() ?: 404);
|
|
} catch (\Throwable $e) {
|
|
return api_error('吊牌绑定失败', 500, ['detail' => $e->getMessage()]);
|
|
}
|
|
|
|
return api_success([
|
|
'id' => $id,
|
|
'material_tag' => $tag,
|
|
], '吊牌已绑定');
|
|
}
|
|
|
|
public function scanTransferTag(Request $request)
|
|
{
|
|
try {
|
|
return api_success((new FulfillmentFlowService())->scanTransferForAppraisal(
|
|
(string)$request->input('internal_tag_no', ''),
|
|
$request
|
|
));
|
|
} catch (\InvalidArgumentException $e) {
|
|
return api_error($e->getMessage(), 422);
|
|
} catch (\RuntimeException $e) {
|
|
return api_error($e->getMessage(), $e->getCode() ?: 404);
|
|
} catch (\Throwable $e) {
|
|
return api_error('内部流转码识别失败', 500, ['detail' => $e->getMessage()]);
|
|
}
|
|
}
|
|
|
|
public function publishWithMaterialTag(Request $request)
|
|
{
|
|
$id = (int)$request->input('id', 0);
|
|
$qrInput = trim((string)$request->input('qr_input', ''));
|
|
if ($id <= 0 || $qrInput === '') {
|
|
return api_error('任务 ID 和验真吊牌不能为空', 422);
|
|
}
|
|
|
|
$task = Db::name('appraisal_tasks')->where('id', $id)->find();
|
|
if (!$task) {
|
|
return api_error('任务不存在', 404);
|
|
}
|
|
Db::startTrans();
|
|
try {
|
|
$tag = (new MaterialTagService())->bindTagToReportByTask($id, $qrInput, $request);
|
|
$report = $this->findLatestAppraisalReport((int)$task['order_id']);
|
|
if (!$report) {
|
|
Db::rollback();
|
|
return api_error('请先提交鉴定结论生成报告草稿', 422);
|
|
}
|
|
Db::commit();
|
|
} catch (\InvalidArgumentException $e) {
|
|
Db::rollback();
|
|
return api_error($e->getMessage(), 422);
|
|
} catch (\RuntimeException $e) {
|
|
Db::rollback();
|
|
return api_error($e->getMessage(), $e->getCode() ?: 404);
|
|
} catch (\Throwable $e) {
|
|
Db::rollback();
|
|
return api_error('验真吊牌绑定失败', 500, ['detail' => $e->getMessage()]);
|
|
}
|
|
|
|
return api_success([
|
|
'id' => $id,
|
|
'material_tag' => $tag,
|
|
'report' => [
|
|
'id' => (int)$report['id'],
|
|
'report_status' => (string)$report['report_status'],
|
|
'publish_time' => (string)($report['publish_time'] ?? ''),
|
|
'verify_url' => '',
|
|
'report_page_url' => '',
|
|
],
|
|
], '验真吊牌已绑定,报告待管理员发布');
|
|
}
|
|
|
|
public function saveZhongjianReport(Request $request)
|
|
{
|
|
$id = (int)$request->input('id', 0);
|
|
$reportNo = trim((string)$request->input('zhongjian_report_no', ''));
|
|
$qrInput = trim((string)$request->input('qr_input', ''));
|
|
$files = $this->evidenceService()->normalize($request->input('report_files', []), $request, true);
|
|
$productInput = $request->input('product_info', null);
|
|
$productPayload = is_array($productInput) ? $this->normalizeProductInput($productInput) : null;
|
|
$attachments = $this->evidenceService()->normalize($request->input('attachments', []), $request, true);
|
|
$keyPoints = $this->normalizeKeyPointInput($request->input('key_points', []));
|
|
$resultText = trim((string)$request->input('result_text', ''));
|
|
$resultDesc = trim((string)$request->input('result_desc', ''));
|
|
if ($id <= 0) {
|
|
return api_error('任务 ID 不能为空', 422);
|
|
}
|
|
if ($reportNo === '') {
|
|
return api_error('中检报告编号不能为空', 422);
|
|
}
|
|
if ($resultText === '') {
|
|
return api_error('鉴定结论不能为空', 422);
|
|
}
|
|
if (!$files) {
|
|
return api_error('请至少上传 1 个中检报告文件', 422);
|
|
}
|
|
if ($qrInput === '') {
|
|
return api_error('请扫描验真吊牌二维码', 422);
|
|
}
|
|
|
|
$task = Db::name('appraisal_tasks')->where('id', $id)->find();
|
|
if (!$task) {
|
|
return api_error('任务不存在', 404);
|
|
}
|
|
if (($task['service_provider'] ?? '') !== 'zhongjian') {
|
|
return api_error('非中检订单不能录入中检报告', 422);
|
|
}
|
|
$order = Db::name('orders')->where('id', (int)$task['order_id'])->find() ?: [];
|
|
$task['order_status'] = $order['order_status'] ?? '';
|
|
$report = $this->findLatestAppraisalReport((int)$task['order_id']);
|
|
$effectiveStatus = $this->effectiveTaskStatus($task, $report);
|
|
if ($effectiveStatus !== $task['status']) {
|
|
Db::name('appraisal_tasks')->where('id', $id)->update([
|
|
'status' => $effectiveStatus,
|
|
'updated_at' => date('Y-m-d H:i:s'),
|
|
]);
|
|
$task['status'] = $effectiveStatus;
|
|
}
|
|
if (in_array($effectiveStatus, ['submitted', 'completed'], true)) {
|
|
return api_error('当前任务已流转完成,不能再录入中检报告', 422);
|
|
}
|
|
|
|
$operatorGuard = $this->guardTaskOperator($request, $task);
|
|
if ($operatorGuard['error']) {
|
|
return $operatorGuard['error'];
|
|
}
|
|
|
|
$operatorId = (int)$request->header('x-admin-id', 0);
|
|
$operatorName = trim((string)$request->header('x-admin-name', ''));
|
|
$now = date('Y-m-d H:i:s');
|
|
|
|
Db::startTrans();
|
|
try {
|
|
if ($operatorGuard['task_update']) {
|
|
Db::name('appraisal_tasks')->where('id', $id)->update(array_merge($operatorGuard['task_update'], [
|
|
'updated_at' => $now,
|
|
]));
|
|
$task = array_merge($task, $operatorGuard['task_update']);
|
|
}
|
|
|
|
if ($productPayload !== null) {
|
|
$this->saveOrderProductSnapshot((int)$task['order_id'], $productPayload, $now);
|
|
}
|
|
|
|
if (!$this->hasSubmittableProductInfo((int)$task['order_id'], $productPayload)) {
|
|
Db::rollback();
|
|
return api_error('提交中检报告前请先完善物品信息', 422);
|
|
}
|
|
|
|
Db::name('appraisal_tasks')->where('id', $id)->update([
|
|
'status' => 'completed',
|
|
'started_at' => $task['started_at'] ?: $now,
|
|
'submitted_at' => $now,
|
|
'updated_at' => $now,
|
|
]);
|
|
|
|
Db::name('orders')->where('id', (int)$task['order_id'])->update([
|
|
'order_status' => 'generating_report',
|
|
'display_status' => '正在生成报告',
|
|
'updated_at' => $now,
|
|
]);
|
|
|
|
$resultPayload = [
|
|
'task_id' => $id,
|
|
'order_id' => (int)$task['order_id'],
|
|
'result_status' => 'zhongjian_report',
|
|
'result_text' => $resultText,
|
|
'result_desc' => $resultDesc,
|
|
'condition_grade' => '',
|
|
'condition_desc' => '',
|
|
'valuation_min' => 0,
|
|
'valuation_max' => 0,
|
|
'valuation_desc' => '',
|
|
'attachments_json' => $attachments ? json_encode($attachments, JSON_UNESCAPED_UNICODE) : null,
|
|
'external_remark' => '',
|
|
'internal_remark' => '中检报告编号:' . $reportNo,
|
|
'updated_at' => $now,
|
|
];
|
|
$resultId = Db::name('appraisal_task_results')->where('task_id', $id)->value('id');
|
|
if ($resultId) {
|
|
Db::name('appraisal_task_results')->where('id', (int)$resultId)->update($resultPayload);
|
|
$savedResultId = (int)$resultId;
|
|
} else {
|
|
$resultPayload['created_at'] = $now;
|
|
$savedResultId = (int)Db::name('appraisal_task_results')->insertGetId($resultPayload);
|
|
}
|
|
$this->saveTaskKeyPoints($savedResultId, $keyPoints, $now);
|
|
|
|
$draftChange = $this->createOrUpdateReportDraft((int)$task['order_id'], $task, $resultPayload, $now);
|
|
$report = $draftChange['report'];
|
|
if (!$report) {
|
|
Db::rollback();
|
|
return api_error('中检报告草稿生成失败', 500);
|
|
}
|
|
|
|
Db::name('reports')->where('id', (int)$report['id'])->update([
|
|
'zhongjian_report_no' => $reportNo,
|
|
'report_entry_admin_id' => $operatorId,
|
|
'report_entry_admin_name' => $operatorName,
|
|
'report_entered_at' => $now,
|
|
'updated_at' => $now,
|
|
]);
|
|
|
|
$content = Db::name('report_contents')->where('report_id', (int)$report['id'])->find();
|
|
if ($content) {
|
|
Db::name('report_contents')->where('id', (int)$content['id'])->update([
|
|
'zhongjian_report_files_json' => json_encode($files, JSON_UNESCAPED_UNICODE),
|
|
'updated_at' => $now,
|
|
]);
|
|
}
|
|
|
|
Db::name('order_timelines')->insert([
|
|
'order_id' => (int)$task['order_id'],
|
|
'node_code' => 'zhongjian_report_entered',
|
|
'node_text' => '中检报告已录入',
|
|
'node_desc' => '报告录入人已补全报告信息、录入中检报告编号并上传报告文件。',
|
|
'operator_type' => 'admin',
|
|
'operator_id' => $operatorId,
|
|
'occurred_at' => $now,
|
|
'created_at' => $now,
|
|
]);
|
|
|
|
$freshReport = $this->findLatestAppraisalReport((int)$task['order_id']);
|
|
if (!$freshReport) {
|
|
Db::rollback();
|
|
return api_error('中检报告草稿生成失败', 500);
|
|
}
|
|
|
|
$tag = (new MaterialTagService())->bindTagToReportByTask($id, $qrInput, $request);
|
|
$this->insertReportLog((int)$freshReport['id'], $draftChange['action'], $draftChange['before'], $freshReport, $request, '报告已提交,待管理员发布');
|
|
$this->insertReportLog((int)$freshReport['id'], 'submit', $draftChange['before'], $freshReport, $request, '鉴定师提交报告');
|
|
|
|
Db::commit();
|
|
|
|
return api_success([
|
|
'id' => $id,
|
|
'material_tag' => $tag,
|
|
'report' => [
|
|
'id' => (int)$freshReport['id'],
|
|
'report_status' => (string)$freshReport['report_status'],
|
|
'publish_time' => (string)($freshReport['publish_time'] ?? ''),
|
|
'verify_url' => '',
|
|
'report_page_url' => '',
|
|
],
|
|
], '报告已提交,待管理员发布');
|
|
} catch (\Throwable $e) {
|
|
Db::rollback();
|
|
return api_error('中检报告录入失败', 500, ['detail' => $e->getMessage()]);
|
|
}
|
|
}
|
|
|
|
public function assignableAdmins(Request $request)
|
|
{
|
|
$id = (int)$request->input('id', 0);
|
|
if (!$id) {
|
|
return api_error('任务 ID 不能为空', 422);
|
|
}
|
|
|
|
$task = Db::name('appraisal_tasks')->where('id', $id)->find();
|
|
if (!$task) {
|
|
return api_error('任务不存在', 404);
|
|
}
|
|
|
|
return api_success([
|
|
'list' => $this->findAssignableAdmins((string)$task['task_stage']),
|
|
]);
|
|
}
|
|
|
|
public function assign(Request $request)
|
|
{
|
|
$id = (int)$request->input('id', 0);
|
|
$assigneeId = (int)$request->input('assignee_id', 0);
|
|
if (!$id || !$assigneeId) {
|
|
return api_error('任务 ID 和处理人不能为空', 422);
|
|
}
|
|
|
|
$task = Db::name('appraisal_tasks')->where('id', $id)->find();
|
|
if (!$task) {
|
|
return api_error('任务不存在', 404);
|
|
}
|
|
if (in_array((string)($task['status'] ?? ''), ['submitted', 'completed'], true)) {
|
|
return api_error('当前任务已流转完成,不能再修改处理人', 422);
|
|
}
|
|
|
|
$candidate = $this->findAssignableAdminById($assigneeId, (string)$task['task_stage']);
|
|
if (!$candidate) {
|
|
return api_error('所选管理员角色不适用于当前任务阶段', 422);
|
|
}
|
|
|
|
Db::name('appraisal_tasks')->where('id', $id)->update([
|
|
'assignee_id' => (int)$candidate['id'],
|
|
'assignee_name' => (string)$candidate['name'],
|
|
'updated_at' => date('Y-m-d H:i:s'),
|
|
]);
|
|
|
|
return api_success([
|
|
'id' => $id,
|
|
'assignee_id' => (int)$candidate['id'],
|
|
'assignee_name' => (string)$candidate['name'],
|
|
], '处理人已分配');
|
|
}
|
|
|
|
public function saveResult(Request $request)
|
|
{
|
|
$id = (int)$request->input('id', 0);
|
|
$action = trim((string)$request->input('action', 'save'));
|
|
$qrInput = trim((string)$request->input('qr_input', ''));
|
|
|
|
if (!$id) {
|
|
return api_error('任务 ID 不能为空', 422);
|
|
}
|
|
|
|
$task = Db::name('appraisal_tasks')->where('id', $id)->find();
|
|
if (!$task) {
|
|
return api_error('任务不存在', 404);
|
|
}
|
|
|
|
$order = Db::name('orders')->where('id', (int)$task['order_id'])->find() ?: [];
|
|
$task['order_status'] = $order['order_status'] ?? '';
|
|
$report = $this->findLatestAppraisalReport((int)$task['order_id']);
|
|
$effectiveStatus = $this->effectiveTaskStatus($task, $report);
|
|
if ($effectiveStatus !== $task['status']) {
|
|
Db::name('appraisal_tasks')->where('id', $id)->update([
|
|
'status' => $effectiveStatus,
|
|
'updated_at' => date('Y-m-d H:i:s'),
|
|
]);
|
|
$task['status'] = $effectiveStatus;
|
|
}
|
|
|
|
if (in_array($effectiveStatus, ['submitted', 'completed'], true)) {
|
|
return api_error('当前任务已流转完成,不能再编辑或提交', 422);
|
|
}
|
|
|
|
$operatorGuard = $this->guardTaskOperator($request, $task);
|
|
if ($operatorGuard['error']) {
|
|
return $operatorGuard['error'];
|
|
}
|
|
|
|
$resultText = trim((string)$request->input('result_text', ''));
|
|
if ($action !== 'save' && $resultText === '') {
|
|
return api_error('鉴定结论不能为空', 422);
|
|
}
|
|
if ($action !== 'save' && $qrInput === '') {
|
|
return api_error('请扫描验真吊牌二维码', 422);
|
|
}
|
|
$productInput = $request->input('product_info', null);
|
|
$productPayload = is_array($productInput) ? $this->normalizeProductInput($productInput) : null;
|
|
$attachments = $this->evidenceService()->normalize($request->input('attachments', []), $request, true);
|
|
$keyPoints = $this->normalizeKeyPointInput($request->input('key_points', []));
|
|
$payload = [
|
|
'task_id' => $id,
|
|
'order_id' => (int)$task['order_id'],
|
|
'result_status' => $this->mapResultStatus($resultText),
|
|
'result_text' => $resultText,
|
|
'result_desc' => trim((string)$request->input('result_desc', '')),
|
|
'condition_grade' => trim((string)$request->input('condition_grade', '')),
|
|
'condition_desc' => trim((string)$request->input('condition_desc', '')),
|
|
'valuation_min' => (float)$request->input('valuation_min', 0),
|
|
'valuation_max' => (float)$request->input('valuation_max', 0),
|
|
'valuation_desc' => trim((string)$request->input('valuation_desc', '')),
|
|
'attachments_json' => $attachments ? json_encode($attachments, JSON_UNESCAPED_UNICODE) : null,
|
|
'external_remark' => trim((string)$request->input('external_remark', '')),
|
|
'internal_remark' => trim((string)$request->input('internal_remark', '')),
|
|
'updated_at' => date('Y-m-d H:i:s'),
|
|
];
|
|
|
|
if ($action !== 'save' && ($task['order_status'] ?? '') === 'pending_shipping') {
|
|
return api_error('订单尚未确认到仓,不能提交鉴定结论', 422);
|
|
}
|
|
|
|
$now = date('Y-m-d H:i:s');
|
|
|
|
Db::startTrans();
|
|
try {
|
|
if ($operatorGuard['task_update']) {
|
|
Db::name('appraisal_tasks')->where('id', $id)->update(array_merge($operatorGuard['task_update'], [
|
|
'updated_at' => $now,
|
|
]));
|
|
$task = array_merge($task, $operatorGuard['task_update']);
|
|
}
|
|
|
|
if ($productPayload !== null) {
|
|
$this->saveOrderProductSnapshot((int)$task['order_id'], $productPayload, $now);
|
|
}
|
|
|
|
$resultId = Db::name('appraisal_task_results')->where('task_id', $id)->value('id');
|
|
if ($resultId) {
|
|
Db::name('appraisal_task_results')->where('id', $resultId)->update($payload);
|
|
$savedResultId = (int)$resultId;
|
|
} else {
|
|
$payload['created_at'] = $now;
|
|
$savedResultId = (int)Db::name('appraisal_task_results')->insertGetId($payload);
|
|
}
|
|
|
|
$this->saveTaskKeyPoints($savedResultId, $keyPoints, $now);
|
|
|
|
if ($action === 'save') {
|
|
$taskUpdate = [
|
|
'status' => 'processing',
|
|
'updated_at' => $now,
|
|
];
|
|
if (!$task['started_at']) {
|
|
$taskUpdate['started_at'] = $now;
|
|
}
|
|
Db::name('appraisal_tasks')->where('id', $id)->update($taskUpdate);
|
|
Db::commit();
|
|
return api_success(['id' => $id], '结论已保存');
|
|
}
|
|
|
|
if ($resultText === '') {
|
|
Db::rollback();
|
|
return api_error('提交结论前请选择鉴定结论', 422);
|
|
}
|
|
|
|
if (!$this->hasSubmittableProductInfo((int)$task['order_id'], $productPayload)) {
|
|
Db::rollback();
|
|
return api_error('提交结论前请先完善物品信息', 422);
|
|
}
|
|
|
|
Db::name('appraisal_tasks')->where('id', $id)->update([
|
|
'status' => 'completed',
|
|
'started_at' => $task['started_at'] ?: $now,
|
|
'submitted_at' => $now,
|
|
'updated_at' => $now,
|
|
]);
|
|
|
|
Db::name('orders')->where('id', $task['order_id'])->update([
|
|
'order_status' => 'generating_report',
|
|
'display_status' => '正在生成报告',
|
|
'updated_at' => $now,
|
|
]);
|
|
|
|
Db::name('order_timelines')->insert([
|
|
'order_id' => $task['order_id'],
|
|
'node_code' => 'generating_report',
|
|
'node_text' => '正在生成报告',
|
|
'node_desc' => '鉴定已完成,系统正在生成正式报告草稿',
|
|
'operator_type' => 'admin',
|
|
'operator_id' => (int)$request->header('x-admin-id', 0),
|
|
'occurred_at' => $now,
|
|
'created_at' => $now,
|
|
]);
|
|
|
|
$draftChange = $this->createOrUpdateReportDraft((int)$task['order_id'], $task, $payload, $now);
|
|
$report = $draftChange['report'];
|
|
if (!$report) {
|
|
Db::rollback();
|
|
return api_error('报告草稿生成失败', 500);
|
|
}
|
|
$tag = (new MaterialTagService())->bindTagToReportByTask($id, $qrInput, $request);
|
|
$freshReport = $this->findLatestAppraisalReport((int)$task['order_id']) ?: $report;
|
|
$this->insertReportLog((int)$freshReport['id'], $draftChange['action'], $draftChange['before'], $freshReport, $request, '报告已提交,待管理员发布');
|
|
$this->insertReportLog((int)$freshReport['id'], 'submit', $draftChange['before'], $freshReport, $request, '鉴定师提交报告');
|
|
|
|
Db::commit();
|
|
(new EnterpriseWebhookService())->recordOrderEvent((int)$task['order_id'], 'appraisal_finished', [
|
|
'task_id' => $id,
|
|
'task_stage' => $task['task_stage'],
|
|
'finished_at' => $now,
|
|
]);
|
|
return api_success([
|
|
'id' => $id,
|
|
'material_tag' => $tag,
|
|
'report' => [
|
|
'id' => (int)$freshReport['id'],
|
|
'report_status' => (string)$freshReport['report_status'],
|
|
'publish_time' => (string)($freshReport['publish_time'] ?? ''),
|
|
'verify_url' => '',
|
|
'report_page_url' => '',
|
|
],
|
|
], '报告已提交,待管理员发布');
|
|
} catch (\Throwable $e) {
|
|
Db::rollback();
|
|
return api_error('结论保存失败', 500, [
|
|
'detail' => $e->getMessage(),
|
|
]);
|
|
}
|
|
}
|
|
|
|
public function requestSupplement(Request $request)
|
|
{
|
|
$id = (int)$request->input('id', 0);
|
|
$reason = trim((string)$request->input('reason', ''));
|
|
$deadline = trim((string)$request->input('deadline', ''));
|
|
$items = $request->input('items', []);
|
|
|
|
if (!$id) {
|
|
return api_error('任务 ID 不能为空', 422);
|
|
}
|
|
|
|
if ($reason === '') {
|
|
return api_error('补资料原因不能为空', 422);
|
|
}
|
|
|
|
$task = Db::name('appraisal_tasks')->where('id', $id)->find();
|
|
if (!$task) {
|
|
return api_error('任务不存在', 404);
|
|
}
|
|
|
|
$order = Db::name('orders')->where('id', (int)$task['order_id'])->find() ?: [];
|
|
$task['order_status'] = $order['order_status'] ?? '';
|
|
$report = $this->findLatestAppraisalReport((int)$task['order_id']);
|
|
$effectiveStatus = $this->effectiveTaskStatus($task, $report);
|
|
if ($effectiveStatus !== $task['status']) {
|
|
Db::name('appraisal_tasks')->where('id', $id)->update([
|
|
'status' => $effectiveStatus,
|
|
'updated_at' => date('Y-m-d H:i:s'),
|
|
]);
|
|
$task['status'] = $effectiveStatus;
|
|
}
|
|
|
|
if (in_array($effectiveStatus, ['submitted', 'completed'], true)) {
|
|
return api_error('当前任务已流转完成,不能再发起补资料', 422);
|
|
}
|
|
|
|
$operatorGuard = $this->guardTaskOperator($request, $task);
|
|
if ($operatorGuard['error']) {
|
|
return $operatorGuard['error'];
|
|
}
|
|
|
|
if (!is_array($items) || !$items) {
|
|
return api_error('请至少填写一项补资料要求', 422);
|
|
}
|
|
|
|
$normalizedItems = [];
|
|
foreach ($items as $index => $item) {
|
|
if (!is_array($item)) {
|
|
continue;
|
|
}
|
|
|
|
$itemName = trim((string)($item['item_name'] ?? ''));
|
|
$guideText = trim((string)($item['guide_text'] ?? ''));
|
|
$isRequired = array_key_exists('is_required', $item) ? (bool)$item['is_required'] : true;
|
|
|
|
if ($itemName === '') {
|
|
continue;
|
|
}
|
|
|
|
$normalizedItems[] = [
|
|
'item_code' => 'supplement_' . ($index + 1),
|
|
'item_name' => $itemName,
|
|
'guide_text' => $guideText,
|
|
'is_required' => $isRequired ? 1 : 0,
|
|
];
|
|
}
|
|
|
|
if (!$normalizedItems) {
|
|
return api_error('请至少填写一项有效的补资料要求', 422);
|
|
}
|
|
|
|
$now = date('Y-m-d H:i:s');
|
|
|
|
Db::startTrans();
|
|
try {
|
|
if ($operatorGuard['task_update']) {
|
|
Db::name('appraisal_tasks')->where('id', $id)->update(array_merge($operatorGuard['task_update'], [
|
|
'updated_at' => $now,
|
|
]));
|
|
$task = array_merge($task, $operatorGuard['task_update']);
|
|
}
|
|
|
|
Db::name('order_supplement_tasks')
|
|
->where('order_id', $task['order_id'])
|
|
->where('status', 'pending')
|
|
->update([
|
|
'status' => 'closed',
|
|
'updated_at' => $now,
|
|
]);
|
|
|
|
$supplementTaskId = (int)Db::name('order_supplement_tasks')->insertGetId([
|
|
'order_id' => $task['order_id'],
|
|
'reason' => $reason,
|
|
'deadline' => $deadline !== '' ? $deadline : null,
|
|
'status' => 'pending',
|
|
'created_by' => (int)$request->header('x-admin-id', 0),
|
|
'submitted_at' => null,
|
|
'approved_at' => null,
|
|
'created_at' => $now,
|
|
'updated_at' => $now,
|
|
]);
|
|
|
|
foreach ($normalizedItems as $item) {
|
|
Db::name('order_supplement_task_items')->insert([
|
|
'task_id' => $supplementTaskId,
|
|
'item_code' => $item['item_code'],
|
|
'item_name' => $item['item_name'],
|
|
'guide_text' => $item['guide_text'],
|
|
'sample_image_url' => '',
|
|
'is_required' => $item['is_required'],
|
|
'created_at' => $now,
|
|
'updated_at' => $now,
|
|
]);
|
|
|
|
Db::name('order_upload_items')->insert([
|
|
'order_id' => $task['order_id'],
|
|
'template_id' => null,
|
|
'item_code' => $item['item_code'],
|
|
'item_name' => $item['item_name'],
|
|
'is_required' => $item['is_required'],
|
|
'source_type' => 'supplement',
|
|
'status' => 'pending',
|
|
'created_at' => $now,
|
|
'updated_at' => $now,
|
|
]);
|
|
}
|
|
|
|
Db::name('appraisal_tasks')->where('id', $id)->update([
|
|
'status' => 'returned',
|
|
'started_at' => $task['started_at'] ?: $now,
|
|
'updated_at' => $now,
|
|
]);
|
|
|
|
Db::name('orders')->where('id', $task['order_id'])->update([
|
|
'order_status' => 'pending_supplement',
|
|
'display_status' => '等待您补充资料',
|
|
'updated_at' => $now,
|
|
]);
|
|
|
|
Db::name('order_timelines')->insert([
|
|
'order_id' => $task['order_id'],
|
|
'node_code' => 'supplement',
|
|
'node_text' => '待补资料',
|
|
'node_desc' => $reason,
|
|
'operator_type' => 'admin',
|
|
'operator_id' => (int)$request->header('x-admin-id', 0),
|
|
'occurred_at' => $now,
|
|
'created_at' => $now,
|
|
]);
|
|
|
|
$order = Db::name('orders')->where('id', $task['order_id'])->find();
|
|
|
|
(new MessageDispatcher())->sendInboxEvent('supplement_required', [
|
|
'user_id' => (int)($order['user_id'] ?? 0),
|
|
'biz_type' => 'supplement',
|
|
'biz_id' => $supplementTaskId,
|
|
'reason' => $reason,
|
|
'deadline' => $deadline,
|
|
'fallback_title' => '请补充鉴定资料',
|
|
'fallback_content' => '鉴定师需要您补充资料后继续处理,请尽快进入订单详情查看。',
|
|
]);
|
|
|
|
Db::commit();
|
|
|
|
(new EnterpriseWebhookService())->recordOrderEvent((int)$task['order_id'], 'supplement_required', [
|
|
'task_id' => $id,
|
|
'supplement_task_id' => $supplementTaskId,
|
|
'reason' => $reason,
|
|
'deadline' => $deadline,
|
|
'items' => $normalizedItems,
|
|
]);
|
|
|
|
return api_success([
|
|
'id' => $id,
|
|
'supplement_task_id' => $supplementTaskId,
|
|
], '已发起补资料要求');
|
|
} catch (\Throwable $e) {
|
|
Db::rollback();
|
|
return api_error('发起补资料失败', 500, [
|
|
'detail' => $e->getMessage(),
|
|
]);
|
|
}
|
|
}
|
|
|
|
public function uploadEvidenceFile(Request $request)
|
|
{
|
|
$taskId = (int)$request->input('task_id', 0);
|
|
if ($taskId <= 0) {
|
|
return api_error('任务 ID 不能为空', 422);
|
|
}
|
|
$editableGuard = $this->guardTaskEditable($taskId, '当前任务已流转完成,不能再上传附件');
|
|
if ($editableGuard) {
|
|
return $editableGuard;
|
|
}
|
|
|
|
try {
|
|
$scene = (string)$request->input('upload_scene', 'appraisal_evidence');
|
|
if (!in_array($scene, ['appraisal_evidence', 'zhongjian_report'], true)) {
|
|
$scene = 'appraisal_evidence';
|
|
}
|
|
$asset = $this->evidenceService()->upload($request, 'file', $scene);
|
|
return api_success($asset);
|
|
} catch (\Throwable $e) {
|
|
return api_error($e->getMessage(), 422);
|
|
}
|
|
}
|
|
|
|
public function deleteEvidenceFile(Request $request)
|
|
{
|
|
$taskId = (int)$request->input('task_id', 0);
|
|
if ($taskId <= 0) {
|
|
return api_error('任务 ID 不能为空', 422);
|
|
}
|
|
$editableGuard = $this->guardTaskEditable($taskId, '当前任务已流转完成,不能再删除附件');
|
|
if ($editableGuard) {
|
|
return $editableGuard;
|
|
}
|
|
|
|
$fileUrl = trim((string)$request->input('file_url', ''));
|
|
if ($fileUrl === '') {
|
|
return api_error('文件地址不能为空', 422);
|
|
}
|
|
|
|
$this->evidenceService()->delete($fileUrl);
|
|
|
|
return api_success([
|
|
'file_url' => $fileUrl,
|
|
], '删除成功');
|
|
}
|
|
|
|
private function buildTaskBaseQuery()
|
|
{
|
|
return Db::name('appraisal_tasks')
|
|
->alias('t')
|
|
->leftJoin('orders o', 'o.id = t.order_id')
|
|
->leftJoin('order_products p', 'p.order_id = t.order_id')
|
|
->leftJoin('appraisal_task_results r', 'r.task_id = t.id')
|
|
->leftJoin('enterprise_customer_order_refs ecr', 'ecr.order_id = t.order_id')
|
|
->field([
|
|
't.id',
|
|
't.order_id',
|
|
't.task_stage',
|
|
't.status',
|
|
't.assignee_id',
|
|
't.assignee_name',
|
|
't.started_at',
|
|
't.submitted_at',
|
|
't.sla_deadline',
|
|
't.is_overtime',
|
|
'o.order_no',
|
|
'o.appraisal_no',
|
|
'ecr.external_order_no',
|
|
'o.service_provider',
|
|
'o.order_status',
|
|
'o.display_status',
|
|
'p.product_name',
|
|
'p.category_id',
|
|
'p.category_name',
|
|
'p.brand_id',
|
|
'p.brand_name',
|
|
'r.result_text',
|
|
]);
|
|
}
|
|
|
|
private function applyTaskScopeFilter($query, Request $request, string $scope): void
|
|
{
|
|
if ($scope !== 'my') {
|
|
return;
|
|
}
|
|
|
|
$adminId = (int)$request->header('x-admin-id', 0);
|
|
if ($adminId <= 0) {
|
|
return;
|
|
}
|
|
|
|
$query->whereRaw('(t.assignee_id = :scope_admin_id OR t.assignee_id IS NULL OR t.assignee_id = 0)', [
|
|
'scope_admin_id' => $adminId,
|
|
]);
|
|
}
|
|
|
|
private function applyTaskScopeFilterRows(array &$rows, Request $request, string $scope): void
|
|
{
|
|
if ($scope !== 'my') {
|
|
return;
|
|
}
|
|
|
|
$adminId = (int)$request->header('x-admin-id', 0);
|
|
if ($adminId <= 0) {
|
|
return;
|
|
}
|
|
|
|
$rows = array_values(array_filter($rows, function (array $row) use ($adminId) {
|
|
$assigneeId = (int)($row['assignee_id'] ?? 0);
|
|
return $assigneeId <= 0 || $assigneeId === $adminId;
|
|
}));
|
|
}
|
|
|
|
private function normalizeTaskListRow(array $item, ?array $report = null): array
|
|
{
|
|
$effectiveStatus = $this->effectiveTaskStatus($item, $report);
|
|
return [
|
|
'id' => (int)$item['id'],
|
|
'order_id' => (int)$item['order_id'],
|
|
'order_no' => $item['order_no'],
|
|
'appraisal_no' => $item['appraisal_no'],
|
|
'external_order_no' => $item['external_order_no'] ?: '',
|
|
'service_provider' => $item['service_provider'],
|
|
'service_provider_text' => $item['service_provider'] === 'zhongjian' ? '中检鉴定' : '实物鉴定',
|
|
'task_stage' => $item['task_stage'],
|
|
'task_stage_text' => '鉴定',
|
|
'status' => $effectiveStatus,
|
|
'status_text' => $this->taskStatusText($effectiveStatus),
|
|
'assignee_id' => (int)($item['assignee_id'] ?? 0),
|
|
'product_name' => $item['product_name'] ?: '待完善物品信息',
|
|
'category_id' => (int)($item['category_id'] ?? 0),
|
|
'category_name' => $item['category_name'] ?: '',
|
|
'brand_id' => (int)($item['brand_id'] ?? 0),
|
|
'brand_name' => $item['brand_name'] ?: '',
|
|
'assignee_name' => $item['assignee_name'] ?: '未分配',
|
|
'result_text' => $item['result_text'] ?: '',
|
|
'started_at' => $item['started_at'],
|
|
'submitted_at' => $item['submitted_at'],
|
|
'sla_deadline' => $item['sla_deadline'],
|
|
'is_overtime' => (bool)$item['is_overtime'],
|
|
'display_status' => $item['display_status'],
|
|
'internal_tag_no' => (string)($item['internal_tag_no'] ?? ''),
|
|
];
|
|
}
|
|
|
|
private function attachTransferFlowToRows(array &$rows): void
|
|
{
|
|
$orderIds = array_values(array_unique(array_filter(array_map(fn (array $item) => (int)($item['order_id'] ?? 0), $rows))));
|
|
if (!$orderIds) {
|
|
return;
|
|
}
|
|
|
|
$flowMap = $this->latestTransferFlowMap($orderIds);
|
|
foreach ($rows as &$row) {
|
|
$orderId = (int)($row['order_id'] ?? 0);
|
|
$row['internal_tag_no'] = (string)($flowMap[$orderId]['internal_tag_no'] ?? '');
|
|
}
|
|
unset($row);
|
|
}
|
|
|
|
private function latestTransferFlowForOrder(int $orderId): ?array
|
|
{
|
|
if ($orderId <= 0) {
|
|
return null;
|
|
}
|
|
|
|
return Db::name('order_transfer_flows')
|
|
->where('order_id', $orderId)
|
|
->order('id', 'desc')
|
|
->find() ?: null;
|
|
}
|
|
|
|
private function latestTransferFlowMap(array $orderIds): array
|
|
{
|
|
$orderIds = array_values(array_unique(array_filter(array_map('intval', $orderIds))));
|
|
if (!$orderIds) {
|
|
return [];
|
|
}
|
|
|
|
$rows = Db::name('order_transfer_flows')
|
|
->whereIn('order_id', $orderIds)
|
|
->order('id', 'desc')
|
|
->select()
|
|
->toArray();
|
|
|
|
$map = [];
|
|
foreach ($rows as $row) {
|
|
$orderId = (int)($row['order_id'] ?? 0);
|
|
if ($orderId > 0 && !isset($map[$orderId])) {
|
|
$map[$orderId] = [
|
|
'internal_tag_no' => (string)($row['internal_tag_no'] ?? ''),
|
|
];
|
|
}
|
|
}
|
|
|
|
return $map;
|
|
}
|
|
|
|
private function guardTaskEditable(int $taskId, string $message)
|
|
{
|
|
$task = Db::name('appraisal_tasks')->where('id', $taskId)->find();
|
|
if (!$task) {
|
|
return api_error('任务不存在', 404);
|
|
}
|
|
|
|
$order = Db::name('orders')->where('id', (int)$task['order_id'])->find() ?: [];
|
|
$task['order_status'] = $order['order_status'] ?? '';
|
|
$report = $this->findLatestAppraisalReport((int)$task['order_id']);
|
|
$effectiveStatus = $this->effectiveTaskStatus($task, $report);
|
|
if ($effectiveStatus !== (string)$task['status']) {
|
|
Db::name('appraisal_tasks')->where('id', $taskId)->update([
|
|
'status' => $effectiveStatus,
|
|
'updated_at' => date('Y-m-d H:i:s'),
|
|
]);
|
|
}
|
|
|
|
return in_array($effectiveStatus, ['submitted', 'completed'], true)
|
|
? api_error($message, 422)
|
|
: null;
|
|
}
|
|
|
|
private function formatResultInfo(array $task, ?Request $request = null): array
|
|
{
|
|
$resultId = 0;
|
|
if (!empty($task['result_id'])) {
|
|
$resultId = (int)$task['result_id'];
|
|
} elseif (!empty($task['id'])) {
|
|
$resultId = (int)Db::name('appraisal_task_results')->where('task_id', (int)$task['id'])->value('id');
|
|
}
|
|
|
|
return [
|
|
'result_text' => $task['result_text'] ?: '',
|
|
'result_desc' => $task['result_desc'] ?: '',
|
|
'condition_grade' => $task['condition_grade'] ?: '',
|
|
'condition_desc' => ($task['result_condition_desc'] ?? $task['condition_desc'] ?? '') ?: '',
|
|
'valuation_min' => (float)($task['valuation_min'] ?? 0),
|
|
'valuation_max' => (float)($task['valuation_max'] ?? 0),
|
|
'valuation_desc' => ($task['result_valuation_desc'] ?? $task['valuation_desc'] ?? '') ?: '',
|
|
'attachments' => $this->evidenceService()->normalize($task['result_attachments_json'] ?? $task['attachments_json'] ?? null, $request),
|
|
'external_remark' => $task['external_remark'] ?: '',
|
|
'internal_remark' => $task['internal_remark'] ?: '',
|
|
'key_points' => $resultId > 0 ? $this->loadTaskKeyPoints($resultId) : [],
|
|
];
|
|
}
|
|
|
|
private function hasResultData(array $resultInfo): bool
|
|
{
|
|
return $resultInfo['result_text'] !== ''
|
|
|| $resultInfo['result_desc'] !== ''
|
|
|| $resultInfo['condition_grade'] !== ''
|
|
|| $resultInfo['condition_desc'] !== ''
|
|
|| (float)$resultInfo['valuation_min'] > 0
|
|
|| (float)$resultInfo['valuation_max'] > 0
|
|
|| $resultInfo['valuation_desc'] !== ''
|
|
|| !empty($resultInfo['attachments'])
|
|
|| $this->hasKeyPointData($resultInfo['key_points'] ?? [])
|
|
|| $resultInfo['external_remark'] !== ''
|
|
|| $resultInfo['internal_remark'] !== '';
|
|
}
|
|
|
|
private function hasKeyPointData(array $keyPoints): bool
|
|
{
|
|
foreach ($keyPoints as $point) {
|
|
if (!is_array($point)) {
|
|
continue;
|
|
}
|
|
if (trim((string)($point['point_value'] ?? '')) !== '' || trim((string)($point['point_remark'] ?? '')) !== '') {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private function buildGroupedTaskList(array $rows, array $reportMap = []): array
|
|
{
|
|
$grouped = [];
|
|
foreach ($rows as $item) {
|
|
$row = $this->normalizeTaskListRow($item, $reportMap[(int)$item['order_id']] ?? null);
|
|
$grouped[$row['order_id']][] = $row;
|
|
}
|
|
|
|
$list = [];
|
|
foreach ($grouped as $tasks) {
|
|
usort($tasks, fn (array $a, array $b) => $this->stagePriority($a['task_stage']) <=> $this->stagePriority($b['task_stage']));
|
|
$currentTask = $this->selectCurrentTask($tasks);
|
|
$latestResultText = $this->latestResultText($tasks);
|
|
$latestSubmittedAt = $this->latestSubmittedAt($tasks);
|
|
|
|
$list[] = array_merge($currentTask, [
|
|
'result_text' => $latestResultText,
|
|
'submitted_at' => $latestSubmittedAt,
|
|
'stage_tasks' => array_map(function (array $task) use ($currentTask) {
|
|
$task['is_current'] = $task['id'] === $currentTask['id'];
|
|
return $task;
|
|
}, $tasks),
|
|
]);
|
|
}
|
|
|
|
usort($list, fn (array $a, array $b) => $b['id'] <=> $a['id']);
|
|
return $list;
|
|
}
|
|
|
|
private function selectCurrentTask(array $tasks): array
|
|
{
|
|
$sorted = $tasks;
|
|
usort($sorted, function (array $a, array $b) {
|
|
$stageCompare = $this->stagePriority($b['task_stage']) <=> $this->stagePriority($a['task_stage']);
|
|
if ($stageCompare !== 0) {
|
|
return $stageCompare;
|
|
}
|
|
|
|
$statusCompare = $this->statusPriority($b['status']) <=> $this->statusPriority($a['status']);
|
|
if ($statusCompare !== 0) {
|
|
return $statusCompare;
|
|
}
|
|
|
|
return $b['id'] <=> $a['id'];
|
|
});
|
|
|
|
return $sorted[0];
|
|
}
|
|
|
|
private function latestResultText(array $tasks): string
|
|
{
|
|
$sorted = $tasks;
|
|
usort($sorted, function (array $a, array $b) {
|
|
$stageCompare = $this->stagePriority($b['task_stage']) <=> $this->stagePriority($a['task_stage']);
|
|
if ($stageCompare !== 0) {
|
|
return $stageCompare;
|
|
}
|
|
|
|
return $b['id'] <=> $a['id'];
|
|
});
|
|
|
|
foreach ($sorted as $task) {
|
|
if ($task['result_text'] !== '') {
|
|
return $task['result_text'];
|
|
}
|
|
}
|
|
|
|
return '';
|
|
}
|
|
|
|
private function latestSubmittedAt(array $tasks): ?string
|
|
{
|
|
$submitted = array_values(array_filter(array_map(fn (array $task) => $task['submitted_at'], $tasks)));
|
|
if (!$submitted) {
|
|
return null;
|
|
}
|
|
|
|
rsort($submitted);
|
|
return $submitted[0];
|
|
}
|
|
|
|
private function stagePriority(string $stage): int
|
|
{
|
|
return match ($stage) {
|
|
'first_review' => 1,
|
|
'final_review' => 2,
|
|
default => 0,
|
|
};
|
|
}
|
|
|
|
private function statusPriority(string $status): int
|
|
{
|
|
return match ($status) {
|
|
'processing' => 5,
|
|
'pending' => 4,
|
|
'returned' => 3,
|
|
'submitted' => 2,
|
|
'completed' => 1,
|
|
default => 0,
|
|
};
|
|
}
|
|
|
|
private function buildAppraisalReportMap(array $orderIds): array
|
|
{
|
|
if (!$orderIds) {
|
|
return [];
|
|
}
|
|
|
|
$rows = Db::name('reports')
|
|
->whereIn('order_id', $orderIds)
|
|
->where('report_type', 'appraisal')
|
|
->order('id', 'desc')
|
|
->select()
|
|
->toArray();
|
|
|
|
$map = [];
|
|
foreach ($rows as $row) {
|
|
$orderId = (int)$row['order_id'];
|
|
if (!isset($map[$orderId])) {
|
|
$map[$orderId] = $row;
|
|
}
|
|
}
|
|
|
|
return $map;
|
|
}
|
|
|
|
private function workbenchVisibleOrderStatuses(): array
|
|
{
|
|
return [
|
|
'received',
|
|
'in_first_review',
|
|
'pending_supplement',
|
|
'in_final_review',
|
|
'generating_report',
|
|
'report_published',
|
|
'completed',
|
|
];
|
|
}
|
|
|
|
private function workbenchVisibleOrderStatusSql(): string
|
|
{
|
|
$quoted = array_map(fn (string $status) => "'" . addslashes($status) . "'", $this->workbenchVisibleOrderStatuses());
|
|
return '(o.order_status IN (' . implode(', ', $quoted) . ") OR (o.order_status = 'pending_shipping' AND o.source_channel = 'enterprise_push'))";
|
|
}
|
|
|
|
private function findLatestAppraisalReport(int $orderId): ?array
|
|
{
|
|
return Db::name('reports')
|
|
->where('order_id', $orderId)
|
|
->where('report_type', 'appraisal')
|
|
->order('id', 'desc')
|
|
->find() ?: null;
|
|
}
|
|
|
|
private function effectiveTaskStatus(array $task, ?array $report = null): string
|
|
{
|
|
$status = (string)($task['status'] ?? '');
|
|
$stage = (string)($task['task_stage'] ?? '');
|
|
$submittedAt = (string)($task['submitted_at'] ?? '');
|
|
$orderStatus = (string)($task['order_status'] ?? '');
|
|
$reportStatus = $report ? (string)($report['report_status'] ?? '') : '';
|
|
|
|
if ($reportStatus === 'published') {
|
|
return 'completed';
|
|
}
|
|
|
|
if (in_array($reportStatus, ['draft', 'pending_publish', 'updated', 'rejected'], true)) {
|
|
return 'processing';
|
|
}
|
|
|
|
if (
|
|
$submittedAt !== ''
|
|
&& (
|
|
$status === 'completed'
|
|
|| $report
|
|
|| in_array($orderStatus, ['generating_report', 'report_published', 'completed'], true)
|
|
)
|
|
) {
|
|
return 'completed';
|
|
}
|
|
|
|
if ($submittedAt !== '' && $status !== 'completed') {
|
|
return 'submitted';
|
|
}
|
|
|
|
return $status;
|
|
}
|
|
|
|
private function taskStatusText(string $status): string
|
|
{
|
|
return match ($status) {
|
|
'pending' => '待处理',
|
|
'processing' => '处理中',
|
|
'submitted' => '已提交',
|
|
'returned' => '待用户补料',
|
|
'completed' => '已完成',
|
|
default => $status,
|
|
};
|
|
}
|
|
|
|
private function reportStatusText(string $status): string
|
|
{
|
|
return match ($status) {
|
|
'draft' => '草稿中',
|
|
'pending_publish' => '待发布',
|
|
'published' => '已发布',
|
|
'rejected' => '已驳回',
|
|
'updated' => '已更新',
|
|
'invalid' => '已作废',
|
|
default => $status,
|
|
};
|
|
}
|
|
|
|
private function mapResultStatus(string $resultText): string
|
|
{
|
|
return match ($resultText) {
|
|
'正品' => 'authentic',
|
|
'不符合正品特征' => 'non_authentic',
|
|
'存疑' => 'suspicious',
|
|
'暂无法明确判断' => 'inconclusive',
|
|
default => '',
|
|
};
|
|
}
|
|
|
|
private function decodeJsonArray(mixed $value): array
|
|
{
|
|
if (is_array($value)) {
|
|
return array_values($value);
|
|
}
|
|
if (is_string($value) && $value !== '') {
|
|
$decoded = json_decode($value, true);
|
|
return is_array($decoded) ? array_values($decoded) : [];
|
|
}
|
|
return [];
|
|
}
|
|
|
|
private function decodeJsonObject(mixed $value): array
|
|
{
|
|
if (is_array($value)) {
|
|
return $value;
|
|
}
|
|
if (is_string($value) && $value !== '') {
|
|
$decoded = json_decode($value, true);
|
|
return is_array($decoded) ? $decoded : [];
|
|
}
|
|
return [];
|
|
}
|
|
|
|
private function serviceProviderText(string $serviceProvider): string
|
|
{
|
|
return $serviceProvider === 'zhongjian' ? '中检鉴定' : '实物鉴定';
|
|
}
|
|
|
|
private function normalizeProductInput(mixed $input): array
|
|
{
|
|
$product = is_array($input) ? $input : [];
|
|
$categoryId = !empty($product['category_id']) ? (int)$product['category_id'] : null;
|
|
$brandId = !empty($product['brand_id']) ? (int)$product['brand_id'] : null;
|
|
|
|
$categoryName = trim((string)($product['category_name'] ?? ''));
|
|
if ($categoryName === '' && $categoryId) {
|
|
$categoryName = $this->lookupName('catalog_categories', 'name', $categoryId);
|
|
}
|
|
|
|
$brandName = trim((string)($product['brand_name'] ?? ''));
|
|
if ($brandName === '' && $brandId) {
|
|
$brandName = $this->lookupName('catalog_brands', 'name', $brandId);
|
|
}
|
|
|
|
$productName = trim((string)($product['product_name'] ?? ''));
|
|
if ($productName === '') {
|
|
$productName = trim($categoryName . ' ' . $brandName);
|
|
}
|
|
|
|
return [
|
|
'category_id' => $categoryId,
|
|
'category_name' => $categoryName,
|
|
'brand_id' => $brandId,
|
|
'brand_name' => $brandName,
|
|
'color' => trim((string)($product['color'] ?? '')),
|
|
'size_spec' => trim((string)($product['size_spec'] ?? '')),
|
|
'serial_no' => trim((string)($product['serial_no'] ?? '')),
|
|
'product_name' => $productName,
|
|
];
|
|
}
|
|
|
|
private function saveOrderProductSnapshot(int $orderId, array $product, string $now): void
|
|
{
|
|
$exists = Db::name('order_products')->where('order_id', $orderId)->find();
|
|
$payload = array_merge($product, [
|
|
'updated_at' => $now,
|
|
]);
|
|
if ($exists) {
|
|
foreach (['category_id', 'brand_id'] as $key) {
|
|
if (empty($payload[$key]) && !empty($exists[$key])) {
|
|
$payload[$key] = (int)$exists[$key];
|
|
}
|
|
}
|
|
|
|
foreach (['category_name', 'brand_name', 'color', 'size_spec', 'serial_no', 'product_name'] as $key) {
|
|
if (($payload[$key] ?? '') === '' && ($exists[$key] ?? '') !== '') {
|
|
$payload[$key] = $exists[$key];
|
|
}
|
|
}
|
|
|
|
if (($payload['product_name'] ?? '') === '') {
|
|
$payload['product_name'] = trim(($payload['category_name'] ?? '') . ' ' . ($payload['brand_name'] ?? ''));
|
|
}
|
|
|
|
Db::name('order_products')->where('order_id', $orderId)->update($payload);
|
|
return;
|
|
}
|
|
|
|
if (($payload['product_name'] ?? '') === '') {
|
|
$payload['product_name'] = trim(($payload['category_name'] ?? '') . ' ' . ($payload['brand_name'] ?? ''));
|
|
}
|
|
|
|
Db::name('order_products')->insert(array_merge($payload, [
|
|
'order_id' => $orderId,
|
|
'product_cover' => '',
|
|
'created_at' => $now,
|
|
]));
|
|
}
|
|
|
|
private function hasSubmittableProductInfo(int $orderId, ?array $incomingProduct = null): bool
|
|
{
|
|
$product = $incomingProduct;
|
|
if ($product === null) {
|
|
$product = Db::name('order_products')->where('order_id', $orderId)->find() ?: [];
|
|
}
|
|
|
|
$name = trim((string)($product['product_name'] ?? ''));
|
|
$categoryName = trim((string)($product['category_name'] ?? ''));
|
|
$brandName = trim((string)($product['brand_name'] ?? ''));
|
|
|
|
return $name !== '' || $categoryName !== '' || $brandName !== '';
|
|
}
|
|
|
|
private function resolveAppraisalTemplate(int $categoryId, string $serviceProvider, array $savedKeyPoints = []): ?array
|
|
{
|
|
if ($categoryId <= 0) {
|
|
return null;
|
|
}
|
|
|
|
$template = Db::name('appraisal_templates')
|
|
->where('scope_type', 'category')
|
|
->where('scope_id', $categoryId)
|
|
->where('is_enabled', 1)
|
|
->order('is_default', 'desc')
|
|
->order('id', 'desc')
|
|
->find();
|
|
|
|
$category = null;
|
|
if (!$template) {
|
|
$category = Db::name('catalog_categories')->field(['id', 'name'])->where('id', $categoryId)->find();
|
|
if (!$category) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
$savedMap = [];
|
|
foreach ($savedKeyPoints as $point) {
|
|
if (!is_array($point)) {
|
|
continue;
|
|
}
|
|
$code = (string)($point['point_code'] ?? '');
|
|
if ($code === '') {
|
|
continue;
|
|
}
|
|
$savedMap[$code] = $point;
|
|
}
|
|
|
|
$pointRows = $template
|
|
? Db::name('appraisal_template_key_points')
|
|
->where('template_id', (int)$template['id'])
|
|
->order('sort_order', 'asc')
|
|
->order('id', 'asc')
|
|
->select()
|
|
->toArray()
|
|
: [];
|
|
|
|
return [
|
|
'id' => $template ? (int)$template['id'] : 0,
|
|
'name' => (string)($template['name'] ?? sprintf('%s鉴定模板', (string)$category['name'])),
|
|
'code' => (string)($template['code'] ?? sprintf('appraisal_category_%d', $categoryId)),
|
|
'service_provider' => 'category',
|
|
'service_provider_text' => '通用品类模板',
|
|
'result_options' => [],
|
|
'condition_options' => [],
|
|
'valuation_hint' => '',
|
|
'key_points' => array_map(function (array $point) use ($savedMap) {
|
|
$pointCode = (string)$point['point_code'];
|
|
$saved = $savedMap[$pointCode] ?? [];
|
|
return [
|
|
'point_code' => $pointCode,
|
|
'point_name' => (string)$point['point_name'],
|
|
'point_type' => (string)$point['point_type'],
|
|
'options' => $this->decodeJsonArray($point['options_json'] ?? null),
|
|
'sort_order' => (int)$point['sort_order'],
|
|
'is_required' => (bool)$point['is_required'],
|
|
'point_value' => (string)($saved['point_value'] ?? ''),
|
|
'point_remark' => (string)($saved['point_remark'] ?? ''),
|
|
];
|
|
}, $pointRows),
|
|
];
|
|
}
|
|
|
|
private function normalizeKeyPointInput(mixed $input): array
|
|
{
|
|
if (!is_array($input)) {
|
|
return [];
|
|
}
|
|
|
|
$list = [];
|
|
foreach ($input as $item) {
|
|
if (!is_array($item)) {
|
|
continue;
|
|
}
|
|
|
|
$pointCode = trim((string)($item['point_code'] ?? ''));
|
|
$pointName = trim((string)($item['point_name'] ?? ''));
|
|
if ($pointCode === '' || $pointName === '') {
|
|
continue;
|
|
}
|
|
|
|
$list[] = [
|
|
'point_code' => $pointCode,
|
|
'point_name' => $pointName,
|
|
'point_value' => trim((string)($item['point_value'] ?? '')),
|
|
'point_remark' => trim((string)($item['point_remark'] ?? '')),
|
|
];
|
|
}
|
|
|
|
return $list;
|
|
}
|
|
|
|
private function saveTaskKeyPoints(int $taskResultId, array $keyPoints, string $now): void
|
|
{
|
|
Db::name('appraisal_task_key_points')->where('task_result_id', $taskResultId)->delete();
|
|
if (!$keyPoints) {
|
|
return;
|
|
}
|
|
|
|
$rows = array_map(fn (array $point) => [
|
|
'task_result_id' => $taskResultId,
|
|
'point_code' => $point['point_code'],
|
|
'point_name' => $point['point_name'],
|
|
'point_value' => $point['point_value'],
|
|
'point_remark' => $point['point_remark'],
|
|
'created_at' => $now,
|
|
'updated_at' => $now,
|
|
], $keyPoints);
|
|
|
|
Db::name('appraisal_task_key_points')->insertAll($rows);
|
|
}
|
|
|
|
private function loadTaskKeyPoints(int $taskResultId): array
|
|
{
|
|
if ($taskResultId <= 0) {
|
|
return [];
|
|
}
|
|
|
|
$rows = Db::name('appraisal_task_key_points')
|
|
->where('task_result_id', $taskResultId)
|
|
->order('id', 'asc')
|
|
->select()
|
|
->toArray();
|
|
|
|
return array_map(fn (array $item) => [
|
|
'point_code' => (string)$item['point_code'],
|
|
'point_name' => (string)$item['point_name'],
|
|
'point_value' => (string)$item['point_value'],
|
|
'point_remark' => (string)$item['point_remark'],
|
|
], $rows);
|
|
}
|
|
|
|
private function loadLatestOrderKeyPoints(int $orderId): array
|
|
{
|
|
$result = Db::name('appraisal_task_results')
|
|
->alias('r')
|
|
->leftJoin('appraisal_tasks t', 't.id = r.task_id')
|
|
->field([
|
|
'r.id',
|
|
't.task_stage',
|
|
't.id as task_id',
|
|
])
|
|
->where('r.order_id', $orderId)
|
|
->orderRaw("CASE WHEN t.task_stage = 'final_review' THEN 2 WHEN t.task_stage = 'first_review' THEN 1 ELSE 0 END DESC")
|
|
->order('r.id', 'desc')
|
|
->find();
|
|
|
|
if (!$result) {
|
|
return [];
|
|
}
|
|
|
|
return $this->loadTaskKeyPoints((int)$result['id']);
|
|
}
|
|
|
|
private function lookupName(string $table, string $field, ?int $id): string
|
|
{
|
|
if (!$id) {
|
|
return '';
|
|
}
|
|
|
|
return (string)Db::name($table)->where('id', $id)->value($field);
|
|
}
|
|
|
|
private function createOrUpdateReportDraft(int $orderId, array $task, array $resultPayload, string $now): array
|
|
{
|
|
$report = Db::name('reports')->where('order_id', $orderId)->order('id', 'desc')->find();
|
|
$order = Db::name('orders')->where('id', $orderId)->find();
|
|
$product = Db::name('order_products')->where('order_id', $orderId)->find();
|
|
$extra = Db::name('order_extras')->where('order_id', $orderId)->find();
|
|
$stageTasks = Db::name('appraisal_tasks')
|
|
->where('order_id', $orderId)
|
|
->select()
|
|
->toArray();
|
|
|
|
$firstReviewTask = null;
|
|
$finalReviewTask = null;
|
|
foreach ($stageTasks as $stageTask) {
|
|
if (($stageTask['task_stage'] ?? '') === 'first_review') {
|
|
$firstReviewTask = $stageTask;
|
|
}
|
|
if (($stageTask['task_stage'] ?? '') === 'final_review') {
|
|
$finalReviewTask = $stageTask;
|
|
}
|
|
}
|
|
|
|
$appraisalSnapshot = $this->buildAppraisalSnapshot(
|
|
$task['service_provider'],
|
|
$now,
|
|
$firstReviewTask,
|
|
$finalReviewTask
|
|
);
|
|
|
|
$reportData = [
|
|
'order_id' => $orderId,
|
|
'appraisal_no' => $order['appraisal_no'] ?? '',
|
|
'report_type' => 'appraisal',
|
|
'service_provider' => $task['service_provider'],
|
|
'institution_name' => $this->displayInstitutionName((string)$task['service_provider']),
|
|
'report_title' => $task['service_provider'] === 'zhongjian' ? '中检鉴定报告' : '安心验鉴定报告',
|
|
'report_status' => 'pending_publish',
|
|
'publish_time' => null,
|
|
'invalid_reason' => '',
|
|
'reject_reason' => '',
|
|
'rejected_by' => null,
|
|
'rejected_by_name' => '',
|
|
'rejected_at' => null,
|
|
'updated_at' => $now,
|
|
];
|
|
|
|
if ($report) {
|
|
$beforeReport = $report;
|
|
$reportData['report_version'] = (int)($report['report_version'] ?? 1) + 1;
|
|
Db::name('reports')->where('id', $report['id'])->update($reportData);
|
|
$reportId = (int)$report['id'];
|
|
$logAction = 'update_draft';
|
|
} else {
|
|
$reportData['report_no'] = 'AXY-R-' . date('Ymd') . '-' . mt_rand(1000, 9999);
|
|
$reportData['report_version'] = 1;
|
|
$reportData['created_at'] = $now;
|
|
$reportId = (int)Db::name('reports')->insertGetId($reportData);
|
|
$beforeReport = [];
|
|
$logAction = 'create_draft';
|
|
}
|
|
|
|
$contentPayload = [
|
|
'report_id' => $reportId,
|
|
'product_snapshot_json' => json_encode([
|
|
'product_name' => $product['product_name'] ?? '',
|
|
'category_name' => $product['category_name'] ?? '',
|
|
'brand_name' => $product['brand_name'] ?? '',
|
|
'color' => $product['color'] ?? '',
|
|
'size_spec' => $product['size_spec'] ?? '',
|
|
'serial_no' => $product['serial_no'] ?? '',
|
|
], JSON_UNESCAPED_UNICODE),
|
|
'result_snapshot_json' => json_encode([
|
|
'result_status' => $resultPayload['result_status'],
|
|
'result_text' => $resultPayload['result_text'],
|
|
'result_desc' => $resultPayload['result_desc'],
|
|
'external_remark' => $resultPayload['external_remark'] ?? '',
|
|
'key_points' => $this->loadLatestOrderKeyPoints($orderId),
|
|
], JSON_UNESCAPED_UNICODE),
|
|
'appraisal_snapshot_json' => json_encode($appraisalSnapshot, JSON_UNESCAPED_UNICODE),
|
|
'valuation_snapshot_json' => json_encode([
|
|
'condition_grade' => $resultPayload['condition_grade'],
|
|
'condition_desc' => $resultPayload['condition_desc'] ?: ($extra['condition_desc'] ?? ''),
|
|
'valuation_min' => $resultPayload['valuation_min'],
|
|
'valuation_max' => $resultPayload['valuation_max'],
|
|
'valuation_desc' => $resultPayload['valuation_desc'],
|
|
], JSON_UNESCAPED_UNICODE),
|
|
'evidence_attachments_json' => $resultPayload['attachments_json'] ?? null,
|
|
'risk_notice_text' => (new ContentService())->getReportRiskNotice('appraisal'),
|
|
'updated_at' => $now,
|
|
];
|
|
|
|
$content = Db::name('report_contents')->where('report_id', $reportId)->find();
|
|
if ($content) {
|
|
Db::name('report_contents')->where('report_id', $reportId)->update($contentPayload);
|
|
} else {
|
|
$contentPayload['created_at'] = $now;
|
|
Db::name('report_contents')->insert($contentPayload);
|
|
}
|
|
|
|
return [
|
|
'report' => Db::name('reports')->where('id', $reportId)->find() ?: [],
|
|
'before' => $beforeReport,
|
|
'action' => $logAction,
|
|
];
|
|
}
|
|
|
|
private function buildAppraisalSnapshot(string $serviceProvider, string $fallbackTime, ?array $firstReviewTask, ?array $finalReviewTask): array
|
|
{
|
|
$institutionName = $this->displayInstitutionName($serviceProvider);
|
|
$appraiserName = $this->normalizeAssigneeName($firstReviewTask['assignee_name'] ?? '')
|
|
?: $this->normalizeAssigneeName($finalReviewTask['assignee_name'] ?? '');
|
|
$reviewerName = $appraiserName;
|
|
$appraisalTime = $firstReviewTask['submitted_at']
|
|
?? $firstReviewTask['started_at']
|
|
?? $finalReviewTask['submitted_at']
|
|
?? $finalReviewTask['started_at']
|
|
?? $fallbackTime;
|
|
|
|
return [
|
|
'service_provider' => $serviceProvider,
|
|
'institution_name' => $institutionName,
|
|
'appraiser_name' => $appraiserName,
|
|
'reviewer_name' => $reviewerName,
|
|
'appraisal_time' => $appraisalTime,
|
|
];
|
|
}
|
|
|
|
private function insertReportLog(int $reportId, string $action, array $before, array $after, Request $request, string $remark = ''): void
|
|
{
|
|
if ($reportId <= 0) {
|
|
return;
|
|
}
|
|
|
|
Db::name('report_logs')->insert([
|
|
'report_id' => $reportId,
|
|
'action' => $action,
|
|
'operator_id' => (int)$request->header('x-admin-id', 0) ?: null,
|
|
'operator_name' => trim((string)$request->header('x-admin-name', '')),
|
|
'before_data' => $before ? json_encode($this->reportLogSnapshot($before), JSON_UNESCAPED_UNICODE) : null,
|
|
'after_data' => $after ? json_encode($this->reportLogSnapshot($after), JSON_UNESCAPED_UNICODE) : null,
|
|
'remark' => mb_substr($remark, 0, 255),
|
|
'created_at' => date('Y-m-d H:i:s'),
|
|
]);
|
|
}
|
|
|
|
private function reportLogSnapshot(array $report): array
|
|
{
|
|
return [
|
|
'id' => (int)($report['id'] ?? 0),
|
|
'report_no' => (string)($report['report_no'] ?? ''),
|
|
'order_id' => (int)($report['order_id'] ?? 0),
|
|
'report_status' => (string)($report['report_status'] ?? ''),
|
|
'report_version' => (int)($report['report_version'] ?? 0),
|
|
'publish_time' => (string)($report['publish_time'] ?? ''),
|
|
'zhongjian_report_no' => (string)($report['zhongjian_report_no'] ?? ''),
|
|
'invalid_reason' => (string)($report['invalid_reason'] ?? ''),
|
|
'reject_reason' => (string)($report['reject_reason'] ?? ''),
|
|
'rejected_by_name' => (string)($report['rejected_by_name'] ?? ''),
|
|
'rejected_at' => (string)($report['rejected_at'] ?? ''),
|
|
];
|
|
}
|
|
|
|
private function normalizeAssigneeName(?string $value): string
|
|
{
|
|
$name = trim((string)$value);
|
|
if ($name === '' || $name === '未分配') {
|
|
return '';
|
|
}
|
|
|
|
return $name;
|
|
}
|
|
|
|
private function displayInstitutionName(string $serviceProvider): string
|
|
{
|
|
return $serviceProvider === 'zhongjian' ? '中检鉴定中心' : '安心检验';
|
|
}
|
|
|
|
private function guardTaskOperator(Request $request, array $task): array
|
|
{
|
|
$adminId = (int)$request->header('x-admin-id', 0);
|
|
$adminName = trim((string)$request->header('x-admin-name', ''));
|
|
if ($adminId <= 0 || $adminName === '') {
|
|
return ['error' => api_error('当前登录管理员信息异常', 401), 'task_update' => []];
|
|
}
|
|
|
|
$roleCodes = $this->adminRoleCodes($adminId);
|
|
if (!$this->canOperateTaskStage((string)$task['task_stage'], $roleCodes)) {
|
|
return ['error' => api_error('当前账号未配置适用于该任务阶段的鉴定角色', 403), 'task_update' => []];
|
|
}
|
|
|
|
$isSuperAdmin = in_array('super_admin', $roleCodes, true);
|
|
$assigneeId = (int)($task['assignee_id'] ?? 0);
|
|
$assigneeName = trim((string)($task['assignee_name'] ?? ''));
|
|
|
|
if ($assigneeId > 0 && $assigneeId !== $adminId && !$isSuperAdmin) {
|
|
return ['error' => api_error("当前任务已分配给 {$assigneeName},请勿越权处理", 422), 'task_update' => []];
|
|
}
|
|
|
|
if ($assigneeId <= 0 || $assigneeName === '' || $assigneeName === '未分配') {
|
|
return [
|
|
'error' => null,
|
|
'task_update' => [
|
|
'assignee_id' => $adminId,
|
|
'assignee_name' => $adminName,
|
|
],
|
|
];
|
|
}
|
|
|
|
return ['error' => null, 'task_update' => []];
|
|
}
|
|
|
|
private function assignableRoleCodesForStage(string $taskStage): array
|
|
{
|
|
return ['appraiser', 'super_admin'];
|
|
}
|
|
|
|
private function canOperateTaskStage(string $taskStage, array $roleCodes): bool
|
|
{
|
|
return (bool)array_intersect($roleCodes, $this->assignableRoleCodesForStage($taskStage));
|
|
}
|
|
|
|
private function adminRoleCodes(int $adminId): array
|
|
{
|
|
$roleIds = Db::name('admin_role_relations')->where('admin_user_id', $adminId)->column('role_id');
|
|
if (!$roleIds) {
|
|
return [];
|
|
}
|
|
|
|
return array_values(Db::name('admin_roles')->whereIn('id', $roleIds)->column('code'));
|
|
}
|
|
|
|
private function findAssignableAdmins(string $taskStage): array
|
|
{
|
|
$expectedRoleCodes = $this->assignableRoleCodesForStage($taskStage);
|
|
$admins = Db::name('admin_users')
|
|
->where('status', 'enabled')
|
|
->order('id', 'asc')
|
|
->select()
|
|
->toArray();
|
|
|
|
$list = [];
|
|
foreach ($admins as $admin) {
|
|
$roleCodes = $this->adminRoleCodes((int)$admin['id']);
|
|
if (!$this->canOperateTaskStage($taskStage, $roleCodes)) {
|
|
continue;
|
|
}
|
|
|
|
$roleIds = Db::name('admin_role_relations')->where('admin_user_id', (int)$admin['id'])->column('role_id');
|
|
$roleNames = $roleIds ? Db::name('admin_roles')->whereIn('id', $roleIds)->column('name') : [];
|
|
|
|
$list[] = [
|
|
'id' => (int)$admin['id'],
|
|
'name' => $admin['name'],
|
|
'mobile' => $admin['mobile'],
|
|
'role_names' => array_values($roleNames),
|
|
'role_codes' => $roleCodes,
|
|
];
|
|
}
|
|
|
|
return $list;
|
|
}
|
|
|
|
private function findAssignableAdminById(int $adminId, string $taskStage): ?array
|
|
{
|
|
$admin = Db::name('admin_users')->where('id', $adminId)->where('status', 'enabled')->find();
|
|
if (!$admin) {
|
|
return null;
|
|
}
|
|
|
|
$roleCodes = $this->adminRoleCodes($adminId);
|
|
if (!$this->canOperateTaskStage($taskStage, $roleCodes)) {
|
|
return null;
|
|
}
|
|
|
|
$admin['role_codes'] = $roleCodes;
|
|
return $admin;
|
|
}
|
|
|
|
private function publishReportRecord(array $report, Request $request, bool $wrapTransaction = true): array
|
|
{
|
|
if (!$report) {
|
|
throw new \RuntimeException('报告不存在', 404);
|
|
}
|
|
if (!in_array((string)$report['report_status'], ['draft', 'pending_publish', 'updated', 'published'], true)) {
|
|
throw new \InvalidArgumentException('当前报告状态不支持发布');
|
|
}
|
|
|
|
$operatorId = (int)$request->header('x-admin-id', 0);
|
|
$now = date('Y-m-d H:i:s');
|
|
$effectivePublishTime = $report['publish_time'] ?: $now;
|
|
$verify = [];
|
|
|
|
if ($wrapTransaction) {
|
|
Db::startTrans();
|
|
}
|
|
try {
|
|
if (($report['report_status'] ?? '') !== 'published') {
|
|
Db::name('reports')->where('id', (int)$report['id'])->update([
|
|
'report_status' => 'published',
|
|
'publish_time' => $effectivePublishTime,
|
|
'updated_at' => $now,
|
|
]);
|
|
$report['report_status'] = 'published';
|
|
$report['publish_time'] = $effectivePublishTime;
|
|
}
|
|
|
|
$verify = $this->createOrUpdateVerifyRecord($report, $now);
|
|
|
|
if (($report['report_type'] ?? 'appraisal') === 'appraisal' && (int)($report['order_id'] ?? 0) > 0) {
|
|
Db::name('orders')->where('id', (int)$report['order_id'])->update([
|
|
'order_status' => 'report_published',
|
|
'display_status' => '报告已出具',
|
|
'updated_at' => $now,
|
|
]);
|
|
|
|
$order = Db::name('orders')->where('id', (int)$report['order_id'])->find();
|
|
$product = Db::name('order_products')->where('order_id', (int)$report['order_id'])->find();
|
|
|
|
$timelineExists = Db::name('order_timelines')
|
|
->where('order_id', (int)$report['order_id'])
|
|
->where('node_code', 'report_published')
|
|
->where('node_text', '报告已出具')
|
|
->find();
|
|
if (!$timelineExists) {
|
|
Db::name('order_timelines')->insert([
|
|
'order_id' => (int)$report['order_id'],
|
|
'node_code' => 'report_published',
|
|
'node_text' => '报告已出具',
|
|
'node_desc' => '正式报告已发布,用户可查看报告。',
|
|
'operator_type' => 'admin',
|
|
'operator_id' => $operatorId ?: null,
|
|
'occurred_at' => $now,
|
|
'created_at' => $now,
|
|
]);
|
|
}
|
|
|
|
(new MessageDispatcher())->sendInboxEvent('report_published', [
|
|
'user_id' => (int)($order['user_id'] ?? 0),
|
|
'biz_type' => 'report',
|
|
'biz_id' => (int)$report['id'],
|
|
'report_no' => (string)$report['report_no'],
|
|
'report_title' => (string)$report['report_title'],
|
|
'product_name' => $product['product_name'] ?? '',
|
|
'publish_time' => $effectivePublishTime,
|
|
'verify_url' => (string)($verify['verify_url'] ?? ''),
|
|
'fallback_title' => '报告已出具',
|
|
'fallback_content' => '您的正式报告已生成,可前往报告中心查看。',
|
|
]);
|
|
}
|
|
|
|
if ($wrapTransaction) {
|
|
Db::commit();
|
|
}
|
|
} catch (\Throwable $e) {
|
|
if ($wrapTransaction) {
|
|
Db::rollback();
|
|
}
|
|
throw $e;
|
|
}
|
|
|
|
if (($report['report_type'] ?? 'appraisal') === 'appraisal' && (int)($report['order_id'] ?? 0) > 0) {
|
|
(new EnterpriseWebhookService())->recordOrderEvent((int)$report['order_id'], 'report_published', [
|
|
'report_id' => (int)$report['id'],
|
|
'report_no' => (string)$report['report_no'],
|
|
'report_title' => (string)$report['report_title'],
|
|
'publish_time' => $effectivePublishTime,
|
|
'verify_url' => (string)($verify['verify_url'] ?? ''),
|
|
'report_page_url' => (string)($verify['report_page_url'] ?? ''),
|
|
]);
|
|
}
|
|
|
|
return [
|
|
'id' => (int)$report['id'],
|
|
'report_status' => 'published',
|
|
'publish_time' => $effectivePublishTime,
|
|
'verify_url' => (string)($verify['verify_url'] ?? ''),
|
|
'report_page_url' => (string)($verify['report_page_url'] ?? ''),
|
|
];
|
|
}
|
|
|
|
private function createOrUpdateVerifyRecord(array $report, string $now): array
|
|
{
|
|
$reportNo = (string)$report['report_no'];
|
|
$verifyToken = 'verify_' . strtolower((string)preg_replace('/[^a-zA-Z0-9]/', '', $reportNo));
|
|
$verifyUrl = $this->buildPublicPageUrl('/pages/verify/result', ['report_no' => $reportNo]);
|
|
$reportPageUrl = $this->buildPublicPageUrl('/pages/report/detail', ['report_no' => $reportNo]);
|
|
|
|
$payload = [
|
|
'report_id' => (int)$report['id'],
|
|
'report_no' => $reportNo,
|
|
'verify_token' => $verifyToken,
|
|
'verify_qrcode_url' => $reportPageUrl,
|
|
'verify_url' => $verifyUrl,
|
|
'verify_status' => 'valid',
|
|
'updated_at' => $now,
|
|
];
|
|
|
|
$verify = Db::name('report_verifies')->where('report_id', (int)$report['id'])->find();
|
|
if ($verify) {
|
|
Db::name('report_verifies')->where('id', (int)$verify['id'])->update($payload);
|
|
} else {
|
|
$payload['last_verified_at'] = null;
|
|
$payload['verify_count'] = 0;
|
|
$payload['created_at'] = $now;
|
|
Db::name('report_verifies')->insert($payload);
|
|
}
|
|
|
|
$fresh = Db::name('report_verifies')->where('report_id', (int)$report['id'])->find() ?: $payload;
|
|
$fresh['report_page_url'] = $reportPageUrl;
|
|
return $fresh;
|
|
}
|
|
|
|
private function buildPublicPageUrl(string $pagePath, array $query = []): string
|
|
{
|
|
$baseUrl = $this->normalizeH5BaseUrl($this->getSystemConfigValue('h5', 'page_base_url'));
|
|
$page = ltrim($pagePath, '/');
|
|
$queryString = http_build_query($query);
|
|
$hashPath = '/#/' . $page;
|
|
if ($queryString !== '') {
|
|
$hashPath .= '?' . $queryString;
|
|
}
|
|
|
|
return $baseUrl === '' ? $hashPath : $baseUrl . $hashPath;
|
|
}
|
|
|
|
private function normalizeH5BaseUrl(string $value): string
|
|
{
|
|
$baseUrl = trim($value);
|
|
if ($baseUrl === '') {
|
|
return '';
|
|
}
|
|
$hashPos = strpos($baseUrl, '#');
|
|
if ($hashPos !== false) {
|
|
$baseUrl = substr($baseUrl, 0, $hashPos);
|
|
}
|
|
if (!preg_match('/^https?:\/\//i', $baseUrl)) {
|
|
$baseUrl = 'https://' . ltrim($baseUrl, '/');
|
|
}
|
|
return rtrim($baseUrl, '/');
|
|
}
|
|
|
|
private function getSystemConfigValue(string $groupCode, string $configKey): string
|
|
{
|
|
$row = Db::name('system_configs')
|
|
->where('config_group', $groupCode)
|
|
->where('config_key', $configKey)
|
|
->find();
|
|
|
|
return trim((string)($row['config_value'] ?? ''));
|
|
}
|
|
|
|
private function evidenceService(): AppraisalEvidenceService
|
|
{
|
|
return new AppraisalEvidenceService();
|
|
}
|
|
|
|
private function assetUrlService(): PublicAssetUrlService
|
|
{
|
|
return new PublicAssetUrlService();
|
|
}
|
|
}
|