550 lines
22 KiB
PHP
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();
|
|
}
|
|
}
|