erp-backend/app/Http/Controllers/AfterSaleController.php
2026-04-01 17:07:04 +08:00

527 lines
21 KiB
PHP
Raw Permalink 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\Http\Controllers;
use App\Models\AfterSale;
use App\Models\AfterSaleItem;
use App\Models\Order;
use App\Models\OrderItem;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Validator;
class AfterSaleController extends Controller
{
/**
* 售后列表
*/
public function index(Request $request)
{
$query = AfterSale::with(['order', 'processor', 'creator', 'items'])
->orderBy('created_at', 'desc');
// 筛选条件
if ($request->filled('status')) {
$query->where('status', $request->status);
}
if ($request->filled('type')) {
$query->where('type', $request->type);
}
if ($request->filled('keyword')) {
$keyword = $request->keyword;
$query->where(function ($q) use ($keyword) {
$q->where('short_id', 'like', "%{$keyword}%")
->orWhere('order_short_id', 'like', "%{$keyword}%")
->orWhere('reason', 'like', "%{$keyword}%");
});
}
if ($request->filled('date_range')) {
$dates = explode(',', $request->date_range);
if (count($dates) === 2) {
$query->whereBetween('created_at', [$dates[0], $dates[1]]);
}
}
$perPage = $request->input('limit', 20);
$afterSales = $query->paginate($perPage);
// 格式化数据
$list = $afterSales->map(function ($as) {
return [
'id' => $as->id,
'short_id' => $as->short_id,
'order_id' => $as->order_id,
'order_short_id' => $as->order_short_id,
'type' => $as->type,
'type_text' => AfterSale::getTypeText($as->type),
'status' => $as->status,
'status_text' => AfterSale::getStatusText($as->status),
'reason' => $as->reason,
'description' => $as->description,
'refund_amount' => $as->refund_amount,
'return_express_company' => $as->return_express_company,
'return_express_no' => $as->return_express_no,
'reject_reason' => $as->reject_reason,
'processor_name' => $as->processor?->name,
'processed_at' => $as->processed_at?->format('Y-m-d H:i:s'),
'creator_name' => $as->creator?->name,
'created_at' => $as->created_at?->format('Y-m-d H:i:s'),
'completed_at' => $as->completed_at?->format('Y-m-d H:i:s'),
'items' => $as->items->map(fn($item) => [
'id' => $item->id,
'goods_name' => $item->goods_name,
'erp_sku' => $item->erp_sku,
'platform_sku' => $item->platform_sku,
'quantity' => $item->quantity,
'price' => $item->price,
'total_amount' => $item->total_amount,
]),
'order' => $as->order ? [
'id' => $as->order->id,
'short_id' => $as->order->short_id,
'platform' => $as->order->platform,
'shop_name' => $as->order->shop_name,
'receiver_name' => $as->order->receiver_name,
'receiver_phone' => $as->order->receiver_phone,
'receiver_address' => $as->order->receiver_address,
'total_amount' => $as->order->total_amount,
'order_status' => $as->order->order_status,
'delivery_status' => $as->order->delivery_status,
] : null,
];
});
return response()->json([
'code' => 200,
'data' => [
'list' => $list,
'total' => $afterSales->total(),
'current_page' => $afterSales->currentPage(),
'last_page' => $afterSales->lastPage(),
],
'message' => 'success',
]);
}
/**
* 获取可售后的订单(仅已发货的订单)
*/
public function availableOrders(Request $request)
{
$query = Order::where('order_status', 'shipped')
->orderBy('created_at', 'desc');
if ($request->filled('keyword')) {
$keyword = $request->keyword;
$query->where(function ($q) use ($keyword) {
$q->where('short_id', 'like', "%{$keyword}%")
->orWhere('platform_order_sn', 'like', "%{$keyword}%")
->orWhere('receiver_name', 'like', "%{$keyword}%")
->orWhere('receiver_phone', 'like', "%{$keyword}%");
});
}
$perPage = $request->input('limit', 10);
$orders = $query->paginate($perPage);
$list = $orders->map(function ($order) {
return [
'id' => $order->id,
'short_id' => $order->short_id,
'platform' => $order->platform,
'shop_name' => $order->shop_name,
'receiver_name' => $order->receiver_name,
'receiver_phone' => $order->receiver_phone,
'receiver_address' => $order->receiver_address,
'total_amount' => $order->total_amount,
'goods_amount' => $order->goods_amount,
'freight' => $order->freight,
'express_company' => $order->express_company,
'express_no' => $order->express_no,
'order_status' => $order->order_status,
'delivery_status' => $order->delivery_status,
'delivery_time' => $order->delivery_time,
'items' => $order->items->map(fn($item) => [
'id' => $item->id,
'goods_name' => $item->goods_name,
'erp_sku' => $item->erp_sku,
'platform_sku' => $item->platform_sku,
'quantity' => $item->quantity,
'price' => $item->price,
'total_amount' => $item->total_amount,
]),
];
});
return response()->json([
'code' => 200,
'data' => [
'list' => $list,
'total' => $orders->total(),
'current_page' => $orders->currentPage(),
'last_page' => $orders->lastPage(),
],
'message' => 'success',
]);
}
/**
* 获取可售后的订单(包含未发货的仅退款订单)
*/
public function allAvailableOrders(Request $request)
{
// 已发货订单:退货、换货
// 未发货订单:仅退款
$query = Order::whereIn('order_status', ['shipped', 'pending', 'auditing'])
->where('delivery_status', 'delivered')
->orderBy('created_at', 'desc');
// 仅退款未发货的订单delivery_status = pending
$queryOnlyRefund = Order::whereIn('order_status', ['pending', 'auditing'])
->where('delivery_status', 'pending')
->orderBy('created_at', 'desc');
if ($request->filled('keyword')) {
$keyword = $request->keyword;
$query->where(function ($q) use ($keyword) {
$q->where('short_id', 'like', "%{$keyword}%")
->orWhere('platform_order_sn', 'like', "%{$keyword}%")
->orWhere('receiver_name', 'like', "%{$keyword}%");
});
$queryOnlyRefund->where(function ($q) use ($keyword) {
$q->where('short_id', 'like', "%{$keyword}%")
->orWhere('platform_order_sn', 'like', "%{$keyword}%")
->orWhere('receiver_name', 'like', "%{$keyword}%");
});
}
$perPage = $request->input('limit', 10);
$ordersShipped = $query->paginate($perPage);
$ordersNotShipped = $queryOnlyRefund->paginate($perPage);
$formatOrders = function ($orders) {
return $orders->map(function ($order) {
return [
'id' => $order->id,
'short_id' => $order->short_id,
'platform' => $order->platform,
'shop_name' => $order->shop_name,
'receiver_name' => $order->receiver_name,
'receiver_phone' => $order->receiver_phone,
'receiver_address' => $order->receiver_address,
'total_amount' => $order->total_amount,
'goods_amount' => $order->goods_amount,
'freight' => $order->freight,
'express_company' => $order->express_company,
'express_no' => $order->express_no,
'order_status' => $order->order_status,
'delivery_status' => $order->delivery_status,
'delivery_time' => $order->delivery_time,
'can_refund_only' => $order->delivery_status === 'pending', // 未发货可仅退款
'items' => $order->items->map(fn($item) => [
'id' => $item->id,
'goods_name' => $item->goods_name,
'erp_sku' => $item->erp_sku,
'platform_sku' => $item->platform_sku,
'quantity' => $item->quantity,
'price' => $item->price,
'total_amount' => $item->total_amount,
]),
];
});
};
return response()->json([
'code' => 200,
'data' => [
'shipped_orders' => [
'list' => $formatOrders($ordersShipped),
'total' => $ordersShipped->total(),
'current_page' => $ordersShipped->currentPage(),
'last_page' => $ordersShipped->lastPage(),
],
'not_shipped_orders' => [
'list' => $formatOrders($ordersNotShipped),
'total' => $ordersNotShipped->total(),
'current_page' => $ordersNotShipped->currentPage(),
'last_page' => $ordersNotShipped->lastPage(),
],
],
'message' => 'success',
]);
}
/**
* 售后详情
*/
public function show(string $id)
{
$afterSale = AfterSale::with(['order', 'processor', 'creator', 'items'])->find($id);
if (!$afterSale) {
return response()->json(['code' => 404, 'message' => '售后单不存在'], 404);
}
return response()->json([
'code' => 200,
'data' => [
'id' => $afterSale->id,
'short_id' => $afterSale->short_id,
'order_id' => $afterSale->order_id,
'order_short_id' => $afterSale->order_short_id,
'type' => $afterSale->type,
'type_text' => AfterSale::getTypeText($afterSale->type),
'status' => $afterSale->status,
'status_text' => AfterSale::getStatusText($afterSale->status),
'reason' => $afterSale->reason,
'description' => $afterSale->description,
'refund_amount' => $afterSale->refund_amount,
'return_express_company' => $afterSale->return_express_company,
'return_express_no' => $afterSale->return_express_no,
'reject_reason' => $afterSale->reject_reason,
'processor_name' => $afterSale->processor?->name,
'processed_at' => $afterSale->processed_at?->format('Y-m-d H:i:s'),
'creator_name' => $afterSale->creator?->name,
'created_at' => $afterSale->created_at?->format('Y-m-d H:i:s'),
'completed_at' => $afterSale->completed_at?->format('Y-m-d H:i:s'),
'items' => $afterSale->items->map(fn($item) => [
'id' => $item->id,
'order_item_id' => $item->order_item_id,
'goods_name' => $item->goods_name,
'erp_sku' => $item->erp_sku,
'platform_sku' => $item->platform_sku,
'quantity' => $item->quantity,
'price' => $item->price,
'total_amount' => $item->total_amount,
]),
'order' => $afterSale->order ? [
'id' => $afterSale->order->id,
'short_id' => $afterSale->order->short_id,
'platform' => $afterSale->order->platform,
'shop_name' => $afterSale->order->shop_name,
'receiver_name' => $afterSale->order->receiver_name,
'receiver_phone' => $afterSale->order->receiver_phone,
'receiver_address' => $afterSale->order->receiver_address,
'total_amount' => $afterSale->order->total_amount,
'goods_amount' => $afterSale->order->goods_amount,
'freight' => $afterSale->order->freight,
'express_company' => $afterSale->order->express_company,
'express_no' => $afterSale->order->express_no,
'order_status' => $afterSale->order->order_status,
'delivery_status' => $afterSale->order->delivery_status,
'items' => $afterSale->order->items->map(fn($item) => [
'id' => $item->id,
'goods_name' => $item->goods_name,
'erp_sku' => $item->erp_sku,
'platform_sku' => $item->platform_sku,
'quantity' => $item->quantity,
'price' => $item->price,
'total_amount' => $item->total_amount,
]),
] : null,
],
'message' => 'success',
]);
}
/**
* 创建售后单
*/
public function store(Request $request)
{
$validator = Validator::make($request->all(), [
'order_id' => 'required|integer|exists:orders,id',
'type' => 'required|in:refund_only,return,exchange',
'reason' => 'required|string|max:255',
'description' => 'nullable|string',
'items' => 'required|array|min:1',
'items.*.order_item_id' => 'required|integer|exists:order_items,id',
'items.*.quantity' => 'required|integer|min:1',
'refund_amount' => 'nullable|numeric|min:0',
]);
if ($validator->fails()) {
return response()->json(['code' => 422, 'message' => '验证失败', 'errors' => $validator->errors()], 422);
}
$order = Order::find($request->order_id);
// 仅退款必须未发货
if ($request->type === 'refund_only' && $order->delivery_status !== 'pending') {
return response()->json(['code' => 400, 'message' => '仅退款只能针对未发货的订单'], 400);
}
// 退货/换货必须已发货
if (in_array($request->type, ['return', 'exchange']) && $order->delivery_status !== 'delivered') {
return response()->json(['code' => 400, 'message' => '退货/换货只能针对已发货的订单'], 400);
}
try {
$afterSale = DB::transaction(function () use ($request, $order) {
// 计算退款金额
$refundAmount = 0;
if ($request->filled('refund_amount')) {
$refundAmount = $request->refund_amount;
} else {
// 自动计算:选中的商品小计之和
foreach ($request->items as $item) {
$orderItem = OrderItem::find($item['order_item_id']);
$refundAmount += $orderItem->price * $item['quantity'];
}
// 如果是退货/换货,退款不含运费
if (in_array($request->type, ['return', 'exchange'])) {
$refundAmount = min($refundAmount, $order->goods_amount);
}
}
$afterSale = AfterSale::create([
'short_id' => AfterSale::generateShortId(),
'order_id' => $order->id,
'order_short_id' => $order->short_id,
'type' => $request->type,
'status' => AfterSale::STATUS_PENDING,
'reason' => $request->reason,
'description' => $request->description,
'refund_amount' => $refundAmount,
'created_by' => auth()->id(),
]);
// 创建明细
foreach ($request->items as $item) {
$orderItem = OrderItem::find($item['order_item_id']);
AfterSaleItem::create([
'after_sale_id' => $afterSale->id,
'order_item_id' => $item['order_item_id'],
'goods_id' => $orderItem->goods_id,
'goods_name' => $orderItem->goods_name,
'erp_sku' => $orderItem->erp_sku,
'platform_sku' => $orderItem->platform_sku,
'quantity' => $item['quantity'],
'price' => $orderItem->price,
'total_amount' => $orderItem->price * $item['quantity'],
]);
}
return $afterSale;
});
return response()->json([
'code' => 200,
'data' => $afterSale,
'message' => '售后单创建成功',
]);
} catch (\Exception $e) {
return response()->json(['code' => 500, 'message' => '创建失败:' . $e->getMessage()], 500);
}
}
/**
* 更新售后单状态
*/
public function updateStatus(Request $request, string $id)
{
$validator = Validator::make($request->all(), [
'status' => 'required|in:pending,processing,completed,rejected',
'reject_reason' => 'required_if:status,rejected|nullable|string|max:255',
'return_express_company' => 'nullable|string|max:100',
'return_express_no' => 'nullable|string|max:100',
]);
if ($validator->fails()) {
return response()->json(['code' => 422, 'message' => '验证失败', 'errors' => $validator->errors()], 422);
}
$afterSale = AfterSale::find($id);
if (!$afterSale) {
return response()->json(['code' => 404, 'message' => '售后单不存在'], 404);
}
// 状态流转校验
$allowedTransitions = [
'pending' => ['processing', 'rejected'],
'processing' => ['completed', 'rejected'],
'processing' => ['completed', 'rejected'],
'completed' => [],
'rejected' => [],
];
if (!in_array($request->status, $allowedTransitions[$afterSale->status] ?? [])) {
return response()->json(['code' => 400, 'message' => '当前状态不允许此操作'], 400);
}
$updateData = [
'status' => $request->status,
'processed_by' => auth()->id(),
'processed_at' => now(),
];
if ($request->status === 'rejected') {
$updateData['reject_reason'] = $request->reject_reason;
}
if ($request->status === 'completed') {
$updateData['completed_at'] = now();
}
if ($request->filled('return_express_company')) {
$updateData['return_express_company'] = $request->return_express_company;
}
if ($request->filled('return_express_no')) {
$updateData['return_express_no'] = $request->return_express_no;
}
$afterSale->update($updateData);
return response()->json([
'code' => 200,
'data' => $afterSale,
'message' => '状态更新成功',
]);
}
/**
* 删除售后单(仅待处理状态可删除)
*/
public function destroy(string $id)
{
$afterSale = AfterSale::find($id);
if (!$afterSale) {
return response()->json(['code' => 404, 'message' => '售后单不存在'], 404);
}
if ($afterSale->status !== 'pending') {
return response()->json(['code' => 400, 'message' => '仅待处理状态的售后单可删除'], 400);
}
// 删除明细
$afterSale->items()->delete();
$afterSale->delete();
return response()->json(['code' => 200, 'message' => '删除成功']);
}
/**
* 统计待处理数量
*/
public function stats(Request $request)
{
$pending = AfterSale::where('status', 'pending')->count();
$processing = AfterSale::where('status', 'processing')->count();
$completedToday = AfterSale::where('status', 'completed')
->whereDate('completed_at', today())
->count();
return response()->json([
'code' => 200,
'data' => [
'pending' => $pending,
'processing' => $processing,
'completed_today' => $completedToday,
],
'message' => 'success',
]);
}
}