feat: 去掉商品取货码,管理员可直接发货完成

已付款订单不再生成或核验取货码,管理员确认后即可完成订单。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
richarjiang
2026-09-25 11:22:15 +08:00
parent a76afb19de
commit 21e3ece21c
15 changed files with 35 additions and 68 deletions

View File

@@ -0,0 +1,3 @@
-- Paid product orders are fulfilled directly by an administrator.
-- The pickup code is no longer generated, shown, or required.
ALTER TABLE `product_orders` DROP COLUMN `pickup_code`;

View File

@@ -663,7 +663,6 @@ model ProductOrder {
contactName String @map("contact_name") @db.VarChar(40)
contactPhone String @map("contact_phone") @db.VarChar(20)
status ProductOrderStatus @default(PENDING)
pickupCode String @map("pickup_code") @db.VarChar(12)
wxTransactionId String? @unique @map("wx_transaction_id")
expiresAt DateTime @map("expires_at")
paidAt DateTime? @map("paid_at")

View File

@@ -15,7 +15,6 @@ const order = () => ({
contactName: '顾客',
contactPhone: '13800138000',
status: 'PENDING',
pickupCode: 'ABCDEF123456',
expiresAt: new Date(Date.now() + 900000),
createdAt: new Date(),
updatedAt: new Date(),
@@ -78,7 +77,6 @@ describe('ShopService inventory and payment', () => {
create: jest.fn(async ({ data }) => ({ ...current, ...data })),
updateMany: jest.fn(async ({ where, data }) => {
if (where.status && where.status !== current.status) return { count: 0 }
if (where.pickupCode && where.pickupCode !== current.pickupCode) return { count: 0 }
Object.assign(current, data)
return { count: 1 }
}),
@@ -246,14 +244,13 @@ describe('ShopService inventory and payment', () => {
data: { productId: 'p1', delta: 2, reason: '到货', operatorId: 'admin' },
})
})
it('requires correct pickup code and paid state', async () => {
await expect(service.complete('o1', current.pickupCode, 'admin')).rejects.toThrow('不可核销')
it('completes a paid order directly and only once', async () => {
await expect(service.complete('o1', 'admin')).rejects.toThrow('仅已付款')
current.status = 'PAID'
await expect(service.complete('o1', 'WRONG', 'admin')).rejects.toThrow('不可核销')
await service.complete('o1', current.pickupCode, 'admin')
await service.complete('o1', 'admin')
expect(current.status).toBe('COMPLETED')
expect(current.completedBy).toBe('admin')
await expect(service.complete('o1', current.pickupCode, 'admin')).rejects.toThrow('不可核销')
await expect(service.complete('o1', 'admin')).rejects.toThrow('仅已付款')
})
it('scopes user lists and hides inactive products publicly', async () => {
await service.orders(new ShopQueryDto(), 'u1')

View File

@@ -62,6 +62,3 @@ export class ShopQueryDto {
@IsOptional() @IsString() @MaxLength(80) search?: string
@IsOptional() @IsString() @MaxLength(50) productId?: string
}
export class PickupDto {
@IsString() @Matches(/^[A-F0-9]{12}$/) pickupCode!: string
}

View File

@@ -17,7 +17,7 @@ import { RolesGuard } from '../auth/roles.guard'
import { Roles } from '../auth/roles.decorator'
import { CurrentUser } from '../common/decorators/current-user.decorator'
import { ShopService } from './shop.service'
import { BuyProductDto, PickupDto, ProductDto, ShopQueryDto, StockDto } from './dto/shop.dto'
import { BuyProductDto, ProductDto, ShopQueryDto, StockDto } from './dto/shop.dto'
@Controller('shop')
export class ShopController {
@@ -101,9 +101,8 @@ export class ShopAdminController {
}
@Post('orders/:id/complete') complete(
@Param('id') id: string,
@Body() dto: PickupDto,
@CurrentUser('sub') userId: string,
) {
return this.shop.complete(id, dto.pickupCode, userId)
return this.shop.complete(id, userId)
}
}

View File

@@ -7,7 +7,7 @@ import {
} from '@nestjs/common'
import { Cron } from '@nestjs/schedule'
import { Prisma, ProductOrder } from '@prisma/client'
import { randomBytes, randomUUID } from 'crypto'
import { randomUUID } from 'crypto'
import { PrismaService } from '../prisma/prisma.service'
import { WechatPayService } from '../payment/wechat-pay.service'
import { BuyProductDto, ProductDto, ShopQueryDto, StockDto } from './dto/shop.dto'
@@ -139,7 +139,6 @@ export class ShopService {
coverUrl: product.coverUrl,
unitPrice: product.price,
amount: product.price * dto.quantity,
pickupCode: randomBytes(6).toString('hex').toUpperCase(),
expiresAt: new Date(Date.now() + 15 * 60 * 1000),
},
})
@@ -273,12 +272,12 @@ export class ShopService {
return { items, total, page: query.page, limit: query.limit }
}
async complete(id: string, pickupCode: string, operatorId: string) {
async complete(id: string, operatorId: string) {
const result = await this.prisma.productOrder.updateMany({
where: { id, pickupCode, status: 'PAID' },
where: { id, status: 'PAID' },
data: { status: 'COMPLETED', completedAt: new Date(), completedBy: operatorId },
})
if (!result.count) throw new BadRequestException('提货码不正确,或订单不可核销')
if (!result.count) throw new BadRequestException('仅已付款订单可以发货完成')
return this.prisma.productOrder.findUniqueOrThrow({ where: { id } })
}