订单软删除

This commit is contained in:
liu
2026-08-12 09:42:48 +08:00
parent fa4bf6fe61
commit ce1f36b5b4
10 changed files with 127 additions and 6 deletions
File diff suppressed because one or more lines are too long
@@ -12,6 +12,7 @@ use App\Models\UserModel;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Modules\AnnoRoute\Attribute\DeleteRoute;
use Modules\AnnoRoute\Attribute\GetRoute;
use Modules\AnnoRoute\Attribute\PutRoute;
use Modules\AnnoRoute\Attribute\RequestAttribute;
@@ -400,6 +401,27 @@ class StoreOrderController extends BaseController
return $this->success(['success' => $orders->count()]);
}
/**
* 删除订单(软删除):仅已取消订单允许删除;删除后后台列表/详情、小程序端均不可见
*/
#[DeleteRoute(route: '/{id}', authorize: 'delete', where: ['id' => '[0-9]+'])]
public function delete(int $id): JsonResponse
{
$order = StoreOrderModel::find($id);
if (empty($order)) {
throw new RepositoryException('订单不存在');
}
if ($order->status !== StoreOrderModel::STATUS_CANCELLED) {
throw new RepositoryException(
'订单当前状态为「' . (self::STATUS_NAMES[$order->status] ?? $order->status) . '」,仅已取消订单可删除'
);
}
$order->delete();
return $this->success();
}
/**
* 状态流转合法路径(与迁移状态定义一致):
* 待接单→已接单/已取消;采购中→配送中/已完成;配送中→已完成
+3 -2
View File
@@ -6,13 +6,14 @@ use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\SoftDeletes;
/**
* 门店订单模型(小程序下单,快照等级价)
* 门店订单模型(小程序下单,快照等级价;软删除,仅已取消订单可由后台删除
*/
class StoreOrderModel extends Model
{
use HasFactory;
use HasFactory, SoftDeletes;
/** 状态:待接单(可被采购单生成归集、可取消) */
public const int STATUS_PENDING = 0;
+1
View File
@@ -41,6 +41,7 @@ class PurchaseAllocateService
$orderItems = StoreOrderItemModel::query()
->join('store_order', 'store_order.id', '=', 'store_order_item.order_id')
->where('store_order.purchase_id', $purchase->id)
->whereNull('store_order.deleted_at')
->select('store_order_item.*')
->get()
->groupBy('product_id');
@@ -39,6 +39,7 @@ class StatementGenerateService
->whereDate('store_order.order_date', '>=', $periodStart)
->whereDate('store_order.order_date', '<=', $periodEnd)
->where('store_order.status', '<>', StoreOrderModel::STATUS_CANCELLED)
->whereNull('store_order.deleted_at')
->select('store_order_item.*')
->get();
@@ -30,6 +30,7 @@ return new class extends Migration
$table->decimal('tray_num', 10, 2)->default(0)->comment('周转托盘数量');
$table->integer('status')->default(0)->comment('订单状态(0待接单 1已接单 2采购中 3配送中 4已完成 9已取消)');
$table->string('remark', 255)->default('')->comment('订单备注');
$table->softDeletes();
$table->timestamps();
$table->index(['store_id', 'order_date'], 'store_order_store_date_index');
$table->index(['status'], 'store_order_status_index');
+1
View File
@@ -173,6 +173,7 @@ class PermissionSeeder extends Seeder
'children' => [
['type' => 'rule', 'key' => 'order.store.query', 'name' => '查询'],
['type' => 'rule', 'key' => 'order.store.update', 'name' => '状态流转'],
['type' => 'rule', 'key' => 'order.store.delete', 'name' => '删除(仅已取消订单)'],
],
],
],
+53
View File
@@ -250,4 +250,57 @@ class StoreOrderTest extends ProcurementTestCase
->assertJsonPath('success', false)
->assertJsonPath('msg', '周转框数量不能小于 0');
}
/** 软删除:仅已取消订单可由后台删除,删除后后台/小程序端均不可见 */
public function test_soft_delete_only_cancelled_orders(): void
{
$order = $this->makeOrderWithStatus(StoreOrderModel::STATUS_CANCELLED);
// 删除前小程序端可见
$this->getJson('/mini/order')->assertOk()->assertJsonPath('data.total', 1);
$this->actingAsSysUser();
$this->deleteJson("/order/store/{$order->id}")->assertOk()->assertJsonPath('success', true);
$this->assertNotNull($order->fresh()->deleted_at, '软删除应写入 deleted_at');
$this->assertNull(StoreOrderModel::find($order->id), '默认查询不可见软删除订单');
$this->assertNotNull(StoreOrderModel::withTrashed()->find($order->id), '数据仍保留在库中');
// 后台列表/详情不可见
$this->getJson('/order/store')->assertOk()->assertJsonPath('data.total', 0);
$this->getJson("/order/store/{$order->id}")->assertJsonPath('success', false);
// 小程序端历史订单同样不可见
$this->actingAsMiniUser(UserModel::where('store_id', $order->store_id)->first());
$this->getJson('/mini/order')->assertOk()->assertJsonPath('data.total', 0);
$this->getJson("/mini/order/{$order->id}")->assertJsonPath('success', false);
}
/** 软删除:非已取消订单拒绝删除 */
public function test_soft_delete_rejected_for_non_cancelled_orders(): void
{
foreach ([
StoreOrderModel::STATUS_PENDING,
StoreOrderModel::STATUS_SUMMARIZED,
StoreOrderModel::STATUS_DELIVERING,
StoreOrderModel::STATUS_DISTRIBUTION,
StoreOrderModel::STATUS_COMPLETED,
] as $status) {
$order = $this->makeOrderWithStatus($status);
$this->actingAsSysUser();
$this->deleteJson("/order/store/{$order->id}")
->assertOk()->assertJsonPath('success', false);
$this->assertNull($order->fresh()->deleted_at, '非已取消订单不得删除');
}
}
/** 软删除:重复删除返回订单不存在 */
public function test_soft_delete_twice_rejected(): void
{
$order = $this->makeOrderWithStatus(StoreOrderModel::STATUS_CANCELLED);
$this->actingAsSysUser();
$this->deleteJson("/order/store/{$order->id}")->assertOk()->assertJsonPath('success', true);
$this->deleteJson("/order/store/{$order->id}")->assertJsonPath('success', false);
}
}
+8
View File
@@ -61,3 +61,11 @@ export async function syncOrderItem(itemId: number) {
method: 'put',
});
}
/** 删除订单(软删除,仅已取消订单允许) */
export async function deleteStoreOrder(id: number) {
return createAxios({
url: `/order/store/${id}`,
method: 'delete',
});
}
+36 -3
View File
@@ -35,6 +35,7 @@ import {
batchUpdateOrderStatus,
updateOrderItem,
syncOrderItem,
deleteStoreOrder,
} from '@/api/order/store.ts';
import { generatePurchase } from '@/api/purchase/order.ts';
import { getStoreOptions } from '@/api/customer/store.ts';
@@ -42,7 +43,7 @@ import { getSupplierOptions } from '@/api/customer/supplier.ts';
import type IStore from '@/domain/iStore.ts';
import type ISupplier from '@/domain/iSupplier.ts';
import AuthButton from '@/components/AuthButton';
import { EditOutlined, SettingOutlined, SyncOutlined, UnorderedListOutlined } from '@ant-design/icons';
import { DeleteOutlined, EditOutlined, SettingOutlined, SyncOutlined, UnorderedListOutlined } from '@ant-design/icons';
import TextArea from "antd/es/input/TextArea";
const { Title, Text } = Typography;
@@ -224,6 +225,14 @@ const StoreOrderPage: React.FC = () => {
await tableRef.current?.reload();
};
/** 删除订单(软删除,仅已取消订单可删;删除后后台/小程序端均不可见) */
const handleDelete = async (id: number) => {
await deleteStoreOrder(id);
message.success('订单已删除');
setDetailOpen(false);
await tableRef.current?.reload();
};
// 弹窗内实时预览:附加金额 = 框×单价 + 托盘×单价;订单总金额 = 商品金额 + 附加金额
const watchBoxNum = Number(Form.useWatch('box_num', containerForm) ?? 0);
const watchTrayNum = Number(Form.useWatch('tray_num', containerForm) ?? 0);
@@ -338,7 +347,7 @@ const StoreOrderPage: React.FC = () => {
align: 'center',
},
{
title: '订单信息',
title: '附加信息',
hideInForm: true,
dataIndex: 'status',
hideInSearch: true,
@@ -361,7 +370,7 @@ const StoreOrderPage: React.FC = () => {
)
},
{
title: '附加信息',
title: '订单金额',
hideInForm: true,
dataIndex: 'box_num',
hideInSearch: true,
@@ -455,6 +464,17 @@ const StoreOrderPage: React.FC = () => {
</Popconfirm>
</AuthButton>
)}
{ record.status === 9 && (
<AuthButton auth="order.store.delete">
<Popconfirm
title="确认删除该订单吗?"
description="仅已取消订单可删除,删除后后台与小程序端均不可见"
onConfirm={() => handleDelete(record.id!)}
>
<Button icon={<DeleteOutlined />} color='danger' variant="solid" />
</Popconfirm>
</AuthButton>
)}
</Space>
)
}
@@ -542,6 +562,19 @@ const StoreOrderPage: React.FC = () => {
</Popconfirm>
</AuthButton>
)}
{ detail.status === 9 && (
<AuthButton auth="order.store.delete">
<Popconfirm
title="确认删除该订单吗?"
description="仅已取消订单可删除,删除后后台与小程序端均不可见"
onConfirm={() => handleDelete(detail.id!)}
>
<Button icon={<DeleteOutlined />} color='danger' variant="solid">
</Button>
</Popconfirm>
</AuthButton>
)}
</Space>
) : null
}