feat: 已实现订单基本逻辑,到完成,却订单售后
This commit is contained in:
@@ -88,6 +88,8 @@ model SupplierOrderMain {
|
||||
supplierId String
|
||||
userId String?
|
||||
aplName String?
|
||||
addressId String?
|
||||
remark String? @db.Text
|
||||
createTime DateTime?
|
||||
updateTime DateTime?
|
||||
deliveryTime DateTime?
|
||||
@@ -125,6 +127,8 @@ model SupplierOrderSub {
|
||||
token String?
|
||||
userId String?
|
||||
aplName String?
|
||||
addressId String?
|
||||
remark String? @db.Text
|
||||
createTime DateTime?
|
||||
updateTime DateTime?
|
||||
deliveryTime DateTime?
|
||||
@@ -175,6 +179,29 @@ model SupplierOrderProduct {
|
||||
@@map("supplier_order_product")
|
||||
}
|
||||
|
||||
model SupplierUserAddress {
|
||||
id Int @id @default(autoincrement())
|
||||
addressId String @unique
|
||||
supplierId String
|
||||
userId String
|
||||
name String
|
||||
tel String
|
||||
province String?
|
||||
city String?
|
||||
county String?
|
||||
addressDetail String? @db.Text
|
||||
areaCode String?
|
||||
postalCode String?
|
||||
isDefault Boolean @default(false)
|
||||
createTime DateTime @default(now())
|
||||
updateTime DateTime @updatedAt
|
||||
deleted Int @default(0)
|
||||
|
||||
@@index([supplierId, userId])
|
||||
@@index([supplierId, userId, isDefault])
|
||||
@@map("supplier_user_address")
|
||||
}
|
||||
|
||||
model SupplierPointsLedger {
|
||||
id Int @id @default(autoincrement())
|
||||
ledgerNo String @unique
|
||||
@@ -269,6 +296,43 @@ model SupplierRefundExchange {
|
||||
@@map("supplier_refund_exchange")
|
||||
}
|
||||
|
||||
model SupplierAfterSale {
|
||||
id Int @id @default(autoincrement())
|
||||
serviceOrderId String @unique
|
||||
supplierId String
|
||||
userId String?
|
||||
mainOrderId String?
|
||||
subOrderId String?
|
||||
serviceType Int
|
||||
serviceState Int @default(0)
|
||||
auditStatus Int @default(0)
|
||||
logisticsState Int @default(0)
|
||||
amount Decimal @default(0) @db.Decimal(10, 2)
|
||||
reason String? @db.Text
|
||||
auditRemark String? @db.Text
|
||||
userLogisticsNo String?
|
||||
userLogisticsCompany String?
|
||||
merchantLogisticsNo String?
|
||||
merchantLogisticsCompany String?
|
||||
applyTime DateTime @default(now())
|
||||
auditTime DateTime?
|
||||
userShipTime DateTime?
|
||||
merchantReceiveTime DateTime?
|
||||
merchantShipTime DateTime?
|
||||
finishTime DateTime?
|
||||
requestBody Json?
|
||||
createTime DateTime @default(now())
|
||||
updateTime DateTime @updatedAt
|
||||
deleted Int @default(0)
|
||||
|
||||
@@index([supplierId, mainOrderId])
|
||||
@@index([supplierId, userId])
|
||||
@@index([subOrderId])
|
||||
@@index([serviceType, serviceState])
|
||||
@@index([auditStatus, logisticsState])
|
||||
@@map("supplier_after_sale")
|
||||
}
|
||||
|
||||
model SupplierInvoiceInfo {
|
||||
id Int @id @default(autoincrement())
|
||||
invoiceBizId String @unique
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
ALTER TABLE `supplier_order_main`
|
||||
ADD COLUMN `addressId` VARCHAR(191) NULL COMMENT '收货地址ID' AFTER `aplName`,
|
||||
ADD COLUMN `remark` TEXT NULL COMMENT '订单备注' AFTER `addressId`;
|
||||
|
||||
ALTER TABLE `supplier_order_sub`
|
||||
ADD COLUMN `addressId` VARCHAR(191) NULL COMMENT '收货地址ID' AFTER `aplName`,
|
||||
ADD COLUMN `remark` TEXT NULL COMMENT '订单备注' AFTER `addressId`;
|
||||
@@ -0,0 +1,36 @@
|
||||
CREATE TABLE IF NOT EXISTS `supplier_after_sale` (
|
||||
`id` INT NOT NULL AUTO_INCREMENT COMMENT '供应商售后单主键ID',
|
||||
`serviceOrderId` VARCHAR(191) NOT NULL COMMENT '售后业务单号',
|
||||
`supplierId` VARCHAR(191) NOT NULL COMMENT '供应商ID',
|
||||
`userId` VARCHAR(191) NULL COMMENT '用户ID',
|
||||
`mainOrderId` VARCHAR(191) NULL COMMENT '关联主单ID',
|
||||
`subOrderId` VARCHAR(191) NULL COMMENT '关联子单ID',
|
||||
`serviceType` INT NOT NULL COMMENT '售后类型:4退货,5换货',
|
||||
`serviceState` INT NOT NULL DEFAULT 0 COMMENT '售后状态:0商家待审核,1审核通过,2审核拒绝,3用户已寄回,4商家已收货,5已完成,6已取消',
|
||||
`auditStatus` INT NOT NULL DEFAULT 0 COMMENT '审核状态:0待审核,1审核通过,2审核拒绝',
|
||||
`logisticsState` INT NOT NULL DEFAULT 0 COMMENT '物流状态:0无物流,1待用户寄回,2用户已寄回,3商家已收货,4商家已发货,5用户已收货',
|
||||
`amount` DECIMAL(10, 2) NOT NULL DEFAULT 0 COMMENT '售后金额',
|
||||
`reason` TEXT NULL COMMENT '售后原因',
|
||||
`auditRemark` TEXT NULL COMMENT '审核备注',
|
||||
`userLogisticsNo` VARCHAR(191) NULL COMMENT '用户寄回物流单号',
|
||||
`userLogisticsCompany` VARCHAR(191) NULL COMMENT '用户寄回物流公司',
|
||||
`merchantLogisticsNo` VARCHAR(191) NULL COMMENT '商家换货物流单号',
|
||||
`merchantLogisticsCompany` VARCHAR(191) NULL COMMENT '商家换货物流公司',
|
||||
`applyTime` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '申请时间',
|
||||
`auditTime` DATETIME(3) NULL COMMENT '审核时间',
|
||||
`userShipTime` DATETIME(3) NULL COMMENT '用户寄回时间',
|
||||
`merchantReceiveTime` DATETIME(3) NULL COMMENT '商家收货时间',
|
||||
`merchantShipTime` DATETIME(3) NULL COMMENT '商家发货时间',
|
||||
`finishTime` DATETIME(3) NULL COMMENT '完成时间',
|
||||
`requestBody` JSON NULL COMMENT '原始业务请求体',
|
||||
`createTime` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
|
||||
`updateTime` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '更新时间',
|
||||
`deleted` INT NOT NULL DEFAULT 0 COMMENT '删除状态:0未删除,1已删除',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `supplier_after_sale_serviceOrderId_key` (`serviceOrderId`),
|
||||
KEY `supplier_after_sale_supplierId_mainOrderId_idx` (`supplierId`, `mainOrderId`),
|
||||
KEY `supplier_after_sale_supplierId_userId_idx` (`supplierId`, `userId`),
|
||||
KEY `supplier_after_sale_subOrderId_idx` (`subOrderId`),
|
||||
KEY `supplier_after_sale_serviceType_serviceState_idx` (`serviceType`, `serviceState`),
|
||||
KEY `supplier_after_sale_auditStatus_logisticsState_idx` (`auditStatus`, `logisticsState`)
|
||||
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci COMMENT='供应商退款售后流程表';
|
||||
@@ -0,0 +1,22 @@
|
||||
CREATE TABLE IF NOT EXISTS `supplier_user_address` (
|
||||
`id` INT NOT NULL AUTO_INCREMENT COMMENT '地址主键ID',
|
||||
`addressId` VARCHAR(191) NOT NULL COMMENT '地址业务ID',
|
||||
`supplierId` VARCHAR(191) NOT NULL COMMENT '供应商ID',
|
||||
`userId` VARCHAR(191) NOT NULL COMMENT '用户ID',
|
||||
`name` VARCHAR(191) NOT NULL COMMENT '收货人姓名',
|
||||
`tel` VARCHAR(191) NOT NULL COMMENT '收货人手机号',
|
||||
`province` VARCHAR(191) NULL COMMENT '省',
|
||||
`city` VARCHAR(191) NULL COMMENT '市',
|
||||
`county` VARCHAR(191) NULL COMMENT '区县',
|
||||
`addressDetail` TEXT NULL COMMENT '详细地址',
|
||||
`areaCode` VARCHAR(191) NULL COMMENT '地区编码',
|
||||
`postalCode` VARCHAR(191) NULL COMMENT '邮政编码',
|
||||
`isDefault` BOOLEAN NOT NULL DEFAULT FALSE COMMENT '是否默认地址',
|
||||
`createTime` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
|
||||
`updateTime` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '更新时间',
|
||||
`deleted` INT NOT NULL DEFAULT 0 COMMENT '删除状态:0未删除,1已删除',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `supplier_user_address_addressId_key` (`addressId`),
|
||||
KEY `supplier_user_address_supplierId_userId_idx` (`supplierId`, `userId`),
|
||||
KEY `supplier_user_address_supplierId_userId_isDefault_idx` (`supplierId`, `userId`, `isDefault`)
|
||||
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci COMMENT='供应商用户收货地址表';
|
||||
@@ -23,7 +23,8 @@ export const ORDER_EVENT_TYPE = {
|
||||
EXCHANGE_ORDER: 5,
|
||||
CONFIRM_RECEIVE: 6,
|
||||
CANCEL_PAID_NOT_SPLIT_MAIN: 7,
|
||||
INVOICE_INFO: 11
|
||||
INVOICE_INFO: 11,
|
||||
SHIP_ORDER: 99
|
||||
} as const
|
||||
|
||||
export const POINTS_LEDGER_BIZ_TYPE = {
|
||||
@@ -44,6 +45,25 @@ export const ORDER_PUSH_STATUS = {
|
||||
FAILED: 2
|
||||
} as const
|
||||
|
||||
export const AFTER_SALE_SERVICE_STATE = {
|
||||
PENDING_AUDIT: 0,
|
||||
APPROVED: 1,
|
||||
REJECTED: 2,
|
||||
USER_SHIPPED: 3,
|
||||
RECEIVED: 4,
|
||||
FINISHED: 5,
|
||||
CANCELLED: 6
|
||||
} as const
|
||||
|
||||
export const AFTER_SALE_LOGISTICS_STATE = {
|
||||
NONE: 0,
|
||||
WAIT_USER_SHIP: 1,
|
||||
USER_SHIPPED: 2,
|
||||
MERCHANT_RECEIVED: 3,
|
||||
MERCHANT_SHIPPED: 4,
|
||||
USER_RECEIVED: 5
|
||||
} as const
|
||||
|
||||
export const ORDER_OPERATE_TYPE = {
|
||||
CREATE_PENDING: 1,
|
||||
PAY_SUCCESS: 2,
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import type { Context } from '../utils/handler.js'
|
||||
import { guardMethod } from '../utils/method.js'
|
||||
import { readJson } from '../utils/request.js'
|
||||
import { withErrorBoundary } from '../utils/handler.js'
|
||||
import {
|
||||
addressService,
|
||||
type AddressDefaultInput,
|
||||
type AddressDeleteInput,
|
||||
type AddressListInput,
|
||||
type AddressSaveInput
|
||||
} from '../services/address.service.js'
|
||||
import { toHttpResponse } from './controller-response.js'
|
||||
|
||||
export const addressListController = withErrorBoundary(async (request: Request, _context: Context) => {
|
||||
const methodResponse = guardMethod(request, ['POST'])
|
||||
if (methodResponse) return methodResponse
|
||||
|
||||
return toHttpResponse(await addressService.list(await readJson<AddressListInput>(request)))
|
||||
})
|
||||
|
||||
export const addressDefaultController = withErrorBoundary(async (request: Request, _context: Context) => {
|
||||
const methodResponse = guardMethod(request, ['POST'])
|
||||
if (methodResponse) return methodResponse
|
||||
|
||||
return toHttpResponse(await addressService.default(await readJson<AddressDefaultInput>(request)))
|
||||
})
|
||||
|
||||
export const addressSaveController = withErrorBoundary(async (request: Request, _context: Context) => {
|
||||
const methodResponse = guardMethod(request, ['POST'])
|
||||
if (methodResponse) return methodResponse
|
||||
|
||||
return toHttpResponse(await addressService.save(await readJson<AddressSaveInput>(request)))
|
||||
})
|
||||
|
||||
export const addressDeleteController = withErrorBoundary(async (request: Request, _context: Context) => {
|
||||
const methodResponse = guardMethod(request, ['POST'])
|
||||
if (methodResponse) return methodResponse
|
||||
|
||||
return toHttpResponse(await addressService.delete(await readJson<AddressDeleteInput>(request)))
|
||||
})
|
||||
@@ -2,6 +2,13 @@ import type { Context } from '../utils/handler.js'
|
||||
import { guardMethod } from '../utils/method.js'
|
||||
import { readJson } from '../utils/request.js'
|
||||
import { withErrorBoundary } from '../utils/handler.js'
|
||||
import {
|
||||
afterSaleService,
|
||||
type AfterSaleApplyInput,
|
||||
type AfterSaleAuditInput,
|
||||
type AfterSaleListInput,
|
||||
type AfterSaleLogisticsInput
|
||||
} from '../services/after-sale.service.js'
|
||||
import { orderActionService } from '../services/order-action.service.js'
|
||||
import { orderCreateService, type CheckoutGoods } from '../services/order-create.service.js'
|
||||
import { orderListService } from '../services/order-list.service.js'
|
||||
@@ -12,6 +19,8 @@ import { toHttpResponse } from './controller-response.js'
|
||||
type OrderListRequest = {
|
||||
supplierId?: string
|
||||
userId?: string
|
||||
orderId?: string
|
||||
mainOrderId?: string
|
||||
}
|
||||
|
||||
type OrderActionRequest = {
|
||||
@@ -19,6 +28,9 @@ type OrderActionRequest = {
|
||||
userId?: string
|
||||
orderId?: string
|
||||
mainOrderId?: string
|
||||
deliveryStatus?: number
|
||||
logisticsOrderId?: string
|
||||
crrgBsnNm?: string
|
||||
amount?: number
|
||||
totalPrice?: number
|
||||
wPayOrAPay?: number
|
||||
@@ -31,6 +43,8 @@ type OrderCreateRequest = {
|
||||
orderNo?: string
|
||||
orderNumberId?: string
|
||||
amount?: number
|
||||
addressId?: string
|
||||
remark?: string
|
||||
goods?: CheckoutGoods[]
|
||||
}
|
||||
|
||||
@@ -47,6 +61,58 @@ export const orderListController = withErrorBoundary(async (request: Request, _c
|
||||
return toHttpResponse(await orderListService.listOrders(supplierId, userId))
|
||||
})
|
||||
|
||||
export const orderCountController = withErrorBoundary(async (request: Request, _context: Context) => {
|
||||
const methodResponse = guardMethod(request, ['POST'])
|
||||
if (methodResponse) return methodResponse
|
||||
|
||||
const body = await readJson<OrderListRequest>(request)
|
||||
const supplierId = String(body.supplierId || '').trim()
|
||||
const userId = String(body.userId || '').trim()
|
||||
|
||||
return toHttpResponse(await orderListService.countOrders(supplierId, userId))
|
||||
})
|
||||
|
||||
export const orderDetailController = withErrorBoundary(async (request: Request, _context: Context) => {
|
||||
const methodResponse = guardMethod(request, ['POST'])
|
||||
if (methodResponse) return methodResponse
|
||||
|
||||
const body = await readJson<OrderListRequest>(request)
|
||||
const supplierId = String(body.supplierId || '').trim()
|
||||
const userId = String(body.userId || '').trim()
|
||||
const orderId = String(body.orderId || '').trim()
|
||||
const mainOrderId = String(body.mainOrderId || '').trim()
|
||||
|
||||
return toHttpResponse(await orderListService.detailOrder(supplierId, userId, orderId, mainOrderId))
|
||||
})
|
||||
|
||||
export const afterSaleApplyController = withErrorBoundary(async (request: Request, _context: Context) => {
|
||||
const methodResponse = guardMethod(request, ['POST'])
|
||||
if (methodResponse) return methodResponse
|
||||
|
||||
return toHttpResponse(await afterSaleService.apply(await readJson<AfterSaleApplyInput>(request)))
|
||||
})
|
||||
|
||||
export const afterSaleListController = withErrorBoundary(async (request: Request, _context: Context) => {
|
||||
const methodResponse = guardMethod(request, ['POST'])
|
||||
if (methodResponse) return methodResponse
|
||||
|
||||
return toHttpResponse(await afterSaleService.list(await readJson<AfterSaleListInput>(request)))
|
||||
})
|
||||
|
||||
export const afterSaleAuditController = withErrorBoundary(async (request: Request, _context: Context) => {
|
||||
const methodResponse = guardMethod(request, ['POST'])
|
||||
if (methodResponse) return methodResponse
|
||||
|
||||
return toHttpResponse(await afterSaleService.audit(await readJson<AfterSaleAuditInput>(request)))
|
||||
})
|
||||
|
||||
export const afterSaleLogisticsController = withErrorBoundary(async (request: Request, _context: Context) => {
|
||||
const methodResponse = guardMethod(request, ['POST'])
|
||||
if (methodResponse) return methodResponse
|
||||
|
||||
return toHttpResponse(await afterSaleService.updateLogistics(await readJson<AfterSaleLogisticsInput>(request)))
|
||||
})
|
||||
|
||||
export const orderCreateController = withErrorBoundary(async (request: Request, _context: Context) => {
|
||||
const methodResponse = guardMethod(request, ['POST'])
|
||||
if (methodResponse) return methodResponse
|
||||
@@ -59,6 +125,8 @@ export const orderCreateController = withErrorBoundary(async (request: Request,
|
||||
orderNo: String(body.orderNo || '').trim(),
|
||||
orderNumberId: body.orderNumberId,
|
||||
amount: body.amount,
|
||||
addressId: String(body.addressId || '').trim(),
|
||||
remark: String(body.remark || '').trim(),
|
||||
goods: Array.isArray(body.goods) ? body.goods : []
|
||||
}))
|
||||
})
|
||||
@@ -121,6 +189,22 @@ export const orderPayController = withErrorBoundary(async (request: Request, _co
|
||||
}))
|
||||
})
|
||||
|
||||
export const orderShipController = withErrorBoundary(async (request: Request, _context: Context) => {
|
||||
const methodResponse = guardMethod(request, ['POST'])
|
||||
if (methodResponse) return methodResponse
|
||||
|
||||
const body = await readJson<OrderActionRequest>(request)
|
||||
|
||||
return toHttpResponse(await orderActionService.shipOrder({
|
||||
supplierId: String(body.supplierId || '').trim(),
|
||||
userId: String(body.userId || '').trim(),
|
||||
orderId: String(body.orderId || body.mainOrderId || '').trim(),
|
||||
deliveryStatus: Number(body.deliveryStatus || 1),
|
||||
logisticsOrderId: String(body.logisticsOrderId || '').trim(),
|
||||
crrgBsnNm: String(body.crrgBsnNm || '').trim()
|
||||
}))
|
||||
})
|
||||
|
||||
export const orderPushEventController = withErrorBoundary(async (request: Request, _context: Context) => {
|
||||
const methodResponse = guardMethod(request, ['POST'])
|
||||
if (methodResponse) return methodResponse
|
||||
|
||||
@@ -37,6 +37,32 @@ export const orderDao = {
|
||||
})
|
||||
},
|
||||
|
||||
findMainOrderWithSubs(supplierId: string, userId: string, mainOrderId: string) {
|
||||
return prisma.supplierOrderMain.findFirst({
|
||||
where: {
|
||||
orderId: mainOrderId,
|
||||
supplierId,
|
||||
userId,
|
||||
deleted: 0
|
||||
},
|
||||
include: {
|
||||
subOrders: {
|
||||
where: { deleted: 0 },
|
||||
include: {
|
||||
products: {
|
||||
where: { deleted: 0 },
|
||||
orderBy: { createAt: 'asc' }
|
||||
}
|
||||
},
|
||||
orderBy: [
|
||||
{ createTime: 'desc' },
|
||||
{ createAt: 'desc' }
|
||||
]
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
findMainOrder(supplierId: string, userId: string, mainOrderId: string) {
|
||||
return prisma.supplierOrderMain.findFirst({
|
||||
where: {
|
||||
@@ -59,6 +85,23 @@ export const orderDao = {
|
||||
})
|
||||
},
|
||||
|
||||
findSubOrderWithProducts(supplierId: string, userId: string, orderId: string) {
|
||||
return prisma.supplierOrderSub.findFirst({
|
||||
where: {
|
||||
orderId,
|
||||
supplierId,
|
||||
userId,
|
||||
deleted: 0
|
||||
},
|
||||
include: {
|
||||
products: {
|
||||
where: { deleted: 0 },
|
||||
orderBy: { createAt: 'asc' }
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
findPayingMainOrder(orderId: string) {
|
||||
return prisma.supplierOrderMain.findUnique({
|
||||
where: { orderId }
|
||||
@@ -94,6 +137,71 @@ export const orderDao = {
|
||||
})
|
||||
},
|
||||
|
||||
findUserAddresses(supplierId: string, userId: string) {
|
||||
return prisma.supplierUserAddress.findMany({
|
||||
where: { supplierId, userId, deleted: 0 },
|
||||
orderBy: [
|
||||
{ isDefault: 'desc' },
|
||||
{ updateTime: 'desc' }
|
||||
]
|
||||
})
|
||||
},
|
||||
|
||||
async findDefaultUserAddress(supplierId: string, userId: string) {
|
||||
const addresses = await prisma.supplierUserAddress.findMany({
|
||||
where: { supplierId, userId, deleted: 0 },
|
||||
orderBy: [
|
||||
{ isDefault: 'desc' },
|
||||
{ updateTime: 'desc' }
|
||||
],
|
||||
take: 2
|
||||
})
|
||||
|
||||
return addresses[0] || null
|
||||
},
|
||||
|
||||
findUserAddress(supplierId: string, userId: string, addressId: string) {
|
||||
return prisma.supplierUserAddress.findFirst({
|
||||
where: { supplierId, userId, addressId, deleted: 0 }
|
||||
})
|
||||
},
|
||||
|
||||
findUserAddressById(addressId: string) {
|
||||
return prisma.supplierUserAddress.findFirst({
|
||||
where: { addressId, deleted: 0 }
|
||||
})
|
||||
},
|
||||
|
||||
countMainOrders(where: Prisma.SupplierOrderMainWhereInput) {
|
||||
return prisma.supplierOrderMain.count({ where })
|
||||
},
|
||||
|
||||
groupSubOrdersByState(where: Prisma.SupplierOrderSubWhereInput) {
|
||||
return prisma.supplierOrderSub.groupBy({
|
||||
by: ['orderState'],
|
||||
where,
|
||||
_count: { _all: true }
|
||||
})
|
||||
},
|
||||
|
||||
findAfterSale(where: Prisma.SupplierAfterSaleWhereInput) {
|
||||
return prisma.supplierAfterSale.findFirst({ where })
|
||||
},
|
||||
|
||||
findAfterSales(where: Prisma.SupplierAfterSaleWhereInput) {
|
||||
return prisma.supplierAfterSale.findMany({
|
||||
where,
|
||||
orderBy: [
|
||||
{ createTime: 'desc' },
|
||||
{ id: 'desc' }
|
||||
]
|
||||
})
|
||||
},
|
||||
|
||||
countAfterSales(where: Prisma.SupplierAfterSaleWhereInput) {
|
||||
return prisma.supplierAfterSale.count({ where })
|
||||
},
|
||||
|
||||
updatePushLog(id: number, data: Parameters<typeof prisma.supplierOrderPushLog.update>[0]['data']) {
|
||||
return prisma.supplierOrderPushLog.update({
|
||||
where: { id },
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export { addressDefaultController as default } from '../controllers/address.controller.js'
|
||||
@@ -0,0 +1 @@
|
||||
export { addressDeleteController as default } from '../controllers/address.controller.js'
|
||||
@@ -0,0 +1 @@
|
||||
export { addressListController as default } from '../controllers/address.controller.js'
|
||||
@@ -0,0 +1 @@
|
||||
export { addressSaveController as default } from '../controllers/address.controller.js'
|
||||
@@ -0,0 +1 @@
|
||||
export { afterSaleApplyController as default } from '../controllers/order.controller.js'
|
||||
@@ -0,0 +1 @@
|
||||
export { afterSaleAuditController as default } from '../controllers/order.controller.js'
|
||||
@@ -0,0 +1 @@
|
||||
export { afterSaleListController as default } from '../controllers/order.controller.js'
|
||||
@@ -0,0 +1 @@
|
||||
export { afterSaleLogisticsController as default } from '../controllers/order.controller.js'
|
||||
@@ -0,0 +1 @@
|
||||
export { orderDetailController as default } from '../controllers/order.controller.js'
|
||||
@@ -4,6 +4,12 @@ import Router from '@koa/router'
|
||||
import bodyParser from 'koa-bodyparser'
|
||||
import type { FunctionHandler } from './utils/handler.js'
|
||||
import { logger } from './utils/logger.js'
|
||||
import {
|
||||
addressDefaultController,
|
||||
addressDeleteController,
|
||||
addressListController,
|
||||
addressSaveController
|
||||
} from './controllers/address.controller.js'
|
||||
import {
|
||||
cartAddController,
|
||||
loginController,
|
||||
@@ -11,13 +17,20 @@ import {
|
||||
userInfoController
|
||||
} from './controllers/basic.controller.js'
|
||||
import {
|
||||
afterSaleApplyController,
|
||||
afterSaleAuditController,
|
||||
afterSaleListController,
|
||||
afterSaleLogisticsController,
|
||||
orderCancelMainController,
|
||||
orderCancelPaidMainController,
|
||||
orderCancelSubController,
|
||||
orderCountController,
|
||||
orderCreateController,
|
||||
orderDetailController,
|
||||
orderListController,
|
||||
orderPayController,
|
||||
orderPushEventController,
|
||||
orderShipController,
|
||||
supplierOrderInfoController
|
||||
} from './controllers/order.controller.js'
|
||||
import { productDetailController, productListController } from './controllers/product.controller.js'
|
||||
@@ -33,15 +46,26 @@ const router = new Router()
|
||||
const port = Number(process.env.PORT || 8888)
|
||||
|
||||
const handlers: Record<string, FunctionHandler> = {
|
||||
'address-default': addressDefaultController,
|
||||
'address-delete': addressDeleteController,
|
||||
'address-list': addressListController,
|
||||
'address-save': addressSaveController,
|
||||
'after-sale-apply': afterSaleApplyController,
|
||||
'after-sale-audit': afterSaleAuditController,
|
||||
'after-sale-list': afterSaleListController,
|
||||
'after-sale-logistics': afterSaleLogisticsController,
|
||||
'cart-add': cartAddController,
|
||||
login: loginController,
|
||||
'order-cancel-main': orderCancelMainController,
|
||||
'order-cancel-paid-main': orderCancelPaidMainController,
|
||||
'order-cancel-sub': orderCancelSubController,
|
||||
'order-count': orderCountController,
|
||||
'order-create': orderCreateController,
|
||||
'order-detail': orderDetailController,
|
||||
'order-list': orderListController,
|
||||
'order-pay': orderPayController,
|
||||
'order-push-event': orderPushEventController,
|
||||
'order-ship': orderShipController,
|
||||
'product-detail': productDetailController,
|
||||
'product-list': productListController,
|
||||
'supplier-debug-tokens': supplierDebugTokensController,
|
||||
|
||||
@@ -0,0 +1,183 @@
|
||||
import { orderDao } from '../dao/order.dao.js'
|
||||
import { serviceFail, serviceOk } from '../types/service-result.js'
|
||||
|
||||
export type AddressListInput = {
|
||||
supplierId?: string
|
||||
userId?: string
|
||||
}
|
||||
|
||||
export type AddressDefaultInput = AddressListInput
|
||||
|
||||
export type AddressSaveInput = AddressListInput & {
|
||||
id?: string
|
||||
addressId?: string
|
||||
name?: string
|
||||
tel?: string
|
||||
province?: string
|
||||
city?: string
|
||||
county?: string
|
||||
addressDetail?: string
|
||||
areaCode?: string
|
||||
postalCode?: string
|
||||
isDefault?: boolean
|
||||
}
|
||||
|
||||
export type AddressDeleteInput = AddressListInput & {
|
||||
id?: string
|
||||
addressId?: string
|
||||
}
|
||||
|
||||
const createAddressId = () => `addr-${Date.now()}`
|
||||
|
||||
const toAddressView = (address: {
|
||||
addressId: string
|
||||
name: string
|
||||
tel: string
|
||||
province: string | null
|
||||
city: string | null
|
||||
county: string | null
|
||||
addressDetail: string | null
|
||||
areaCode: string | null
|
||||
postalCode: string | null
|
||||
isDefault: boolean
|
||||
}) => ({
|
||||
id: address.addressId,
|
||||
addressId: address.addressId,
|
||||
name: address.name,
|
||||
tel: address.tel,
|
||||
province: address.province || '',
|
||||
city: address.city || '',
|
||||
county: address.county || '',
|
||||
addressDetail: address.addressDetail || '',
|
||||
areaCode: address.areaCode || '',
|
||||
postalCode: address.postalCode || '',
|
||||
isDefault: address.isDefault
|
||||
})
|
||||
|
||||
export const addressService = {
|
||||
async list(input: AddressListInput) {
|
||||
const supplierId = String(input.supplierId || '').trim()
|
||||
const userId = String(input.userId || '').trim()
|
||||
|
||||
if (!supplierId || !userId) {
|
||||
return serviceFail('供应商或用户信息不能为空')
|
||||
}
|
||||
|
||||
const addresses = await orderDao.findUserAddresses(supplierId, userId)
|
||||
return serviceOk({ addresses: addresses.map(toAddressView) })
|
||||
},
|
||||
|
||||
async default(input: AddressDefaultInput) {
|
||||
const supplierId = String(input.supplierId || '').trim()
|
||||
const userId = String(input.userId || '').trim()
|
||||
|
||||
if (!supplierId || !userId) {
|
||||
return serviceFail('渚涘簲鍟嗘垨鐢ㄦ埛淇℃伅涓嶈兘涓虹┖')
|
||||
}
|
||||
|
||||
const address = await orderDao.findDefaultUserAddress(supplierId, userId)
|
||||
return serviceOk({ address: address ? toAddressView(address) : null })
|
||||
},
|
||||
|
||||
async save(input: AddressSaveInput) {
|
||||
const supplierId = String(input.supplierId || '').trim()
|
||||
const userId = String(input.userId || '').trim()
|
||||
const addressId = String(input.addressId || input.id || createAddressId()).trim()
|
||||
const name = String(input.name || '').trim()
|
||||
const tel = String(input.tel || '').trim()
|
||||
const isDefault = Boolean(input.isDefault)
|
||||
|
||||
if (!supplierId || !userId || !addressId || !name || !tel) {
|
||||
return serviceFail('供应商、用户、收货人或手机号不能为空')
|
||||
}
|
||||
|
||||
const existingAddress = await orderDao.findUserAddressById(addressId)
|
||||
if (existingAddress && (existingAddress.supplierId !== supplierId || existingAddress.userId !== userId)) {
|
||||
return serviceFail('地址不存在或无权操作', 404, 'ADDRESS_NOT_FOUND')
|
||||
}
|
||||
|
||||
await orderDao.transaction(async tx => {
|
||||
const count = await tx.supplierUserAddress.count({
|
||||
where: { supplierId, userId, deleted: 0 }
|
||||
})
|
||||
const nextIsDefault = isDefault || count === 0
|
||||
|
||||
if (nextIsDefault) {
|
||||
await tx.supplierUserAddress.updateMany({
|
||||
where: { supplierId, userId, deleted: 0, addressId: { not: addressId } },
|
||||
data: { isDefault: false }
|
||||
})
|
||||
}
|
||||
|
||||
await tx.supplierUserAddress.upsert({
|
||||
where: { addressId },
|
||||
update: {
|
||||
supplierId,
|
||||
userId,
|
||||
name,
|
||||
tel,
|
||||
province: input.province || null,
|
||||
city: input.city || null,
|
||||
county: input.county || null,
|
||||
addressDetail: input.addressDetail || null,
|
||||
areaCode: input.areaCode || null,
|
||||
postalCode: input.postalCode || null,
|
||||
isDefault: nextIsDefault,
|
||||
deleted: 0
|
||||
},
|
||||
create: {
|
||||
addressId,
|
||||
supplierId,
|
||||
userId,
|
||||
name,
|
||||
tel,
|
||||
province: input.province || null,
|
||||
city: input.city || null,
|
||||
county: input.county || null,
|
||||
addressDetail: input.addressDetail || null,
|
||||
areaCode: input.areaCode || null,
|
||||
postalCode: input.postalCode || null,
|
||||
isDefault: nextIsDefault
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
const address = await orderDao.findUserAddress(supplierId, userId, addressId)
|
||||
return serviceOk({ address: address ? toAddressView(address) : null })
|
||||
},
|
||||
|
||||
async delete(input: AddressDeleteInput) {
|
||||
const supplierId = String(input.supplierId || '').trim()
|
||||
const userId = String(input.userId || '').trim()
|
||||
const addressId = String(input.addressId || input.id || '').trim()
|
||||
|
||||
if (!supplierId || !userId || !addressId) {
|
||||
return serviceFail('供应商、用户或地址不能为空')
|
||||
}
|
||||
|
||||
await orderDao.transaction(async tx => {
|
||||
await tx.supplierUserAddress.updateMany({
|
||||
where: { supplierId, userId, addressId, deleted: 0 },
|
||||
data: { deleted: 1, isDefault: false }
|
||||
})
|
||||
|
||||
const defaultCount = await tx.supplierUserAddress.count({
|
||||
where: { supplierId, userId, deleted: 0, isDefault: true }
|
||||
})
|
||||
if (defaultCount === 0) {
|
||||
const firstAddress = await tx.supplierUserAddress.findFirst({
|
||||
where: { supplierId, userId, deleted: 0 },
|
||||
orderBy: { updateTime: 'desc' }
|
||||
})
|
||||
if (firstAddress) {
|
||||
await tx.supplierUserAddress.update({
|
||||
where: { addressId: firstAddress.addressId },
|
||||
data: { isDefault: true }
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return serviceOk({ addressId })
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,344 @@
|
||||
import {
|
||||
AFTER_SALE_LOGISTICS_STATE,
|
||||
AFTER_SALE_SERVICE_STATE,
|
||||
ORDER_EVENT_TYPE,
|
||||
ORDER_STATE
|
||||
} from '../constants/order.js'
|
||||
import { orderDao } from '../dao/order.dao.js'
|
||||
import { logger } from '../utils/logger.js'
|
||||
import { serviceFail, serviceOk } from '../types/service-result.js'
|
||||
|
||||
export type AfterSaleApplyInput = {
|
||||
supplierId?: string
|
||||
userId?: string
|
||||
mainOrderId?: string
|
||||
subOrderId?: string
|
||||
orderId?: string
|
||||
serviceType?: number
|
||||
amount?: number
|
||||
reason?: string
|
||||
}
|
||||
|
||||
export type AfterSaleListInput = {
|
||||
supplierId?: string
|
||||
userId?: string
|
||||
}
|
||||
|
||||
export type AfterSaleAuditInput = {
|
||||
supplierId?: string
|
||||
serviceOrderId?: string
|
||||
auditStatus?: number
|
||||
auditRemark?: string
|
||||
}
|
||||
|
||||
export type AfterSaleLogisticsInput = {
|
||||
supplierId?: string
|
||||
serviceOrderId?: string
|
||||
logisticsState?: number
|
||||
userLogisticsNo?: string
|
||||
userLogisticsCompany?: string
|
||||
merchantLogisticsNo?: string
|
||||
merchantLogisticsCompany?: string
|
||||
}
|
||||
|
||||
const activeAfterSaleStates = [
|
||||
AFTER_SALE_SERVICE_STATE.PENDING_AUDIT,
|
||||
AFTER_SALE_SERVICE_STATE.APPROVED,
|
||||
AFTER_SALE_SERVICE_STATE.USER_SHIPPED,
|
||||
AFTER_SALE_SERVICE_STATE.RECEIVED
|
||||
]
|
||||
const supportedServiceTypes = new Set<number>([
|
||||
ORDER_EVENT_TYPE.RETURN_ORDER,
|
||||
ORDER_EVENT_TYPE.EXCHANGE_ORDER
|
||||
])
|
||||
|
||||
const afterSaleOrderStates = new Set<number>([
|
||||
ORDER_STATE.FINISH_PAY,
|
||||
ORDER_STATE.IN_PROGRESS,
|
||||
ORDER_STATE.COMPLETED
|
||||
])
|
||||
|
||||
const serviceTypeText = (serviceType: number) => {
|
||||
if (serviceType === ORDER_EVENT_TYPE.RETURN_ORDER) return '退货'
|
||||
if (serviceType === ORDER_EVENT_TYPE.EXCHANGE_ORDER) return '换货'
|
||||
return '售后'
|
||||
}
|
||||
|
||||
const serviceStateText = (serviceState: number) => {
|
||||
if (serviceState === AFTER_SALE_SERVICE_STATE.PENDING_AUDIT) return '商家待审核'
|
||||
if (serviceState === AFTER_SALE_SERVICE_STATE.APPROVED) return '审核通过'
|
||||
if (serviceState === AFTER_SALE_SERVICE_STATE.REJECTED) return '审核拒绝'
|
||||
if (serviceState === AFTER_SALE_SERVICE_STATE.USER_SHIPPED) return '用户已寄回'
|
||||
if (serviceState === AFTER_SALE_SERVICE_STATE.RECEIVED) return '商家已收货'
|
||||
if (serviceState === AFTER_SALE_SERVICE_STATE.FINISHED) return '已完成'
|
||||
if (serviceState === AFTER_SALE_SERVICE_STATE.CANCELLED) return '已取消'
|
||||
return '售后处理中'
|
||||
}
|
||||
|
||||
const logisticsStateText = (logisticsState: number) => {
|
||||
if (logisticsState === AFTER_SALE_LOGISTICS_STATE.WAIT_USER_SHIP) return '待用户寄回'
|
||||
if (logisticsState === AFTER_SALE_LOGISTICS_STATE.USER_SHIPPED) return '用户已寄回'
|
||||
if (logisticsState === AFTER_SALE_LOGISTICS_STATE.MERCHANT_RECEIVED) return '商家已收货'
|
||||
if (logisticsState === AFTER_SALE_LOGISTICS_STATE.MERCHANT_SHIPPED) return '商家已发货'
|
||||
if (logisticsState === AFTER_SALE_LOGISTICS_STATE.USER_RECEIVED) return '用户已收货'
|
||||
return '暂无物流'
|
||||
}
|
||||
|
||||
const toYuanText = (value: { toFixed: (digits: number) => string } | number | string | null | undefined) => {
|
||||
if (value && typeof value === 'object' && 'toFixed' in value) {
|
||||
return value.toFixed(2)
|
||||
}
|
||||
|
||||
return Number(value || 0).toFixed(2)
|
||||
}
|
||||
|
||||
const firstImage = (picInfo: unknown) => {
|
||||
if (Array.isArray(picInfo)) {
|
||||
return String(picInfo[0] || '')
|
||||
}
|
||||
|
||||
return ''
|
||||
}
|
||||
|
||||
const toDateTimeText = (date?: Date | null) => {
|
||||
if (!date) return ''
|
||||
const pad = (value: number) => String(value).padStart(2, '0')
|
||||
|
||||
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`
|
||||
}
|
||||
|
||||
const createServiceOrderId = (subOrderId: string, serviceType: number) => {
|
||||
return `AS${serviceType}_${subOrderId}_${Date.now()}`
|
||||
}
|
||||
|
||||
export const afterSaleService = {
|
||||
async apply(input: AfterSaleApplyInput) {
|
||||
const supplierId = String(input.supplierId || '').trim()
|
||||
const userId = String(input.userId || '').trim()
|
||||
const subOrderId = String(input.subOrderId || input.orderId || '').trim()
|
||||
const serviceType = Number(input.serviceType)
|
||||
const reason = String(input.reason || '').trim()
|
||||
logger.info('after sale apply', { supplierId, userId, subOrderId, serviceType })
|
||||
|
||||
if (!supplierId || !userId || !subOrderId || !serviceType) {
|
||||
return serviceFail('供应商、用户、订单或售后类型不能为空')
|
||||
}
|
||||
|
||||
if (!supportedServiceTypes.has(serviceType)) {
|
||||
return serviceFail('售后类型只支持退货或换货', 400, 'AFTER_SALE_TYPE_NOT_SUPPORTED')
|
||||
}
|
||||
|
||||
const subOrder = await orderDao.findSubOrderWithProducts(supplierId, userId, subOrderId)
|
||||
if (!subOrder) {
|
||||
return serviceFail('子订单不存在或已取消', 404, 'SUB_ORDER_NOT_FOUND')
|
||||
}
|
||||
|
||||
if (!afterSaleOrderStates.has(subOrder.orderState)) {
|
||||
return serviceFail('当前订单状态不能申请退款/售后', 400, 'ORDER_NOT_ALLOW_AFTER_SALE')
|
||||
}
|
||||
|
||||
const activeAfterSale = await orderDao.findAfterSale({
|
||||
supplierId,
|
||||
userId,
|
||||
subOrderId,
|
||||
deleted: 0,
|
||||
serviceState: { in: activeAfterSaleStates }
|
||||
})
|
||||
if (activeAfterSale) {
|
||||
return serviceFail('该订单已有处理中的退款/售后申请', 400, 'AFTER_SALE_ALREADY_EXISTS')
|
||||
}
|
||||
|
||||
const now = new Date()
|
||||
const amount = Number(input.amount || subOrder.totalPrice || 0)
|
||||
const serviceOrderId = createServiceOrderId(subOrderId, serviceType)
|
||||
|
||||
await orderDao.transaction(async tx => {
|
||||
await tx.supplierAfterSale.create({
|
||||
data: {
|
||||
serviceOrderId,
|
||||
supplierId,
|
||||
userId,
|
||||
mainOrderId: input.mainOrderId || subOrder.mainOrderId,
|
||||
subOrderId,
|
||||
serviceType,
|
||||
serviceState: AFTER_SALE_SERVICE_STATE.PENDING_AUDIT,
|
||||
auditStatus: 0,
|
||||
logisticsState: AFTER_SALE_LOGISTICS_STATE.NONE,
|
||||
amount,
|
||||
reason: reason || null,
|
||||
applyTime: now,
|
||||
requestBody: input
|
||||
}
|
||||
})
|
||||
await tx.supplierOrderOperateLog.create({
|
||||
data: {
|
||||
supplierId,
|
||||
userId,
|
||||
mainOrderId: subOrder.mainOrderId,
|
||||
subOrderId,
|
||||
operateType: serviceType,
|
||||
beforeState: subOrder.orderState,
|
||||
afterState: subOrder.orderState,
|
||||
remark: reason || serviceTypeText(serviceType),
|
||||
operator: 'user'
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
return serviceOk({
|
||||
serviceOrderId,
|
||||
supplierId,
|
||||
userId,
|
||||
mainOrderId: input.mainOrderId || subOrder.mainOrderId,
|
||||
subOrderId,
|
||||
serviceType,
|
||||
serviceTypeText: serviceTypeText(serviceType),
|
||||
serviceState: AFTER_SALE_SERVICE_STATE.PENDING_AUDIT,
|
||||
serviceStateText: serviceStateText(AFTER_SALE_SERVICE_STATE.PENDING_AUDIT),
|
||||
logisticsState: AFTER_SALE_LOGISTICS_STATE.NONE,
|
||||
logisticsStateText: logisticsStateText(AFTER_SALE_LOGISTICS_STATE.NONE)
|
||||
})
|
||||
},
|
||||
|
||||
async list(input: AfterSaleListInput) {
|
||||
const supplierId = String(input.supplierId || '').trim()
|
||||
const userId = String(input.userId || '').trim()
|
||||
logger.info('after sale list query', { supplierId, userId })
|
||||
|
||||
if (!supplierId || !userId) {
|
||||
return serviceFail('供应商或用户信息不能为空')
|
||||
}
|
||||
|
||||
const records = await orderDao.findAfterSales({
|
||||
supplierId,
|
||||
userId,
|
||||
deleted: 0,
|
||||
serviceType: { in: [ORDER_EVENT_TYPE.RETURN_ORDER, ORDER_EVENT_TYPE.EXCHANGE_ORDER] }
|
||||
})
|
||||
const subOrderIds = records.map(item => item.subOrderId).filter(Boolean) as string[]
|
||||
const subOrders = subOrderIds.length
|
||||
? await orderDao.findOpenApiSubOrders(supplierId, subOrderIds)
|
||||
: []
|
||||
const subOrderMap = new Map(subOrders.map(item => [item.orderId, item]))
|
||||
const afterSaleItems = records.map(record => {
|
||||
const subOrder = record.subOrderId ? subOrderMap.get(record.subOrderId) : null
|
||||
const products = subOrder?.products.map(product => ({
|
||||
id: product.id,
|
||||
sku: product.sku,
|
||||
name: product.name,
|
||||
number: product.number,
|
||||
price: toYuanText(product.price),
|
||||
image: product.productImgUrl || firstImage(product.picInfo)
|
||||
})) || []
|
||||
|
||||
return {
|
||||
serviceOrderId: record.serviceOrderId,
|
||||
supplierId: record.supplierId,
|
||||
userId: record.userId,
|
||||
mainOrderId: record.mainOrderId,
|
||||
subOrderId: record.subOrderId,
|
||||
orderId: record.subOrderId,
|
||||
orderNumberId: subOrder?.orderNumberId || '',
|
||||
serviceType: record.serviceType,
|
||||
serviceTypeText: serviceTypeText(record.serviceType),
|
||||
serviceState: record.serviceState,
|
||||
serviceStateText: serviceStateText(record.serviceState),
|
||||
auditStatus: record.auditStatus,
|
||||
logisticsState: record.logisticsState,
|
||||
logisticsStateText: logisticsStateText(record.logisticsState),
|
||||
amount: toYuanText(record.amount),
|
||||
reason: record.reason || '',
|
||||
auditRemark: record.auditRemark || '',
|
||||
userLogisticsNo: record.userLogisticsNo || '',
|
||||
userLogisticsCompany: record.userLogisticsCompany || '',
|
||||
merchantLogisticsNo: record.merchantLogisticsNo || '',
|
||||
merchantLogisticsCompany: record.merchantLogisticsCompany || '',
|
||||
applyTime: toDateTimeText(record.applyTime || record.createTime),
|
||||
auditTime: toDateTimeText(record.auditTime),
|
||||
userShipTime: toDateTimeText(record.userShipTime),
|
||||
merchantReceiveTime: toDateTimeText(record.merchantReceiveTime),
|
||||
merchantShipTime: toDateTimeText(record.merchantShipTime),
|
||||
finishTime: toDateTimeText(record.finishTime),
|
||||
products
|
||||
}
|
||||
})
|
||||
|
||||
return serviceOk({ afterSaleItems })
|
||||
},
|
||||
|
||||
async audit(input: AfterSaleAuditInput) {
|
||||
const supplierId = String(input.supplierId || '').trim()
|
||||
const serviceOrderId = String(input.serviceOrderId || '').trim()
|
||||
const auditStatus = Number(input.auditStatus)
|
||||
const auditRemark = String(input.auditRemark || '').trim()
|
||||
|
||||
if (!supplierId || !serviceOrderId || ![1, 2].includes(auditStatus)) {
|
||||
return serviceFail('供应商、售后单或审核状态不能为空')
|
||||
}
|
||||
|
||||
const record = await orderDao.findAfterSale({ supplierId, serviceOrderId, deleted: 0 })
|
||||
if (!record) {
|
||||
return serviceFail('退款/售后申请不存在', 404, 'AFTER_SALE_NOT_FOUND')
|
||||
}
|
||||
|
||||
const approved = auditStatus === 1
|
||||
const now = new Date()
|
||||
await orderDao.transaction(async tx => {
|
||||
await tx.supplierAfterSale.update({
|
||||
where: { serviceOrderId },
|
||||
data: {
|
||||
auditStatus,
|
||||
auditRemark: auditRemark || null,
|
||||
auditTime: now,
|
||||
serviceState: approved ? AFTER_SALE_SERVICE_STATE.APPROVED : AFTER_SALE_SERVICE_STATE.REJECTED,
|
||||
logisticsState: approved ? AFTER_SALE_LOGISTICS_STATE.WAIT_USER_SHIP : AFTER_SALE_LOGISTICS_STATE.NONE
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
return serviceOk({ serviceOrderId, auditStatus, serviceState: approved ? AFTER_SALE_SERVICE_STATE.APPROVED : AFTER_SALE_SERVICE_STATE.REJECTED })
|
||||
},
|
||||
|
||||
async updateLogistics(input: AfterSaleLogisticsInput) {
|
||||
const supplierId = String(input.supplierId || '').trim()
|
||||
const serviceOrderId = String(input.serviceOrderId || '').trim()
|
||||
const logisticsState = Number(input.logisticsState)
|
||||
|
||||
if (!supplierId || !serviceOrderId || !logisticsState) {
|
||||
return serviceFail('供应商、售后单或物流状态不能为空')
|
||||
}
|
||||
|
||||
const record = await orderDao.findAfterSale({ supplierId, serviceOrderId, deleted: 0 })
|
||||
if (!record) {
|
||||
return serviceFail('退款/售后申请不存在', 404, 'AFTER_SALE_NOT_FOUND')
|
||||
}
|
||||
|
||||
const now = new Date()
|
||||
const finish = logisticsState === AFTER_SALE_LOGISTICS_STATE.USER_RECEIVED ||
|
||||
(record.serviceType === ORDER_EVENT_TYPE.RETURN_ORDER && logisticsState === AFTER_SALE_LOGISTICS_STATE.MERCHANT_RECEIVED)
|
||||
|
||||
await orderDao.transaction(async tx => {
|
||||
await tx.supplierAfterSale.update({
|
||||
where: { serviceOrderId },
|
||||
data: {
|
||||
logisticsState,
|
||||
serviceState: finish ? AFTER_SALE_SERVICE_STATE.FINISHED : logisticsState === AFTER_SALE_LOGISTICS_STATE.USER_SHIPPED
|
||||
? AFTER_SALE_SERVICE_STATE.USER_SHIPPED
|
||||
: logisticsState === AFTER_SALE_LOGISTICS_STATE.MERCHANT_RECEIVED
|
||||
? AFTER_SALE_SERVICE_STATE.RECEIVED
|
||||
: record.serviceState,
|
||||
userLogisticsNo: input.userLogisticsNo || undefined,
|
||||
userLogisticsCompany: input.userLogisticsCompany || undefined,
|
||||
merchantLogisticsNo: input.merchantLogisticsNo || undefined,
|
||||
merchantLogisticsCompany: input.merchantLogisticsCompany || undefined,
|
||||
userShipTime: logisticsState === AFTER_SALE_LOGISTICS_STATE.USER_SHIPPED ? now : undefined,
|
||||
merchantReceiveTime: logisticsState === AFTER_SALE_LOGISTICS_STATE.MERCHANT_RECEIVED ? now : undefined,
|
||||
merchantShipTime: logisticsState === AFTER_SALE_LOGISTICS_STATE.MERCHANT_SHIPPED ? now : undefined,
|
||||
finishTime: finish ? now : undefined
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
return serviceOk({ serviceOrderId, logisticsState, logisticsStateText: logisticsStateText(logisticsState) })
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ORDER_EVENT_TYPE, ORDER_SPLIT_STATE, ORDER_STATE } from '../constants/order.js'
|
||||
import { ORDER_EVENT_TYPE, ORDER_OPERATE_TYPE, ORDER_SPLIT_STATE, ORDER_STATE } from '../constants/order.js'
|
||||
import { orderDao } from '../dao/order.dao.js'
|
||||
import { bridgeFetchWithAuth } from '../utils/bridge-auth.js'
|
||||
import { logger } from '../utils/logger.js'
|
||||
@@ -31,6 +31,20 @@ type PayOrderInput = {
|
||||
pointsPrice: number
|
||||
}
|
||||
|
||||
type ShipOrderInput = {
|
||||
supplierId: string
|
||||
userId: string
|
||||
orderId: string
|
||||
deliveryStatus: number
|
||||
logisticsOrderId: string
|
||||
crrgBsnNm: string
|
||||
}
|
||||
|
||||
const toDateTimeText = (date: Date) => {
|
||||
const pad = (value: number) => String(value).padStart(2, '0')
|
||||
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`
|
||||
}
|
||||
|
||||
export const orderActionService = {
|
||||
async cancelPendingMain(input: CancelMainInput) {
|
||||
const { supplierId, userId, mainOrderId } = input
|
||||
@@ -234,7 +248,7 @@ export const orderActionService = {
|
||||
})
|
||||
await tx.supplierOrderSub.updateMany({
|
||||
where: { mainOrderId: orderId, deleted: 0 },
|
||||
data: { orderState: ORDER_STATE.IN_PROGRESS, updateTime: new Date() }
|
||||
data: { orderState: ORDER_STATE.FINISH_PAY, updateTime: new Date() }
|
||||
})
|
||||
})
|
||||
|
||||
@@ -251,5 +265,129 @@ export const orderActionService = {
|
||||
}
|
||||
|
||||
return serviceOk({ orderId, subOrderIds, totalPrice, wPayOrAPay, pointsPrice })
|
||||
},
|
||||
|
||||
async shipOrder(input: ShipOrderInput) {
|
||||
const { supplierId, userId, orderId, logisticsOrderId, crrgBsnNm } = input
|
||||
const deliveryStatus = 1
|
||||
logger.info('ship order', { supplierId, userId, orderId })
|
||||
|
||||
if (!supplierId || !userId || !orderId) {
|
||||
return serviceFail('供应商、用户或订单信息不能为空')
|
||||
}
|
||||
|
||||
const now = new Date()
|
||||
let affectedSubOrderIds: string[] = []
|
||||
let mainOrderId = ''
|
||||
|
||||
await orderDao.transaction(async tx => {
|
||||
const mainOrder = await tx.supplierOrderMain.findFirst({
|
||||
where: { supplierId, userId, orderId, deleted: 0 }
|
||||
})
|
||||
|
||||
const targetSubOrders = mainOrder
|
||||
? await tx.supplierOrderSub.findMany({
|
||||
where: { supplierId, userId, mainOrderId: orderId, deleted: 0 },
|
||||
orderBy: { createAt: 'asc' }
|
||||
})
|
||||
: await tx.supplierOrderSub.findMany({
|
||||
where: { supplierId, userId, orderId, deleted: 0 },
|
||||
orderBy: { createAt: 'asc' }
|
||||
})
|
||||
|
||||
if (!targetSubOrders.length) {
|
||||
return
|
||||
}
|
||||
|
||||
mainOrderId = mainOrder?.orderId || targetSubOrders[0].mainOrderId
|
||||
const pendingShipSubOrders = targetSubOrders.filter(subOrder => subOrder.orderState === ORDER_STATE.FINISH_PAY)
|
||||
affectedSubOrderIds = pendingShipSubOrders.map(subOrder => subOrder.orderId)
|
||||
|
||||
if (!affectedSubOrderIds.length) {
|
||||
return
|
||||
}
|
||||
|
||||
await tx.supplierOrderSub.updateMany({
|
||||
where: { supplierId, userId, orderId: { in: affectedSubOrderIds }, deleted: 0 },
|
||||
data: {
|
||||
orderState: ORDER_STATE.IN_PROGRESS,
|
||||
deliveryStatus: String(deliveryStatus),
|
||||
logisticsOrderId: logisticsOrderId || undefined,
|
||||
crrgBsnNm: crrgBsnNm || undefined,
|
||||
deliveryTime: now,
|
||||
updateTime: now
|
||||
}
|
||||
})
|
||||
|
||||
await tx.supplierOrderOperateLog.createMany({
|
||||
data: pendingShipSubOrders.map(subOrder => ({
|
||||
supplierId,
|
||||
userId: subOrder.userId,
|
||||
mainOrderId: subOrder.mainOrderId,
|
||||
subOrderId: subOrder.orderId,
|
||||
operateType: ORDER_OPERATE_TYPE.RECEIVE,
|
||||
beforeState: subOrder.orderState,
|
||||
afterState: ORDER_STATE.IN_PROGRESS,
|
||||
operator: 'api'
|
||||
}))
|
||||
})
|
||||
|
||||
const unshippedCount = await tx.supplierOrderSub.count({
|
||||
where: {
|
||||
supplierId,
|
||||
userId,
|
||||
mainOrderId,
|
||||
deleted: 0,
|
||||
orderState: ORDER_STATE.FINISH_PAY
|
||||
}
|
||||
})
|
||||
|
||||
if (unshippedCount === 0) {
|
||||
await tx.supplierOrderMain.update({
|
||||
where: { orderId: mainOrderId },
|
||||
data: {
|
||||
orderState: ORDER_STATE.IN_PROGRESS,
|
||||
deliveryStatus: String(deliveryStatus),
|
||||
logisticsOrderId: logisticsOrderId || undefined,
|
||||
crrgBsnNm: crrgBsnNm || undefined,
|
||||
deliveryTime: now,
|
||||
updateTime: now
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
if (!mainOrderId) {
|
||||
return serviceFail('订单不存在或已取消', 404, 'ORDER_NOT_FOUND')
|
||||
}
|
||||
|
||||
if (!affectedSubOrderIds.length) {
|
||||
return serviceFail('当前订单不是待发货状态', 400, 'ORDER_NOT_PENDING_SHIP')
|
||||
}
|
||||
|
||||
const ship = await bridgeFetchWithAuth<BridgeOrderResponse>(supplierId, '/pointOrder/pushOrderInfoMsg', {
|
||||
orderType: ORDER_EVENT_TYPE.SHIP_ORDER,
|
||||
orderId: affectedSubOrderIds.join(','),
|
||||
mainOrderId,
|
||||
userId,
|
||||
deliveryStatus,
|
||||
deliveryTime: toDateTimeText(now),
|
||||
logisticsOrderId,
|
||||
crrgBsnNm,
|
||||
time: Date.now()
|
||||
})
|
||||
if (!ship.response.ok || ship.payload.resultCode !== 0) {
|
||||
return serviceFail(ship.payload.message || '发货状态推送失败', 502, 'BRIDGE_SHIP_ORDER_FAILED')
|
||||
}
|
||||
|
||||
return serviceOk({
|
||||
supplierId,
|
||||
userId,
|
||||
orderId,
|
||||
mainOrderId,
|
||||
orderState: ORDER_STATE.IN_PROGRESS,
|
||||
deliveryStatus,
|
||||
affectedSubOrderIds
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,6 +23,8 @@ type OrderCreateInput = {
|
||||
orderNo: string
|
||||
orderNumberId?: string
|
||||
amount?: number
|
||||
addressId?: string
|
||||
remark?: string
|
||||
goods: CheckoutGoods[]
|
||||
}
|
||||
|
||||
@@ -34,7 +36,7 @@ type BridgeOrderResponse = {
|
||||
type OrderProductSnapshot = {
|
||||
sku: string
|
||||
number: number
|
||||
price: number
|
||||
price: string
|
||||
name: string
|
||||
taxRate: number
|
||||
picInfo: string[]
|
||||
@@ -58,6 +60,7 @@ const createOrderNumberId = () => {
|
||||
|
||||
const toYuan = (centAmount?: number) => Number((Number(centAmount || 0) / 100).toFixed(2))
|
||||
const toMoney = (value: number) => Number(value.toFixed(2))
|
||||
const toMoneyText = (value: number) => Number(value || 0).toFixed(2)
|
||||
const createSubOrderId = (orderId: string, index: number) => `${orderId}_${String(index + 1).padStart(3, '0')}`
|
||||
const createSubOrderNumberId = (orderNumberId: string, index: number) => `${orderNumberId}_${String(index + 1).padStart(3, '0')}`
|
||||
|
||||
@@ -70,7 +73,7 @@ const toOrderProducts = (goods: CheckoutGoods[]) => goods.map((item, index) => {
|
||||
return {
|
||||
sku: String(sku || item.id || `SKU_${index + 1}`),
|
||||
number: Number(item.count || item.number || 1),
|
||||
price: Number(item.price || 0),
|
||||
price: toMoneyText(Number(item.price || 0)),
|
||||
name: String(item.name || ''),
|
||||
taxRate: Number(item.taxRate || 0.13),
|
||||
picInfo: item.picInfo?.length ? item.picInfo : image ? [image] : [],
|
||||
@@ -88,6 +91,8 @@ export const orderCreateService = {
|
||||
const orderId = input.orderNo
|
||||
const amount = toYuan(input.amount)
|
||||
const goods = Array.isArray(input.goods) ? input.goods : []
|
||||
const addressId = String(input.addressId || '').trim()
|
||||
const remark = String(input.remark || '').trim()
|
||||
const orderProInfos = toOrderProducts(goods)
|
||||
logger.info('create pending order', { supplierId, userId, orderId, goodsCount: goods.length, amount })
|
||||
|
||||
@@ -103,6 +108,11 @@ export const orderCreateService = {
|
||||
return serviceFail('订单商品不能为空')
|
||||
}
|
||||
|
||||
const address = addressId ? await orderDao.findUserAddress(supplierId, userId, addressId) : null
|
||||
if (!address) {
|
||||
return serviceFail('请选择收货地址', 400, 'ADDRESS_NOT_FOUND')
|
||||
}
|
||||
|
||||
const orderNumberId = String(input.orderNumberId || createOrderNumberId())
|
||||
const orderTime = nowText()
|
||||
const now = new Date()
|
||||
@@ -122,6 +132,8 @@ export const orderCreateService = {
|
||||
orderNumberId,
|
||||
supplierId,
|
||||
aplName: '积分商城',
|
||||
addressId,
|
||||
remark,
|
||||
createTime: orderTime,
|
||||
updateTime: orderTime,
|
||||
deliveryTime: null,
|
||||
@@ -161,6 +173,8 @@ export const orderCreateService = {
|
||||
supplierId,
|
||||
userId,
|
||||
aplName: orderInfo.aplName,
|
||||
addressId: orderInfo.addressId,
|
||||
remark: orderInfo.remark,
|
||||
updateTime: now,
|
||||
deliveryStatus: orderInfo.deliveryStatus,
|
||||
freightCost: orderInfo.freightCost,
|
||||
@@ -182,6 +196,8 @@ export const orderCreateService = {
|
||||
supplierId,
|
||||
userId,
|
||||
aplName: orderInfo.aplName,
|
||||
addressId: orderInfo.addressId,
|
||||
remark: orderInfo.remark,
|
||||
createTime: now,
|
||||
updateTime: now,
|
||||
deliveryStatus: orderInfo.deliveryStatus,
|
||||
@@ -206,6 +222,8 @@ export const orderCreateService = {
|
||||
supplierId,
|
||||
userId,
|
||||
aplName: orderInfo.aplName,
|
||||
addressId: orderInfo.addressId,
|
||||
remark: orderInfo.remark,
|
||||
createTime: now,
|
||||
updateTime: now,
|
||||
deliveryStatus: orderInfo.deliveryStatus,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ORDER_SPLIT_STATE, ORDER_STATE } from '../constants/order.js'
|
||||
import { ORDER_EVENT_TYPE, ORDER_SPLIT_STATE, ORDER_STATE } from '../constants/order.js'
|
||||
import { orderDao } from '../dao/order.dao.js'
|
||||
import { logger } from '../utils/logger.js'
|
||||
import { serviceFail, serviceOk } from '../types/service-result.js'
|
||||
@@ -49,7 +49,152 @@ const sumProductsCount = (subOrders: Array<{ products: Array<{ number: number |
|
||||
}, 0)
|
||||
}
|
||||
|
||||
const formatAddress = (address: {
|
||||
name: string
|
||||
tel: string
|
||||
province: string | null
|
||||
city: string | null
|
||||
county: string | null
|
||||
addressDetail: string | null
|
||||
} | null) => {
|
||||
if (!address) return null
|
||||
|
||||
return {
|
||||
name: address.name,
|
||||
tel: address.tel,
|
||||
address: `${address.province || ''}${address.city || ''}${address.county || ''}${address.addressDetail || ''}`
|
||||
}
|
||||
}
|
||||
|
||||
export const orderListService = {
|
||||
async detailOrder(supplierId: string, userId: string, orderId: string, mainOrderId: string) {
|
||||
logger.info('order detail query', { supplierId, userId, orderId, mainOrderId })
|
||||
|
||||
if (!supplierId || !userId || !orderId) {
|
||||
return serviceFail('供应商、用户或订单信息不能为空')
|
||||
}
|
||||
|
||||
const targetSubOrder = await orderDao.findSubOrderWithProducts(supplierId, userId, orderId)
|
||||
const targetMainOrderId = mainOrderId || targetSubOrder?.mainOrderId || orderId
|
||||
const mainOrder = await orderDao.findMainOrderWithSubs(supplierId, userId, targetMainOrderId)
|
||||
|
||||
if (!mainOrder) {
|
||||
return serviceFail('订单不存在或已取消', 404, 'ORDER_NOT_FOUND')
|
||||
}
|
||||
|
||||
const addressId = targetSubOrder?.addressId || mainOrder.addressId || ''
|
||||
const address = addressId ? await orderDao.findUserAddressById(addressId) : null
|
||||
const addressView = formatAddress(address)
|
||||
const mainStatus = statusMap[mainOrder.orderState] || statusMap[ORDER_STATE.FINISH_PAY]
|
||||
const subOrders = mainOrder.subOrders.map(subOrder => {
|
||||
const status = statusMap[subOrder.orderState] || mainStatus
|
||||
const products = subOrder.products.map(product => ({
|
||||
id: product.id,
|
||||
sku: product.sku,
|
||||
name: product.name,
|
||||
number: product.number,
|
||||
price: toYuanText(product.price),
|
||||
image: product.productImgUrl || firstImage(product.picInfo)
|
||||
}))
|
||||
|
||||
return {
|
||||
id: subOrder.orderId,
|
||||
orderId: subOrder.orderId,
|
||||
orderNumberId: subOrder.orderNumberId || '',
|
||||
mainOrderId: subOrder.mainOrderId,
|
||||
supplierId: subOrder.supplierId,
|
||||
addressId: subOrder.addressId || mainOrder.addressId || '',
|
||||
receiverName: addressView?.name || '',
|
||||
receiverMobile: addressView?.tel || '',
|
||||
receiverAddress: addressView?.address || '',
|
||||
remark: subOrder.remark || mainOrder.remark || '',
|
||||
createTime: toDateTimeText(subOrder.createTime || subOrder.createAt),
|
||||
status: status.text,
|
||||
statusClass: status.statusClass,
|
||||
orderState: subOrder.orderState,
|
||||
amount: toYuanText(subOrder.totalPrice),
|
||||
freightCost: toYuanText(subOrder.freightCost),
|
||||
logisticsOrderId: subOrder.logisticsOrderId || mainOrder.logisticsOrderId || '',
|
||||
crrgBsnNm: subOrder.crrgBsnNm || mainOrder.crrgBsnNm || '',
|
||||
deliveryTime: toDateTimeText(subOrder.deliveryTime || mainOrder.deliveryTime),
|
||||
deliveryStatus: subOrder.deliveryStatus || mainOrder.deliveryStatus || '',
|
||||
wPayOrAPay: subOrder.wPayOrAPay,
|
||||
count: products.reduce((total, item) => total + Number(item.number || 0), 0),
|
||||
products
|
||||
}
|
||||
})
|
||||
const subOrder = subOrders.find(item => item.orderId === orderId) || subOrders[0] || null
|
||||
|
||||
return serviceOk({
|
||||
order: {
|
||||
id: mainOrder.orderId,
|
||||
orderId: mainOrder.orderId,
|
||||
mainOrderId: mainOrder.orderId,
|
||||
orderNumberId: mainOrder.orderNumberId || '',
|
||||
supplierId: mainOrder.supplierId,
|
||||
addressId: mainOrder.addressId || '',
|
||||
receiverName: addressView?.name || '',
|
||||
receiverMobile: addressView?.tel || '',
|
||||
receiverAddress: addressView?.address || '',
|
||||
remark: mainOrder.remark || '',
|
||||
createTime: toDateTimeText(mainOrder.createTime || mainOrder.createAt),
|
||||
status: mainStatus.text,
|
||||
statusClass: mainStatus.statusClass,
|
||||
orderState: mainOrder.orderState,
|
||||
amount: toYuanText(mainOrder.totalPrice),
|
||||
freightCost: toYuanText(mainOrder.freightCost),
|
||||
logisticsOrderId: mainOrder.logisticsOrderId || '',
|
||||
crrgBsnNm: mainOrder.crrgBsnNm || '',
|
||||
deliveryTime: toDateTimeText(mainOrder.deliveryTime),
|
||||
deliveryStatus: mainOrder.deliveryStatus || '',
|
||||
wPayOrAPay: mainOrder.wPayOrAPay,
|
||||
count: sumProductsCount(subOrders),
|
||||
subOrders
|
||||
},
|
||||
subOrder
|
||||
})
|
||||
},
|
||||
|
||||
async countOrders(supplierId: string, userId: string) {
|
||||
logger.info('order count query', { supplierId, userId })
|
||||
|
||||
if (!supplierId || !userId) {
|
||||
return serviceFail('供应商或用户信息不能为空')
|
||||
}
|
||||
|
||||
const [pendingPay, subStateCounts, afterSale] = await Promise.all([
|
||||
orderDao.countMainOrders({
|
||||
supplierId,
|
||||
userId,
|
||||
deleted: 0,
|
||||
orderState: ORDER_STATE.PENDING_PAY
|
||||
}),
|
||||
orderDao.groupSubOrdersByState({
|
||||
supplierId,
|
||||
userId,
|
||||
deleted: 0,
|
||||
orderState: {
|
||||
in: [ORDER_STATE.IN_PROGRESS]
|
||||
}
|
||||
}),
|
||||
orderDao.countAfterSales({
|
||||
supplierId,
|
||||
userId,
|
||||
deleted: 0,
|
||||
serviceType: {
|
||||
in: [ORDER_EVENT_TYPE.RETURN_ORDER, ORDER_EVENT_TYPE.EXCHANGE_ORDER]
|
||||
}
|
||||
})
|
||||
])
|
||||
const countByState = subStateCounts.reduce<Record<number, number>>((result, item) => {
|
||||
result[item.orderState] = item._count._all
|
||||
return result
|
||||
}, {})
|
||||
const pendingReceive = countByState[ORDER_STATE.IN_PROGRESS] || 0
|
||||
|
||||
return serviceOk({ pendingPay, pendingReceive, afterSale })
|
||||
},
|
||||
|
||||
async listOrders(supplierId: string, userId: string) {
|
||||
logger.info('order list query', { supplierId, userId })
|
||||
|
||||
|
||||
Reference in New Issue
Block a user