Files
anxinyan/server-api/app/support/EnterpriseOrderService.php
wushumin edd1a02157 first
2026-05-11 15:28:27 +08:00

450 lines
19 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?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'] ?? ''),
];
}
}