feat: manage group buy order readiness
This commit is contained in:
@@ -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 {
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user