527 lines
21 KiB
PHP
527 lines
21 KiB
PHP
<?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',
|
||
]);
|
||
}
|
||
}
|