450 lines
19 KiB
PHP
450 lines
19 KiB
PHP
<?php
|
||
|
||
namespace app\support;
|
||
|
||
use support\Request;
|
||
use support\think\Db;
|
||
|
||
class EnterpriseOrderService
|
||
{
|
||
public function createOrder(array $customer, array $payload, Request $request): array
|
||
{
|
||
$externalOrderNo = trim((string)($payload['external_order_no'] ?? ''));
|
||
if ($externalOrderNo === '') {
|
||
throw new \InvalidArgumentException('external_order_no 不能为空');
|
||
}
|
||
|
||
$payloadHash = hash('sha256', json_encode($this->normalizePayloadForHash($payload), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
|
||
$existingRef = Db::name('enterprise_customer_order_refs')
|
||
->where('customer_id', (int)$customer['id'])
|
||
->where('external_order_no', $externalOrderNo)
|
||
->find();
|
||
if ($existingRef) {
|
||
if (($existingRef['payload_hash'] ?? '') !== $payloadHash) {
|
||
throw new \RuntimeException('external_order_no 已存在,但请求内容不一致');
|
||
}
|
||
return [
|
||
'idempotent' => true,
|
||
'order' => $this->buildOrderProgress((int)$customer['id'], $existingRef, (string)$customer['customer_code']),
|
||
];
|
||
}
|
||
|
||
$serviceProvider = trim((string)($payload['service_provider'] ?? 'anxinyan'));
|
||
if (!in_array($serviceProvider, ['anxinyan', 'zhongjian'], true)) {
|
||
throw new \InvalidArgumentException('service_provider 无效');
|
||
}
|
||
|
||
$serviceConfig = $this->serviceConfig($serviceProvider);
|
||
|
||
$product = $this->normalizeProduct((array)($payload['product_info'] ?? []));
|
||
$returnAddress = $this->normalizeReturnAddress((array)($payload['return_address'] ?? []));
|
||
$materials = $this->normalizeMaterials((array)($payload['materials'] ?? []));
|
||
|
||
$now = date('Y-m-d H:i:s');
|
||
$orderNo = 'AXY' . date('YmdHis') . mt_rand(100, 999);
|
||
$appraisalNo = 'AXY-APP-' . date('Ymd') . '-' . mt_rand(1000, 9999);
|
||
$estimated = date('Y-m-d H:i:s', strtotime(sprintf('+%d hours', (int)$serviceConfig['sla_hours'])));
|
||
$userId = (new EnterpriseCustomerService())->ensureVirtualUser($customer);
|
||
$productName = $this->resolveProductName($product);
|
||
|
||
Db::startTrans();
|
||
try {
|
||
$orderId = (int)Db::name('orders')->insertGetId([
|
||
'order_no' => $orderNo,
|
||
'appraisal_no' => $appraisalNo,
|
||
'user_id' => $userId,
|
||
'service_mode' => 'physical',
|
||
'service_provider' => $serviceProvider,
|
||
'payment_status' => 'paid',
|
||
'order_status' => 'pending_shipping',
|
||
'display_status' => '待寄送商品',
|
||
'estimated_finish_time' => $estimated,
|
||
'source_channel' => 'enterprise_push',
|
||
'source_customer_id' => $customer['customer_code'],
|
||
'pay_amount' => $serviceConfig['price'],
|
||
'paid_at' => $now,
|
||
'created_at' => $now,
|
||
'updated_at' => $now,
|
||
]);
|
||
|
||
Db::name('order_products')->insert(array_merge($product, [
|
||
'order_id' => $orderId,
|
||
'product_name' => $productName,
|
||
'product_cover' => $materials[0]['file_url'] ?? '',
|
||
'created_at' => $now,
|
||
'updated_at' => $now,
|
||
]));
|
||
|
||
$extra = (array)($payload['extra_info'] ?? []);
|
||
Db::name('order_extras')->insert([
|
||
'order_id' => $orderId,
|
||
'purchase_channel' => trim((string)($extra['purchase_channel'] ?? '')),
|
||
'purchase_price' => (float)($extra['purchase_price'] ?? 0),
|
||
'purchase_date' => $extra['purchase_date'] ?? null,
|
||
'usage_status' => trim((string)($extra['usage_status'] ?? '')),
|
||
'condition_desc' => trim((string)($extra['condition_desc'] ?? '')),
|
||
'has_accessories' => !empty($extra['has_accessories']) ? 1 : 0,
|
||
'accessories_json' => json_encode(array_values((array)($extra['accessories'] ?? [])), JSON_UNESCAPED_UNICODE),
|
||
'remark' => trim((string)($extra['remark'] ?? '')),
|
||
'created_at' => $now,
|
||
'updated_at' => $now,
|
||
]);
|
||
|
||
if ($returnAddress) {
|
||
Db::name('order_return_addresses')->insert(array_merge($returnAddress, [
|
||
'order_id' => $orderId,
|
||
'user_address_id' => null,
|
||
'created_at' => $now,
|
||
'updated_at' => $now,
|
||
]));
|
||
}
|
||
|
||
$shippingTarget = (new WarehouseService())->bindOrderTarget(
|
||
$orderId,
|
||
$serviceProvider,
|
||
!empty($product['category_id']) ? (int)$product['category_id'] : null,
|
||
null
|
||
);
|
||
|
||
$this->insertMaterials($orderId, $materials, $now);
|
||
$this->insertTimelines($orderId, $now, $shippingTarget);
|
||
$this->insertTask($orderId, $serviceProvider, $estimated, $now);
|
||
$this->insertInboundLogistics($orderId, $this->normalizeInboundLogistics($payload), $now);
|
||
|
||
Db::name('enterprise_customer_order_refs')->insert([
|
||
'customer_id' => (int)$customer['id'],
|
||
'external_order_no' => $externalOrderNo,
|
||
'order_id' => $orderId,
|
||
'order_no' => $orderNo,
|
||
'appraisal_no' => $appraisalNo,
|
||
'payload_hash' => $payloadHash,
|
||
'created_at' => $now,
|
||
'updated_at' => $now,
|
||
]);
|
||
|
||
Db::commit();
|
||
} catch (\Throwable $e) {
|
||
Db::rollback();
|
||
throw $e;
|
||
}
|
||
|
||
(new EnterpriseWebhookService())->recordOrderEvent($orderId, 'order_created', [
|
||
'product_name' => $productName,
|
||
'pay_amount' => (float)$serviceConfig['price'],
|
||
]);
|
||
|
||
$ref = Db::name('enterprise_customer_order_refs')->where('order_id', $orderId)->find();
|
||
return [
|
||
'idempotent' => false,
|
||
'order' => $this->buildOrderProgress((int)$customer['id'], $ref, (string)$customer['customer_code']),
|
||
];
|
||
}
|
||
|
||
public function findOrder(array $customer, string $externalOrderNo = '', string $orderNo = ''): array
|
||
{
|
||
$query = Db::name('enterprise_customer_order_refs')->where('customer_id', (int)$customer['id']);
|
||
if ($externalOrderNo !== '') {
|
||
$query->where('external_order_no', $externalOrderNo);
|
||
} elseif ($orderNo !== '') {
|
||
$query->where('order_no', $orderNo);
|
||
} else {
|
||
throw new \InvalidArgumentException('external_order_no 或 order_no 不能为空');
|
||
}
|
||
|
||
$ref = $query->find();
|
||
if (!$ref) {
|
||
throw new \RuntimeException('订单不存在');
|
||
}
|
||
|
||
return $this->buildOrderProgress((int)$customer['id'], $ref, (string)$customer['customer_code']);
|
||
}
|
||
|
||
public function buildOrderProgress(int $customerId, array $ref, string $customerCode = ''): array
|
||
{
|
||
$order = Db::name('orders')->where('id', (int)$ref['order_id'])->find();
|
||
if (!$order) {
|
||
throw new \RuntimeException('订单不存在');
|
||
}
|
||
|
||
$timeline = Db::name('order_timelines')->where('order_id', (int)$order['id'])->order('occurred_at', 'asc')->select()->toArray();
|
||
$sendLogistics = Db::name('order_logistics')->where('order_id', (int)$order['id'])->where('logistics_type', 'send_to_center')->order('id', 'desc')->find();
|
||
$returnLogistics = Db::name('order_logistics')->where('order_id', (int)$order['id'])->where('logistics_type', 'return_to_user')->order('id', 'desc')->find();
|
||
$report = Db::name('reports')->where('order_id', (int)$order['id'])->order('id', 'desc')->find();
|
||
$verify = $report ? (Db::name('report_verifies')->where('report_id', (int)$report['id'])->find() ?: null) : null;
|
||
|
||
return [
|
||
'customer_id' => $customerCode !== '' ? $customerCode : (string)$customerId,
|
||
'customer_code' => $customerCode !== '' ? $customerCode : (string)$customerId,
|
||
'external_order_no' => (string)$ref['external_order_no'],
|
||
'order_id' => (int)$order['id'],
|
||
'order_no' => (string)$order['order_no'],
|
||
'appraisal_no' => (string)$order['appraisal_no'],
|
||
'order_status' => (string)$order['order_status'],
|
||
'display_status' => (string)$order['display_status'],
|
||
'payment_status' => (string)$order['payment_status'],
|
||
'pay_amount' => (float)$order['pay_amount'],
|
||
'estimated_finish_time' => (string)($order['estimated_finish_time'] ?? ''),
|
||
'created_at' => (string)$order['created_at'],
|
||
'timeline' => array_map(fn(array $item) => [
|
||
'node_code' => (string)$item['node_code'],
|
||
'node_text' => (string)$item['node_text'],
|
||
'node_desc' => (string)$item['node_desc'],
|
||
'occurred_at' => (string)$item['occurred_at'],
|
||
], $timeline),
|
||
'inbound_logistics' => $this->formatLogistics($sendLogistics),
|
||
'return_logistics' => $this->formatLogistics($returnLogistics),
|
||
'report_summary' => $report ? [
|
||
'report_no' => (string)$report['report_no'],
|
||
'report_title' => (string)$report['report_title'],
|
||
'report_status' => (string)$report['report_status'],
|
||
'publish_time' => (string)($report['publish_time'] ?? ''),
|
||
'verify_url' => (string)($verify['verify_url'] ?? ''),
|
||
'report_page_url' => (string)($verify['verify_qrcode_url'] ?? ''),
|
||
'verify_status' => (string)($verify['verify_status'] ?? ''),
|
||
] : null,
|
||
];
|
||
}
|
||
|
||
private function normalizePayloadForHash(array $payload): array
|
||
{
|
||
ksort($payload);
|
||
foreach ($payload as &$value) {
|
||
if (is_array($value)) {
|
||
$value = $this->normalizePayloadForHash($value);
|
||
}
|
||
}
|
||
unset($value);
|
||
return $payload;
|
||
}
|
||
|
||
private function normalizeProduct(array $product): array
|
||
{
|
||
$productName = trim((string)($product['product_name'] ?? ''));
|
||
|
||
return [
|
||
'category_id' => !empty($product['category_id']) ? (int)$product['category_id'] : null,
|
||
'category_name' => trim((string)($product['category_name'] ?? '')),
|
||
'brand_id' => !empty($product['brand_id']) ? (int)$product['brand_id'] : null,
|
||
'brand_name' => trim((string)($product['brand_name'] ?? '')),
|
||
'color' => trim((string)($product['color'] ?? '')),
|
||
'size_spec' => trim((string)($product['size_spec'] ?? '')),
|
||
'serial_no' => trim((string)($product['serial_no'] ?? '')),
|
||
'product_name' => $productName,
|
||
];
|
||
}
|
||
|
||
private function normalizeReturnAddress(array $address): ?array
|
||
{
|
||
$requiredKeys = ['consignee', 'mobile', 'province', 'city', 'district', 'detail_address'];
|
||
$hasAnyValue = false;
|
||
foreach ($requiredKeys as $key) {
|
||
if (trim((string)($address[$key] ?? '')) !== '') {
|
||
$hasAnyValue = true;
|
||
break;
|
||
}
|
||
}
|
||
if (!$hasAnyValue) {
|
||
return null;
|
||
}
|
||
|
||
foreach ($requiredKeys as $key) {
|
||
if (trim((string)($address[$key] ?? '')) === '') {
|
||
throw new \InvalidArgumentException("return_address.{$key} 不能为空");
|
||
}
|
||
}
|
||
|
||
return [
|
||
'consignee' => trim((string)$address['consignee']),
|
||
'mobile' => trim((string)$address['mobile']),
|
||
'province' => trim((string)$address['province']),
|
||
'city' => trim((string)$address['city']),
|
||
'district' => trim((string)$address['district']),
|
||
'detail_address' => trim((string)$address['detail_address']),
|
||
];
|
||
}
|
||
|
||
private function normalizeMaterials(array $materials): array
|
||
{
|
||
$list = [];
|
||
foreach ($materials as $index => $item) {
|
||
if (is_string($item)) {
|
||
$url = trim($item);
|
||
$itemCode = 'material_' . ($index + 1);
|
||
$itemName = '鉴定资料';
|
||
} elseif (is_array($item)) {
|
||
$url = trim((string)($item['file_url'] ?? $item['url'] ?? ''));
|
||
$itemCode = trim((string)($item['item_code'] ?? 'material_' . ($index + 1)));
|
||
$itemName = trim((string)($item['item_name'] ?? '鉴定资料'));
|
||
} else {
|
||
$url = '';
|
||
$itemCode = 'material_' . ($index + 1);
|
||
$itemName = '鉴定资料';
|
||
}
|
||
if ($url === '' || !preg_match('/^https?:\/\//i', $url)) {
|
||
throw new \InvalidArgumentException('materials 只支持 http/https 图片 URL');
|
||
}
|
||
$list[] = [
|
||
'item_code' => $itemCode,
|
||
'item_name' => $itemName,
|
||
'file_url' => $url,
|
||
'thumbnail_url' => is_array($item) ? trim((string)($item['thumbnail_url'] ?? $url)) : $url,
|
||
'is_required' => is_array($item) && !empty($item['is_required']) ? 1 : 0,
|
||
];
|
||
}
|
||
|
||
return $list;
|
||
}
|
||
|
||
private function normalizeInboundLogistics(array $payload): array
|
||
{
|
||
$logistics = (array)($payload['inbound_logistics'] ?? []);
|
||
if (!empty($payload['express_company']) || !empty($payload['tracking_no'])) {
|
||
$logistics = array_merge($logistics, [
|
||
'express_company' => $payload['express_company'] ?? ($logistics['express_company'] ?? ''),
|
||
'tracking_no' => $payload['tracking_no'] ?? ($logistics['tracking_no'] ?? ''),
|
||
]);
|
||
}
|
||
|
||
return $logistics;
|
||
}
|
||
|
||
private function resolveProductName(array $product): string
|
||
{
|
||
$productName = trim((string)($product['product_name'] ?? ''));
|
||
if ($productName !== '') {
|
||
return $productName;
|
||
}
|
||
return trim(($product['brand_name'] ?? '') . ' ' . ($product['category_name'] ?? ''));
|
||
}
|
||
|
||
private function serviceConfig(string $serviceProvider): array
|
||
{
|
||
$configs = [
|
||
'anxinyan' => ['price' => 99.00, 'sla_hours' => 48],
|
||
'zhongjian' => ['price' => 199.00, 'sla_hours' => 72],
|
||
];
|
||
if (isset($configs[$serviceProvider])) {
|
||
return $configs[$serviceProvider];
|
||
}
|
||
return $configs['anxinyan'];
|
||
}
|
||
|
||
private function insertMaterials(int $orderId, array $materials, string $now): void
|
||
{
|
||
foreach ($materials as $item) {
|
||
$uploadItemId = (int)Db::name('order_upload_items')->insertGetId([
|
||
'order_id' => $orderId,
|
||
'template_id' => null,
|
||
'item_code' => $item['item_code'],
|
||
'item_name' => $item['item_name'],
|
||
'is_required' => $item['is_required'],
|
||
'source_type' => 'initial',
|
||
'status' => 'uploaded',
|
||
'created_at' => $now,
|
||
'updated_at' => $now,
|
||
]);
|
||
|
||
Db::name('order_upload_files')->insert([
|
||
'order_upload_item_id' => $uploadItemId,
|
||
'file_id' => md5($item['file_url']),
|
||
'file_url' => $item['file_url'],
|
||
'thumbnail_url' => $item['thumbnail_url'],
|
||
'quality_status' => 'uploaded',
|
||
'quality_message' => '',
|
||
'uploaded_by_user_id' => null,
|
||
'created_at' => $now,
|
||
'updated_at' => $now,
|
||
]);
|
||
}
|
||
}
|
||
|
||
private function insertTimelines(int $orderId, string $now, array $shippingTarget): void
|
||
{
|
||
Db::name('order_timelines')->insertAll([
|
||
[
|
||
'order_id' => $orderId,
|
||
'node_code' => 'created',
|
||
'node_text' => '下单成功',
|
||
'node_desc' => '大客户订单已推送并创建成功',
|
||
'operator_type' => 'system',
|
||
'operator_id' => null,
|
||
'occurred_at' => $now,
|
||
'created_at' => $now,
|
||
],
|
||
[
|
||
'order_id' => $orderId,
|
||
'node_code' => 'pending_shipping',
|
||
'node_text' => '待寄送商品',
|
||
'node_desc' => sprintf('请将商品寄送至%s', $shippingTarget['warehouse_name'] ?: '鉴定中心'),
|
||
'operator_type' => 'system',
|
||
'operator_id' => null,
|
||
'occurred_at' => $now,
|
||
'created_at' => $now,
|
||
],
|
||
]);
|
||
}
|
||
|
||
private function insertTask(int $orderId, string $serviceProvider, string $estimated, string $now): void
|
||
{
|
||
Db::name('appraisal_tasks')->insert([
|
||
'order_id' => $orderId,
|
||
'task_stage' => 'first_review',
|
||
'service_provider' => $serviceProvider,
|
||
'status' => 'pending',
|
||
'assignee_id' => null,
|
||
'assignee_name' => '未分配',
|
||
'started_at' => null,
|
||
'submitted_at' => null,
|
||
'sla_deadline' => $estimated,
|
||
'is_overtime' => 0,
|
||
'created_at' => $now,
|
||
'updated_at' => $now,
|
||
]);
|
||
}
|
||
|
||
private function insertInboundLogistics(int $orderId, array $logistics, string $now): void
|
||
{
|
||
$expressCompany = trim((string)($logistics['express_company'] ?? ''));
|
||
$trackingNo = trim((string)($logistics['tracking_no'] ?? ''));
|
||
if ($expressCompany === '' || $trackingNo === '') {
|
||
return;
|
||
}
|
||
|
||
$latestDesc = sprintf('客户已提交寄送运单:%s %s,等待鉴定中心签收。', $expressCompany, $trackingNo);
|
||
$logisticsId = (int)Db::name('order_logistics')->insertGetId([
|
||
'order_id' => $orderId,
|
||
'logistics_type' => 'send_to_center',
|
||
'express_company' => $expressCompany,
|
||
'tracking_no' => $trackingNo,
|
||
'tracking_status' => 'submitted',
|
||
'latest_desc' => $latestDesc,
|
||
'latest_time' => $now,
|
||
'created_at' => $now,
|
||
'updated_at' => $now,
|
||
]);
|
||
|
||
Db::name('order_logistics_nodes')->insert([
|
||
'logistics_id' => $logisticsId,
|
||
'node_time' => $now,
|
||
'node_desc' => $latestDesc,
|
||
'node_location' => '',
|
||
'created_at' => $now,
|
||
]);
|
||
}
|
||
|
||
private function formatLogistics(?array $logistics): ?array
|
||
{
|
||
if (!$logistics) {
|
||
return null;
|
||
}
|
||
|
||
return [
|
||
'express_company' => (string)$logistics['express_company'],
|
||
'tracking_no' => (string)$logistics['tracking_no'],
|
||
'tracking_status' => (string)$logistics['tracking_status'],
|
||
'latest_desc' => (string)$logistics['latest_desc'],
|
||
'latest_time' => (string)($logistics['latest_time'] ?? ''),
|
||
];
|
||
}
|
||
}
|