feat: manage group buy order readiness

This commit is contained in:
王鹏
2026-07-07 17:45:31 +08:00
parent 09217c4821
commit 4658b35aaf
5 changed files with 161 additions and 4 deletions

View File

@@ -1,5 +1,9 @@
import { describe, expect, it } from 'vitest' import { describe, expect, it } from 'vitest'
import { filterPendingDeliveryGroupBuyOrders, groupBuyOrderDispatchLabel } from './groupBuyApi' import {
buildGroupBuyOrderActionPath,
filterPendingDeliveryGroupBuyOrders,
groupBuyOrderDispatchLabel
} from './groupBuyApi'
import type { GroupBuyOrder } from '../types' import type { GroupBuyOrder } from '../types'
describe('group buy dispatch helpers', () => { describe('group buy dispatch helpers', () => {
@@ -21,6 +25,11 @@ describe('group buy dispatch helpers', () => {
amountCent: 5980 amountCent: 5980
}))).toBe('GB00000001 Today eggs x2 ¥59.80') }))).toBe('GB00000001 Today eggs x2 ¥59.80')
}) })
it('builds admin action paths for group buy orders', () => {
expect(buildGroupBuyOrderActionPath(1, 'confirm')).toBe('/api/admin/group-buy-orders/1/confirm')
expect(buildGroupBuyOrderActionPath(1, 'ready')).toBe('/api/admin/group-buy-orders/1/ready')
})
}) })
function groupBuyOrder(overrides: Partial<GroupBuyOrder>): GroupBuyOrder { function groupBuyOrder(overrides: Partial<GroupBuyOrder>): GroupBuyOrder {

View File

@@ -17,6 +17,18 @@ export function groupBuyOrderDispatchLabel(order: GroupBuyOrder) {
return `${order.orderNo} ${order.groupBuyTitle} x${order.quantity} ¥${(order.amountCent / 100).toFixed(2)}` return `${order.orderNo} ${order.groupBuyTitle} x${order.quantity} ¥${(order.amountCent / 100).toFixed(2)}`
} }
export function buildGroupBuyOrderActionPath(id: number, action: 'confirm' | 'ready') {
return `/api/admin/group-buy-orders/${id}/${action}`
}
export function confirmGroupBuyOrder(id: number) {
return apiPut<GroupBuyOrder>(buildGroupBuyOrderActionPath(id, 'confirm'))
}
export function markGroupBuyOrderReady(id: number) {
return apiPut<GroupBuyOrder>(buildGroupBuyOrderActionPath(id, 'ready'))
}
export function createGroupBuy(data: Partial<GroupBuy>) { export function createGroupBuy(data: Partial<GroupBuy>) {
return apiPost<GroupBuy>('/api/admin/group-buys', data) return apiPost<GroupBuy>('/api/admin/group-buys', data)
} }

View File

@@ -1,20 +1,27 @@
<script setup lang="ts"> <script setup lang="ts">
import { onMounted, reactive, ref } from 'vue' import { onMounted, reactive, ref } from 'vue'
import { Check, Close, Plus, Refresh, VideoPlay } from '@element-plus/icons-vue' import { Check, Close, Plus, Refresh, Tickets, VideoPlay } from '@element-plus/icons-vue'
import { ElMessage } from 'element-plus' import { ElMessage } from 'element-plus'
import { import {
closeGroupBuy, closeGroupBuy,
completeGroupBuy, completeGroupBuy,
confirmGroupBuyOrder,
createGroupBuy, createGroupBuy,
listGroupBuyOrders,
listGroupBuys, listGroupBuys,
markGroupBuyOrderReady,
startGroupBuy startGroupBuy
} from '../api/groupBuyApi' } from '../api/groupBuyApi'
import type { GroupBuy } from '../types' import type { GroupBuy, GroupBuyOrder } from '../types'
import { statusText, yuan } from '../utils/format' import { statusText, yuan } from '../utils/format'
const loading = ref(false) const loading = ref(false)
const orderLoading = ref(false)
const rows = ref<GroupBuy[]>([]) const rows = ref<GroupBuy[]>([])
const orders = ref<GroupBuyOrder[]>([])
const activeGroupBuy = ref<GroupBuy | null>(null)
const dialog = ref(false) const dialog = ref(false)
const orderDialog = ref(false)
const form = reactive({ const form = reactive({
title: '', title: '',
coverUrl: '', coverUrl: '',
@@ -37,6 +44,24 @@ async function load() {
} }
} }
async function openOrders(row: GroupBuy) {
activeGroupBuy.value = row
orderDialog.value = true
await loadOrders(row.id)
}
async function loadOrders(groupBuyId = activeGroupBuy.value?.id) {
if (!groupBuyId) return
orderLoading.value = true
try {
orders.value = await listGroupBuyOrders(groupBuyId)
} catch (error) {
ElMessage.error(error instanceof Error ? error.message : '加载失败')
} finally {
orderLoading.value = false
}
}
async function save() { async function save() {
try { try {
await createGroupBuy(form) await createGroupBuy(form)
@@ -58,6 +83,17 @@ async function run(action: () => Promise<GroupBuy>) {
} }
} }
async function runOrder(action: () => Promise<GroupBuyOrder>) {
try {
await action()
ElMessage.success('操作成功')
await loadOrders()
await load()
} catch (error) {
ElMessage.error(error instanceof Error ? error.message : '操作失败')
}
}
onMounted(load) onMounted(load)
</script> </script>
@@ -79,18 +115,46 @@ onMounted(load)
<template #default="{ row }">{{ statusText(row.status) }}</template> <template #default="{ row }">{{ statusText(row.status) }}</template>
</el-table-column> </el-table-column>
<el-table-column prop="endTime" label="截单时间" min-width="170" /> <el-table-column prop="endTime" label="截单时间" min-width="170" />
<el-table-column label="操作" width="260" fixed="right"> <el-table-column label="操作" width="330" fixed="right">
<template #default="{ row }"> <template #default="{ row }">
<div class="table-action"> <div class="table-action">
<el-button size="small" :icon="VideoPlay" @click="run(() => startGroupBuy(row.id))">开始</el-button> <el-button size="small" :icon="VideoPlay" @click="run(() => startGroupBuy(row.id))">开始</el-button>
<el-button size="small" :icon="Close" @click="run(() => closeGroupBuy(row.id))">截单</el-button> <el-button size="small" :icon="Close" @click="run(() => closeGroupBuy(row.id))">截单</el-button>
<el-button size="small" :icon="Check" @click="run(() => completeGroupBuy(row.id))">完成</el-button> <el-button size="small" :icon="Check" @click="run(() => completeGroupBuy(row.id))">完成</el-button>
<el-button size="small" :icon="Tickets" @click="openOrders(row)">订单</el-button>
</div> </div>
</template> </template>
</el-table-column> </el-table-column>
</el-table> </el-table>
</div> </div>
<el-dialog v-model="orderDialog" :title="activeGroupBuy ? `${activeGroupBuy.title} 订单` : '团购订单'" width="760px">
<div class="toolbar">
<el-button :icon="Refresh" :loading="orderLoading" @click="loadOrders()">刷新</el-button>
</div>
<el-table :data="orders" border v-loading="orderLoading">
<el-table-column prop="orderNo" label="订单号" min-width="140" />
<el-table-column prop="quantity" label="数量" width="80" />
<el-table-column label="金额" width="110">
<template #default="{ row }">{{ yuan(row.amountCent) }}</template>
</el-table-column>
<el-table-column label="配送" width="110">
<template #default="{ row }">{{ row.deliveryMethod === 'SELF_PICKUP' ? '自提' : '配送' }}</template>
</el-table-column>
<el-table-column label="状态" width="130">
<template #default="{ row }">{{ statusText(row.status) }}</template>
</el-table-column>
<el-table-column label="操作" width="190" fixed="right">
<template #default="{ row }">
<div class="table-action">
<el-button v-if="row.status === 'PENDING_CONFIRM'" size="small" :icon="Check" @click="runOrder(() => confirmGroupBuyOrder(row.id))">确认</el-button>
<el-button v-if="row.status === 'CONFIRMED'" size="small" type="primary" :icon="Check" @click="runOrder(() => markGroupBuyOrderReady(row.id))">备货完成</el-button>
</div>
</template>
</el-table-column>
</el-table>
</el-dialog>
<el-dialog v-model="dialog" title="新建团购" width="560px"> <el-dialog v-model="dialog" title="新建团购" width="560px">
<el-form label-width="90px"> <el-form label-width="90px">
<el-form-item label="标题"><el-input v-model="form.title" /></el-form-item> <el-form-item label="标题"><el-input v-model="form.title" /></el-form-item>

View File

@@ -77,6 +77,18 @@ public class GroupBuyController {
return ApiResponse.ok(orderService.listByGroupBuy(id)); return ApiResponse.ok(orderService.listByGroupBuy(id));
} }
@PutMapping("/api/admin/group-buy-orders/{id}/confirm")
@RequireRole({"PLATFORM_ADMIN", "COMMUNITY_ADMIN", "MERCHANT"})
public ApiResponse<GroupBuyOrderResponse> confirmOrder(@PathVariable Long id) {
return ApiResponse.ok(orderService.confirm(id));
}
@PutMapping("/api/admin/group-buy-orders/{id}/ready")
@RequireRole({"PLATFORM_ADMIN", "COMMUNITY_ADMIN", "MERCHANT"})
public ApiResponse<GroupBuyOrderResponse> markOrderReady(@PathVariable Long id) {
return ApiResponse.ok(orderService.markReady(id));
}
@GetMapping("/api/mini/group-buys") @GetMapping("/api/mini/group-buys")
public ApiResponse<List<GroupBuyResponse>> listMini() { public ApiResponse<List<GroupBuyResponse>> listMini() {
CurrentUser user = authService.currentUser(); CurrentUser user = authService.currentUser();

View File

@@ -0,0 +1,60 @@
package com.linhelp.groupbuy;
import com.linhelp.common.enums.DeliveryMethod;
import com.linhelp.common.enums.GroupBuyOrderStatus;
import com.linhelp.common.security.AuthService;
import com.linhelp.common.security.CurrentUser;
import org.junit.jupiter.api.Test;
import java.time.LocalDateTime;
import static org.assertj.core.api.Assertions.assertThat;
class GroupBuyControllerTests {
@Test
void adminCanConfirmAndMarkGroupBuyOrderReady() {
GroupBuyService groupBuyService = new GroupBuyService();
GroupBuyOrderService orderService = new GroupBuyOrderService(groupBuyService);
GroupBuyController controller = new GroupBuyController(groupBuyService, orderService, new StubAuthService());
GroupBuyResponse groupBuy = groupBuyService.create(groupBuyRequest());
groupBuyService.start(groupBuy.getId());
GroupBuyOrderResponse order = orderService.create(1000L, orderRequest(groupBuy.getId()));
GroupBuyOrderResponse confirmed = controller.confirmOrder(order.getId()).getData();
GroupBuyOrderResponse ready = controller.markOrderReady(order.getId()).getData();
assertThat(confirmed.getStatus()).isEqualTo(GroupBuyOrderStatus.CONFIRMED);
assertThat(ready.getStatus()).isEqualTo(GroupBuyOrderStatus.PENDING_DELIVERY);
}
private GroupBuyRequest groupBuyRequest() {
GroupBuyRequest request = new GroupBuyRequest();
request.setTenantId(1L);
request.setCommunityId(1L);
request.setTitle("Today eggs");
request.setPriceCent(2990);
request.setStock(100);
request.setStartTime(LocalDateTime.now().minusHours(1));
request.setEndTime(LocalDateTime.now().plusDays(1));
request.setPickupAddress("North gate");
return request;
}
private GroupBuyOrderRequest orderRequest(Long groupBuyId) {
GroupBuyOrderRequest request = new GroupBuyOrderRequest();
request.setTenantId(1L);
request.setCommunityId(1L);
request.setGroupBuyId(groupBuyId);
request.setAddressId(1L);
request.setQuantity(2);
request.setDeliveryMethod(DeliveryMethod.IMMEDIATE);
return request;
}
private static class StubAuthService extends AuthService {
@Override
public CurrentUser currentUser() {
return new CurrentUser(1L, 1L, 1L, "COMMUNITY_ADMIN");
}
}
}