Files
anxinyan/server-api/app/controller/app/AppraisalController.php
2026-05-21 17:56:35 +08:00

695 lines
28 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\controller\app;
use app\support\MessageDispatcher;
use app\support\ContentService;
use app\support\FileStorageService;
use app\support\PublicAssetUrlService;
use app\support\WarehouseService;
use support\Request;
use support\think\Db;
use function str_starts_with;
class AppraisalController
{
public function uploadFile(Request $request)
{
$userId = app_user_id($request);
$draftId = (int)$request->post('draft_id', 0);
$itemCode = trim((string)$request->post('item_code', ''));
$itemName = trim((string)$request->post('item_name', ''));
if (!$draftId || $itemCode === '') {
return api_error('草稿 ID 和资料项编码不能为空', 422);
}
$draft = Db::name('appraisal_drafts')->where('id', $draftId)->where('user_id', $userId)->find();
if (!$draft) {
return api_error('草稿不存在', 404);
}
$file = $request->file('file');
if (!$file || !$file->isValid()) {
return api_error('上传文件无效', 422);
}
$extension = strtolower($file->getUploadExtension() ?: 'jpg');
$filename = sprintf('%s_%s.%s', $itemCode, uniqid(), $extension);
$relativeDir = 'uploads/appraisal/' . date('Ymd');
$relativePath = $relativeDir . '/' . $filename;
$this->storage()->putUploadedFile($file, $relativePath);
$fileUrl = $this->storage()->publicUrl($request, $relativePath);
return api_success([
'file_id' => md5($relativePath),
'item_code' => $itemCode,
'item_name' => $itemName,
'file_url' => $fileUrl,
'thumbnail_url' => $fileUrl,
'name' => $file->getUploadName(),
]);
}
public function createDraft(Request $request)
{
$userId = app_user_id($request);
$serviceProvider = (string)$request->input('service_provider', 'anxinyan');
$serviceMode = (string)$request->input('service_mode', 'physical');
$draftId = Db::name('appraisal_drafts')->insertGetId([
'user_id' => $userId,
'service_mode' => $serviceMode,
'service_provider' => $serviceProvider,
'current_step' => 1,
'status' => 'draft',
'created_at' => date('Y-m-d H:i:s'),
'updated_at' => date('Y-m-d H:i:s'),
]);
return api_success([
'draft_id' => (int)$draftId,
'service_provider' => $serviceProvider,
'service_mode' => $serviceMode,
]);
}
public function deleteFile(Request $request)
{
$fileUrl = trim((string)$request->post('file_url', ''));
if ($fileUrl === '') {
return api_error('文件地址不能为空', 422);
}
$relativePath = $this->storage()->storagePath($fileUrl);
if (!str_starts_with($relativePath, 'uploads/appraisal/')) {
return api_error('不允许删除该文件', 403);
}
$this->storage()->delete($relativePath);
return api_success([
'file_url' => $fileUrl,
], '删除成功');
}
public function draftDetail(Request $request)
{
$draftId = (int)$request->input('draft_id', 0);
$draft = Db::name('appraisal_drafts')->where('id', $draftId)->where('user_id', app_user_id($request))->find();
if (!$draft) {
return api_error('草稿不存在', 404);
}
$product = Db::name('appraisal_draft_products')->where('draft_id', $draftId)->find();
if ($product) {
$product['brand_name'] = $this->resolveBrandName($product);
}
$extra = Db::name('appraisal_draft_extras')->where('draft_id', $draftId)->find();
return api_success([
'draft_id' => (int)$draft['id'],
'service_provider' => $draft['service_provider'],
'service_mode' => $draft['service_mode'],
'current_step' => (int)$draft['current_step'],
'product_info' => $product ?: new \stdClass(),
'extra_info' => $extra ?: new \stdClass(),
'upload_info' => [
'items' => $this->draftUploadItems($draftId, $request),
],
]);
}
public function saveDraft(Request $request)
{
$draftId = (int)$request->input('draft_id', 0);
$draft = Db::name('appraisal_drafts')->where('id', $draftId)->where('user_id', app_user_id($request))->find();
if (!$draft) {
return api_error('草稿不存在', 404);
}
$currentStep = (int)$request->input('current_step', $draft['current_step']);
$productInfo = (array)$request->input('product_info', []);
$extraInfo = (array)$request->input('extra_info', []);
$uploadInfo = (array)$request->input('upload_info', []);
Db::name('appraisal_drafts')
->where('id', $draftId)
->update([
'service_provider' => $request->input('service_provider', $draft['service_provider']),
'current_step' => $currentStep,
'updated_at' => date('Y-m-d H:i:s'),
]);
if ($productInfo) {
$brandId = !empty($productInfo['brand_id']) ? (int)$productInfo['brand_id'] : null;
$brandName = $this->limitText(trim((string)($productInfo['brand_name'] ?? '')), 128);
if ($brandName === '' && $brandId) {
$brandName = $this->lookupName('catalog_brands', 'name', $brandId);
}
$payload = [
'draft_id' => $draftId,
'category_id' => $productInfo['category_id'] ?? null,
'brand_id' => $brandId,
'brand_name' => $brandName,
'color' => $productInfo['color'] ?? '',
'size_spec' => $productInfo['size_spec'] ?? '',
'serial_no' => $productInfo['serial_no'] ?? '',
'updated_at' => date('Y-m-d H:i:s'),
];
$exists = Db::name('appraisal_draft_products')->where('draft_id', $draftId)->find();
if ($exists) {
Db::name('appraisal_draft_products')->where('draft_id', $draftId)->update($payload);
} else {
$payload['created_at'] = date('Y-m-d H:i:s');
Db::name('appraisal_draft_products')->insert($payload);
}
}
if ($extraInfo) {
$purchaseDate = $extraInfo['purchase_date'] ?? null;
if ($purchaseDate === '') {
$purchaseDate = null;
}
$payload = [
'draft_id' => $draftId,
'purchase_channel' => $extraInfo['purchase_channel'] ?? '',
'purchase_price' => $extraInfo['purchase_price'] ?? 0,
'purchase_date' => $purchaseDate,
'usage_status' => $extraInfo['usage_status'] ?? '',
'condition_desc' => $extraInfo['condition_desc'] ?? '',
'has_accessories' => !empty($extraInfo['accessories']) ? 1 : 0,
'accessories_json' => json_encode($extraInfo['accessories'] ?? [], JSON_UNESCAPED_UNICODE),
'remark' => $extraInfo['remark'] ?? '',
'updated_at' => date('Y-m-d H:i:s'),
];
$exists = Db::name('appraisal_draft_extras')->where('draft_id', $draftId)->find();
if ($exists) {
Db::name('appraisal_draft_extras')->where('draft_id', $draftId)->update($payload);
} else {
$payload['created_at'] = date('Y-m-d H:i:s');
Db::name('appraisal_draft_extras')->insert($payload);
}
}
if ($uploadInfo) {
$draftUploadIds = Db::name('appraisal_draft_uploads')->where('draft_id', $draftId)->column('id');
if ($draftUploadIds) {
Db::name('appraisal_draft_upload_files')->whereIn('draft_upload_id', $draftUploadIds)->delete();
}
Db::name('appraisal_draft_uploads')->where('draft_id', $draftId)->delete();
foreach (($uploadInfo['items'] ?? []) as $item) {
$draftUploadId = Db::name('appraisal_draft_uploads')->insertGetId([
'draft_id' => $draftId,
'template_id' => $uploadInfo['template_id'] ?? null,
'item_code' => $item['item_code'] ?? '',
'item_name' => $item['item_name'] ?? '',
'is_required' => !empty($item['is_required']) ? 1 : 0,
'quality_status' => $item['quality_status'] ?? 'pending',
'quality_message' => $item['quality_message'] ?? '',
'created_at' => date('Y-m-d H:i:s'),
'updated_at' => date('Y-m-d H:i:s'),
]);
foreach (($item['files'] ?? []) as $index => $file) {
Db::name('appraisal_draft_upload_files')->insert([
'draft_upload_id' => $draftUploadId,
'file_id' => $file['file_id'] ?? '',
'file_url' => $this->assetUrlService()->storagePath((string)($file['file_url'] ?? '')),
'thumbnail_url' => $this->assetUrlService()->storagePath((string)($file['thumbnail_url'] ?? ($file['file_url'] ?? ''))),
'sort_order' => $index,
'created_at' => date('Y-m-d H:i:s'),
'updated_at' => date('Y-m-d H:i:s'),
]);
}
}
}
return api_success(['draft_id' => $draftId, 'current_step' => $currentStep]);
}
public function uploadTemplate(Request $request)
{
$categoryId = (int)$request->input('category_id', 1);
$serviceProvider = (string)$request->input('service_provider', 'anxinyan');
$template = Db::name('upload_templates')
->where('scope_type', 'category')
->where('scope_id', $categoryId)
->where('service_provider', $serviceProvider)
->where('is_enabled', 1)
->find();
if (!$template) {
$template = Db::name('upload_templates')
->where('scope_type', 'category')
->where('scope_id', $categoryId)
->where('service_provider', 'anxinyan')
->where('is_enabled', 1)
->find();
}
if (!$template) {
return api_success([
'template_id' => 0,
'required_items' => [],
'optional_items' => [],
]);
}
$items = Db::name('upload_template_items')
->where('template_id', $template['id'])
->where('is_enabled', 1)
->order('sort_order', 'asc')
->select()
->toArray();
$requiredItems = [];
$optionalItems = [];
foreach ($items as $item) {
$payload = [
'item_code' => $item['item_code'],
'item_name' => $item['item_name'],
'guide_text' => $item['guide_text'],
'sample_image_url' => $this->assetUrlService()->normalizeUrl((string)$item['sample_image_url'], $request),
'is_required' => (bool)$item['is_required'],
'quality_status' => $item['is_required'] ? 'pending' : 'optional',
'quality_message' => '',
];
if ($item['is_required']) {
$requiredItems[] = $payload;
} else {
$optionalItems[] = $payload;
}
}
return api_success([
'template_id' => (int)$template['id'],
'required_items' => $requiredItems,
'optional_items' => $optionalItems,
]);
}
public function preview(Request $request)
{
$draftId = (int)$request->input('draft_id', 0);
$draft = Db::name('appraisal_drafts')->where('id', $draftId)->where('user_id', app_user_id($request))->find();
$product = Db::name('appraisal_draft_products')->where('draft_id', $draftId)->find();
$extra = Db::name('appraisal_draft_extras')->where('draft_id', $draftId)->find();
if (!$draft) {
return api_error('预览数据不存在', 404);
}
$serviceConfig = $this->serviceConfig((string)$draft['service_provider']);
$policyConfig = (new ContentService())->getPolicyConfig();
return api_success([
'service_summary' => [
'service_provider' => $draft['service_provider'],
'service_provider_text' => $draft['service_provider'] === 'zhongjian' ? '中检鉴定' : '实物鉴定',
],
'product_summary' => [
'product_name' => $this->resolveProductName($product),
'category_name' => $this->lookupName('catalog_categories', 'name', $product['category_id'] ?? null),
'brand_name' => $this->resolveBrandName($product),
'price' => $extra['purchase_price'] ?? 0,
],
'upload_summary' => [
'uploaded_count' => $this->countUploadedDraftItems($draftId),
],
'fee_detail' => [
'service_fee' => (float)$serviceConfig['price'],
'discount_fee' => 0,
'pay_amount' => (float)$serviceConfig['price'],
],
'agreements' => $policyConfig['appraisal_agreements'],
]);
}
public function submit(Request $request)
{
$userId = app_user_id($request);
$draftId = (int)$request->input('draft_id', 0);
$returnAddressId = (int)$request->input('return_address_id', 0);
$sourceChannel = $this->normalizeOrderSourceChannel((string)$request->input('source_channel', 'mini_program'));
$sourceCustomerId = trim((string)$request->input('source_customer_id', ''));
if ($sourceChannel === 'enterprise_push' && $sourceCustomerId === '') {
return api_error('大客户推送订单必须提供客户 ID', 422);
}
if ($sourceChannel !== 'enterprise_push') {
$sourceCustomerId = '';
}
$draft = Db::name('appraisal_drafts')->where('id', $draftId)->where('user_id', $userId)->find();
$product = Db::name('appraisal_draft_products')->where('draft_id', $draftId)->find();
$extra = Db::name('appraisal_draft_extras')->where('draft_id', $draftId)->find();
if (!$draft || !$product) {
return api_error('提交数据不完整', 422);
}
$serviceConfig = $this->serviceConfig((string)$draft['service_provider']);
$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'])));
$productName = $this->resolveProductName($product);
$warehouseService = new WarehouseService();
$defaultAddress = Db::name('user_addresses')
->where('user_id', $userId)
->where('is_default', 1)
->find();
$returnAddress = null;
if ($returnAddressId > 0) {
$returnAddress = Db::name('user_addresses')
->where('id', $returnAddressId)
->where('user_id', $userId)
->find();
if (!$returnAddress) {
return api_error('寄回地址不存在,请重新选择', 422);
}
}
if (!$returnAddress) {
$returnAddress = $defaultAddress ?: Db::name('user_addresses')
->where('user_id', $userId)
->order('id', 'desc')
->find();
}
if (!$returnAddress) {
return api_error('请先添加并确认寄回地址', 422);
}
Db::startTrans();
try {
$orderId = Db::name('orders')->insertGetId([
'order_no' => $orderNo,
'appraisal_no' => $appraisalNo,
'user_id' => $userId,
'service_mode' => $draft['service_mode'],
'service_provider' => $draft['service_provider'],
'payment_status' => 'paid',
'order_status' => 'pending_shipping',
'display_status' => '待寄送商品',
'estimated_finish_time' => $estimated,
'source_channel' => $sourceChannel,
'source_customer_id' => $sourceCustomerId,
'pay_amount' => $serviceConfig['price'],
'paid_at' => $now,
'created_at' => $now,
'updated_at' => $now,
]);
Db::name('order_products')->insert([
'order_id' => $orderId,
'category_id' => $product['category_id'] ?? null,
'category_name' => $this->lookupName('catalog_categories', 'name', $product['category_id'] ?? null),
'brand_id' => !empty($product['brand_id']) ? (int)$product['brand_id'] : null,
'brand_name' => $this->resolveBrandName($product),
'color' => $product['color'] ?? '',
'size_spec' => $product['size_spec'] ?? '',
'serial_no' => $product['serial_no'] ?? '',
'product_name' => $productName,
'product_cover' => '',
'created_at' => $now,
'updated_at' => $now,
]);
Db::name('order_extras')->insert([
'order_id' => $orderId,
'purchase_channel' => $extra['purchase_channel'] ?? '',
'purchase_price' => $extra['purchase_price'] ?? 0,
'purchase_date' => $extra['purchase_date'] ?? null,
'usage_status' => $extra['usage_status'] ?? '',
'condition_desc' => $extra['condition_desc'] ?? '',
'has_accessories' => $extra['has_accessories'] ?? 0,
'accessories_json' => $extra['accessories_json'] ?? json_encode([], JSON_UNESCAPED_UNICODE),
'remark' => $extra['remark'] ?? '',
'created_at' => $now,
'updated_at' => $now,
]);
if ($returnAddress) {
Db::name('order_return_addresses')->insert([
'order_id' => $orderId,
'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'] ?? '',
'created_at' => $now,
'updated_at' => $now,
]);
}
$shippingTarget = $warehouseService->bindOrderTarget(
$orderId,
(string)$draft['service_provider'],
!empty($product['category_id']) ? (int)$product['category_id'] : null,
$defaultAddress ?: null
);
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,
],
]);
$draftUploads = Db::name('appraisal_draft_uploads')->where('draft_id', $draftId)->select()->toArray();
foreach ($draftUploads as $draftUpload) {
$draftFiles = Db::name('appraisal_draft_upload_files')->where('draft_upload_id', $draftUpload['id'])->select()->toArray();
if (!$draftFiles) {
continue;
}
$orderUploadId = Db::name('order_upload_items')->insertGetId([
'order_id' => $orderId,
'template_id' => $draftUpload['template_id'],
'item_code' => $draftUpload['item_code'],
'item_name' => $draftUpload['item_name'],
'is_required' => $draftUpload['is_required'],
'source_type' => 'initial',
'status' => $draftUpload['quality_status'],
'created_at' => $now,
'updated_at' => $now,
]);
foreach ($draftFiles as $draftFile) {
Db::name('order_upload_files')->insert([
'order_upload_item_id' => $orderUploadId,
'file_id' => $draftFile['file_id'],
'file_url' => $draftFile['file_url'],
'thumbnail_url' => $draftFile['thumbnail_url'],
'quality_status' => $draftUpload['quality_status'],
'quality_message' => $draftUpload['quality_message'],
'uploaded_by_user_id' => $userId,
'created_at' => $now,
'updated_at' => $now,
]);
}
}
Db::name('appraisal_tasks')->insert([
'order_id' => $orderId,
'task_stage' => 'first_review',
'service_provider' => $draft['service_provider'],
'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,
]);
Db::name('appraisal_drafts')->where('id', $draftId)->update([
'status' => 'submitted',
'updated_at' => $now,
]);
(new MessageDispatcher())->sendInboxEvent('order_created', [
'user_id' => $userId,
'biz_type' => 'order',
'biz_id' => (int)$orderId,
'order_no' => $orderNo,
'appraisal_no' => $appraisalNo,
'product_name' => $productName,
'pay_amount' => (string)$serviceConfig['price'],
'fallback_title' => '订单提交成功',
'fallback_content' => '您的鉴定订单已提交成功,可前往订单中心查看进度。',
]);
Db::commit();
} catch (\Throwable $e) {
Db::rollback();
return api_error('提交失败,请稍后重试', 500, [
'detail' => $e->getMessage(),
]);
}
return api_success([
'order_id' => (int)$orderId,
'order_no' => $orderNo,
'appraisal_no' => $appraisalNo,
'pay_amount' => (float)$serviceConfig['price'],
'next_status' => 'pending_shipping',
]);
}
private function lookupName(string $table, string $field, mixed $id): string
{
if (empty($id)) {
return '';
}
return (string)Db::name($table)->where('id', $id)->value($field);
}
private function resolveBrandName(?array $product): string
{
if (!$product) {
return '';
}
$brandName = trim((string)($product['brand_name'] ?? ''));
if ($brandName !== '') {
return $brandName;
}
return $this->lookupName('catalog_brands', 'name', $product['brand_id'] ?? null);
}
private function resolveProductName(?array $product): string
{
if (!$product) {
return '';
}
$categoryName = $this->lookupName('catalog_categories', 'name', $product['category_id'] ?? null);
$brandName = $this->resolveBrandName($product);
$fallbackName = trim($categoryName . ' ' . $brandName);
if ($fallbackName !== '') {
return $fallbackName;
}
return '';
}
private function limitText(string $value, int $maxLength): string
{
if (function_exists('mb_substr')) {
return mb_substr($value, 0, $maxLength, 'UTF-8');
}
return substr($value, 0, $maxLength);
}
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 draftUploadItems(int $draftId, Request $request): array
{
$uploads = Db::name('appraisal_draft_uploads')->where('draft_id', $draftId)->select()->toArray();
if (!$uploads) {
return [];
}
return array_map(function (array $item) use ($request) {
$files = Db::name('appraisal_draft_upload_files')
->where('draft_upload_id', $item['id'])
->order('sort_order', 'asc')
->select()
->toArray();
return [
'item_code' => $item['item_code'],
'item_name' => $item['item_name'],
'is_required' => (bool)$item['is_required'],
'quality_status' => $item['quality_status'],
'quality_message' => $item['quality_message'],
'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),
];
}, $uploads);
}
private function countUploadedDraftItems(int $draftId): int
{
$uploadIds = Db::name('appraisal_draft_upload_files')
->alias('f')
->join('appraisal_draft_uploads u', 'u.id = f.draft_upload_id')
->where('u.draft_id', $draftId)
->group('u.id')
->column('u.id');
return count($uploadIds);
}
private function assetUrlService(): PublicAssetUrlService
{
return new PublicAssetUrlService();
}
private function storage(): FileStorageService
{
return new FileStorageService();
}
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',
];
$sourceChannel = $aliases[$sourceChannel] ?? $sourceChannel;
return in_array($sourceChannel, ['mini_program', 'h5', 'enterprise_push'], true)
? $sourceChannel
: 'mini_program';
}
}