Files
anxinyan/server-api/app/controller/app/OrdersController.php
2026-05-16 16:32:56 +08:00

550 lines
22 KiB
PHP

<?php
namespace app\controller\app;
use app\model\Order;
use app\model\OrderProduct;
use app\model\OrderSupplementTask;
use app\model\OrderSupplementTaskItem;
use app\model\OrderTimeline;
use app\support\PublicAssetUrlService;
use support\Request;
use support\think\Db;
class OrdersController
{
public function index(Request $request)
{
$userId = app_user_id($request);
$orders = Db::name('orders')
->alias('o')
->leftJoin('order_products p', 'p.order_id = o.id')
->leftJoin('order_logistics l', 'l.order_id = o.id AND l.logistics_type = "send_to_center"')
->field([
'o.id',
'o.order_no',
'o.appraisal_no',
'o.service_provider',
'o.order_status',
'o.display_status',
'o.estimated_finish_time',
'p.product_name',
'p.product_cover',
'l.tracking_no',
])
->where('o.user_id', $userId)
->order('o.id', 'desc')
->select()
->toArray();
$returnTrackingMap = [];
if ($orders) {
$returnRows = Db::name('order_logistics')
->whereIn('order_id', array_column($orders, 'id'))
->where('logistics_type', 'return_to_user')
->order('id', 'desc')
->select()
->toArray();
foreach ($returnRows as $row) {
$orderId = (int)($row['order_id'] ?? 0);
if ($orderId > 0 && !isset($returnTrackingMap[$orderId])) {
$returnTrackingMap[$orderId] = [
'tracking_no' => (string)($row['tracking_no'] ?? ''),
'tracking_status' => (string)($row['tracking_status'] ?? ''),
];
}
}
}
$list = array_map(function (array $item) use ($returnTrackingMap) {
return [
'order_id' => (int)$item['id'],
'order_no' => $item['order_no'],
'appraisal_no' => $item['appraisal_no'],
'order_status' => $item['order_status'],
'product_name' => $item['product_name'] ?: '待补充商品名称',
'product_cover' => $item['product_cover'] ?: '',
'service_provider' => $item['service_provider'],
'display_status' => $this->displayStatus(
$item['order_status'],
$item['display_status'],
$item['tracking_no'] ?? '',
$returnTrackingMap[(int)$item['id']]['tracking_no'] ?? '',
$returnTrackingMap[(int)$item['id']]['tracking_status'] ?? '',
),
'status_desc' => $this->statusDescription(
$item['order_status'],
$item['tracking_no'] ?? '',
$returnTrackingMap[(int)$item['id']]['tracking_no'] ?? '',
$returnTrackingMap[(int)$item['id']]['tracking_status'] ?? '',
),
'estimated_finish_time' => $item['estimated_finish_time'],
'primary_action' => $this->primaryAction(
$item['order_status'],
$returnTrackingMap[(int)$item['id']]['tracking_no'] ?? '',
$returnTrackingMap[(int)$item['id']]['tracking_status'] ?? '',
),
];
}, $orders);
return api_success(['list' => $list]);
}
public function detail(Request $request)
{
$id = (int)$request->input('id', 1);
$userId = app_user_id($request);
$order = Order::where('id', $id)->where('user_id', $userId)->find();
if (!$order) {
return api_error('订单不存在', 404);
}
$product = OrderProduct::where('order_id', $id)->find();
$extra = Db::name('order_extras')->where('order_id', $id)->find();
$sendLogistics = Db::name('order_logistics')
->where('order_id', $id)
->where('logistics_type', 'send_to_center')
->order('id', 'desc')
->find();
$returnLogistics = Db::name('order_logistics')
->where('order_id', $id)
->where('logistics_type', 'return_to_user')
->order('id', 'desc')
->find();
$timeline = OrderTimeline::where('order_id', $id)
->order('occurred_at', 'asc')
->select()
->map(fn ($item) => [
'node_code' => $item->node_code,
'node_text' => $item->node_text,
'node_desc' => $item->node_desc,
'occurred_at' => $item->occurred_at,
])
->toArray();
$supplement = OrderSupplementTask::where('order_id', $id)
->where('status', 'pending')
->order('id', 'desc')
->find();
$supplementItems = [];
if ($supplement) {
$supplementItems = OrderSupplementTaskItem::where('task_id', $supplement->id)
->select()
->map(fn ($item) => [
'item_code' => $item->item_code,
'item_name' => $item->item_name,
'guide_text' => $item->guide_text,
])
->toArray();
}
$materials = Db::name('order_upload_items')
->where('order_id', $id)
->order('id', 'asc')
->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 [
'upload_item_id' => (int)$item['id'],
'item_code' => $item['item_code'],
'item_name' => $item['item_name'],
'is_required' => (bool)$item['is_required'],
'source_type' => $item['source_type'],
'source_type_text' => $this->materialSourceTypeText($item['source_type']),
'status' => $item['status'],
'status_text' => $this->materialStatusText($item['status']),
'file_count' => count($files),
'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),
'quality_status' => $file['quality_status'],
'quality_message' => $file['quality_message'],
], $files),
];
}, $materials)));
$returnAddress = Db::name('order_return_addresses')->where('order_id', $id)->find();
if (!$returnAddress) {
$returnAddress = Db::name('user_addresses')
->where('user_id', (int)$order->user_id)
->where('is_default', 1)
->order('id', 'desc')
->find()
?: Db::name('user_addresses')
->where('user_id', (int)$order->user_id)
->order('id', 'desc')
->find();
if ($returnAddress) {
$returnAddress = [
'user_address_id' => (int)$returnAddress['id'],
'consignee' => $returnAddress['consignee'],
'mobile' => $returnAddress['mobile'],
'province' => $returnAddress['province'],
'city' => $returnAddress['city'],
'district' => $returnAddress['district'],
'detail_address' => $returnAddress['detail_address'],
];
}
}
$returnNodes = [];
if ($returnLogistics) {
$returnNodes = Db::name('order_logistics_nodes')
->where('logistics_id', $returnLogistics['id'])
->order('node_time', 'desc')
->select()
->toArray();
}
return api_success([
'order_info' => [
'order_id' => (int)$order->id,
'order_no' => $order->order_no,
'appraisal_no' => $order->appraisal_no,
'service_provider' => $order->service_provider,
'source_channel' => $this->normalizeOrderSourceChannel((string)($order->source_channel ?? '')),
'source_channel_text' => $this->sourceChannelText((string)($order->source_channel ?? '')),
'source_customer_id' => (string)($order->source_customer_id ?? ''),
'order_status' => $order->order_status,
'display_status' => $this->displayStatus(
$order->order_status,
$order->display_status,
$sendLogistics['tracking_no'] ?? '',
$returnLogistics['tracking_no'] ?? '',
$returnLogistics['tracking_status'] ?? '',
),
'status_desc' => $this->statusDescription(
$order->order_status,
$sendLogistics['tracking_no'] ?? '',
$returnLogistics['tracking_no'] ?? '',
$returnLogistics['tracking_status'] ?? '',
),
'estimated_finish_time' => $order->estimated_finish_time,
'can_edit_return_address' => empty($returnLogistics['tracking_no']),
],
'product_info' => [
'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 ?: '',
],
'extra_info' => [
'purchase_channel' => $extra['purchase_channel'] ?? '',
'purchase_price' => (float)($extra['purchase_price'] ?? 0),
'purchase_date' => $extra['purchase_date'] ?? '',
'usage_status' => $extra['usage_status'] ?? '',
'usage_status_text' => $this->usageStatusText($extra['usage_status'] ?? ''),
'condition_desc' => $extra['condition_desc'] ?? '',
'has_accessories' => (bool)($extra['has_accessories'] ?? false),
'accessories' => $this->decodeJsonArray($extra['accessories_json'] ?? null),
'remark' => $extra['remark'] ?? '',
],
'materials' => $materials,
'return_address' => $returnAddress ? $this->formatReturnAddress($returnAddress) : null,
'return_logistics' => $returnLogistics ? [
'express_company' => $returnLogistics['express_company'],
'tracking_no' => $returnLogistics['tracking_no'],
'tracking_status' => $returnLogistics['tracking_status'],
'tracking_status_text' => $this->trackingStatusText((string)$returnLogistics['tracking_status'], 'return_to_user'),
'latest_desc' => $returnLogistics['latest_desc'],
'latest_time' => $returnLogistics['latest_time'],
'nodes' => array_map(fn (array $item) => [
'node_time' => $item['node_time'],
'node_desc' => $item['node_desc'],
'node_location' => $item['node_location'],
], $returnNodes),
] : null,
'timeline' => $timeline,
'supplement_task' => $supplement ? [
'task_id' => (int)$supplement->id,
'reason' => $supplement->reason,
'deadline' => $supplement->deadline,
'items' => $supplementItems,
] : null,
'available_actions' => [
'primary_action' => $this->primaryAction($order->order_status),
'secondary_action' => '联系客服',
],
]);
}
public function saveReturnAddress(Request $request)
{
$orderId = (int)$request->input('order_id', 0);
$addressId = (int)$request->input('address_id', 0);
$userId = app_user_id($request);
if ($orderId <= 0 || $addressId <= 0) {
return api_error('订单和地址参数不能为空', 422);
}
$order = Db::name('orders')->where('id', $orderId)->where('user_id', $userId)->find();
if (!$order) {
return api_error('订单不存在', 404);
}
$returnLogistics = Db::name('order_logistics')
->where('order_id', $orderId)
->where('logistics_type', 'return_to_user')
->order('id', 'desc')
->find();
if (!empty($returnLogistics['tracking_no'])) {
return api_error('回寄运单已生成,当前不可再修改寄回地址', 422);
}
$address = Db::name('user_addresses')->where('id', $addressId)->where('user_id', $userId)->find();
if (!$address) {
return api_error('地址不存在', 404);
}
$now = date('Y-m-d H:i:s');
$snapshot = [
'user_address_id' => (int)$address['id'],
'consignee' => $address['consignee'],
'mobile' => $address['mobile'],
'province' => $address['province'],
'city' => $address['city'],
'district' => $address['district'],
'detail_address' => $address['detail_address'],
];
Db::startTrans();
try {
$existing = Db::name('order_return_addresses')->where('order_id', $orderId)->find();
if ($existing) {
Db::name('order_return_addresses')->where('order_id', $orderId)->update(array_merge($snapshot, [
'updated_at' => $now,
]));
$nodeText = '已更新寄回地址';
} else {
Db::name('order_return_addresses')->insert(array_merge($snapshot, [
'order_id' => $orderId,
'created_at' => $now,
'updated_at' => $now,
]));
$nodeText = '已确认寄回地址';
}
Db::name('order_timelines')->insert([
'order_id' => $orderId,
'node_code' => 'return_address_selected',
'node_text' => $nodeText,
'node_desc' => sprintf('用户已确认寄回地址:%s%s%s%s', $address['province'], $address['city'], $address['district'], $address['detail_address']),
'operator_type' => 'user',
'operator_id' => $userId,
'occurred_at' => $now,
'created_at' => $now,
]);
Db::commit();
} catch (\Throwable $e) {
Db::rollback();
return api_error('寄回地址保存失败', 500, [
'detail' => $e->getMessage(),
]);
}
return api_success([
'order_id' => $orderId,
'return_address' => $this->formatReturnAddress($snapshot),
], '寄回地址已更新');
}
private function primaryAction(string $status, string $returnTrackingNo = '', string $returnTrackingStatus = ''): string
{
return match ($status) {
'pending_payment' => '去支付',
'pending_submission' => '去上传',
'pending_shipping' => '查看寄送',
'pending_supplement' => '去补资料',
'report_published' => '查看报告',
'completed' => ($returnTrackingNo !== '' && $returnTrackingStatus !== 'received') ? '查看物流' : '查看报告',
default => '查看进度',
};
}
private function statusDescription(string $status, string $trackingNo = '', string $returnTrackingNo = '', string $returnTrackingStatus = ''): string
{
return match ($status) {
'pending_payment' => '请完成支付后继续本次鉴定服务',
'pending_submission' => '请补充必要资料后继续进入鉴定流程',
'pending_shipping' => $trackingNo !== '' ? '运单已提交,等待鉴定中心签收' : '请尽快将商品寄送至鉴定中心',
'received' => '商品已由鉴定中心签收,等待鉴定师开始处理',
'in_first_review' => '鉴定师正在处理,后续节点会持续同步',
'in_final_review' => '鉴定师正在处理,预计 24 小时内出具报告',
'pending_supplement' => '鉴定师需要您补充资料后继续处理',
'report_published' => '正式报告已生成,待平台安排回寄商品',
'completed' => $returnTrackingStatus === 'received'
? '回寄商品已签收,本次订单已完成'
: ($returnTrackingNo !== '' ? '鉴定物品已寄回,请留意签收与物流信息' : '正式报告已生成,可立即查看并验真'),
default => '当前无需操作,请耐心等待',
};
}
private function displayStatus(
string $status,
string $displayStatus,
string $trackingNo = '',
string $returnTrackingNo = '',
string $returnTrackingStatus = '',
): string
{
if ($status === 'pending_shipping' && $trackingNo !== '') {
return '已提交运单';
}
if ($status === 'report_published') {
return '待寄回';
}
if ($status === 'completed') {
if ($returnTrackingStatus === 'received') {
return '已完成';
}
if ($returnTrackingNo !== '') {
return '物品已寄回';
}
}
return $displayStatus;
}
private function usageStatusText(string $status): string
{
return match ($status) {
'new' => '全新未使用',
'light_use' => '轻微使用痕迹',
'used' => '长期使用',
default => $status,
};
}
private function materialStatusText(string $status): string
{
return match ($status) {
'uploaded' => '已上传',
'optional' => '选填未上传',
'pending' => '待上传',
default => $status,
};
}
private function materialSourceTypeText(string $sourceType): string
{
return match ($sourceType) {
'supplement' => '补充资料',
default => '下单资料',
};
}
private function normalizeOrderSourceChannel(string $sourceChannel): string
{
$sourceChannel = trim($sourceChannel);
$aliases = [
'wechat_mini_program' => 'mini_program',
'weixin_mini_program' => 'mini_program',
'mp_weixin' => 'mini_program',
'miniapp' => 'mini_program',
'user_app' => 'mini_program',
'web_h5' => 'h5',
'enterprise' => 'enterprise_push',
'enterprise_order' => 'enterprise_push',
'customer_push' => 'enterprise_push',
'large_customer_push' => 'enterprise_push',
'manual' => 'manual_entry',
'manual_order' => 'manual_entry',
];
$sourceChannel = $aliases[$sourceChannel] ?? $sourceChannel;
return in_array($sourceChannel, ['mini_program', 'h5', 'enterprise_push', 'manual_entry'], true) ? $sourceChannel : '';
}
private function sourceChannelText(string $sourceChannel): string
{
return match ($this->normalizeOrderSourceChannel($sourceChannel)) {
'mini_program' => '小程序',
'h5' => 'H5',
'enterprise_push' => '大客户推送订单',
'manual_entry' => '后台补录订单',
default => '未知渠道',
};
}
private function decodeJsonArray(mixed $value): array
{
if (is_array($value)) {
return array_values(array_filter($value, fn ($item) => is_string($item) && $item !== ''));
}
if (!is_string($value) || $value === '') {
return [];
}
$decoded = json_decode($value, true);
if (!is_array($decoded)) {
return [];
}
return array_values(array_filter($decoded, fn ($item) => is_string($item) && $item !== ''));
}
private function formatReturnAddress(array $item): array
{
return [
'user_address_id' => (int)($item['user_address_id'] ?? 0),
'consignee' => $item['consignee'] ?? '',
'mobile' => $item['mobile'] ?? '',
'province' => $item['province'] ?? '',
'city' => $item['city'] ?? '',
'district' => $item['district'] ?? '',
'detail_address' => $item['detail_address'] ?? '',
'full_address' => trim(sprintf(
'%s%s%s%s',
$item['province'] ?? '',
$item['city'] ?? '',
$item['district'] ?? '',
$item['detail_address'] ?? ''
)),
];
}
private function trackingStatusText(string $status, string $logisticsType = 'send_to_center'): string
{
if ($logisticsType === 'return_to_user') {
return match ($status) {
'submitted' => '已登记回寄运单',
'in_transit' => '回寄途中',
'received' => '用户已签收',
default => $status === '' ? '待回寄' : $status,
};
}
return match ($status) {
'submitted' => '已提交运单',
'in_transit' => '运输中',
'received' => '已签收',
default => $status === '' ? '待提交' : $status,
};
}
private function assetUrlService(): PublicAssetUrlService
{
return new PublicAssetUrlService();
}
}