import Koa from 'koa' import dotenv from 'dotenv' import route from 'koa-route' import Debug from 'debug' import koaBody from 'koa-body' import func from './func' import { ErrorDescEnum, HttpError } from './classes/HttpError' import { PrismaClient } from '@prisma/client' dotenv.config() console.log = Debug('invoiceIssuer:app.ts') const app = new Koa() app.use(koaBody()) // 语境中间件 app.use(async (ctx, next) => { try { const prismaClient = new PrismaClient() ctx.prisma = prismaClient await next() prismaClient.$disconnect() } catch (e) { if (e instanceof HttpError) { ctx.status = e.status let errorBody: { error: string, context?: string[] } = { error: e.message } if (e.context) errorBody['context'] = e.context ctx.body = JSON.stringify(errorBody) } else { console.log(e) ctx.status = 500 ctx.body = JSON.stringify({ error: ErrorDescEnum.unknown_issues }) } } }) // 具体路由 app.use(route.get('/', (ctx) => { ctx.body = 'Hello World' })) app.use(route.post('/payer', async (ctx) => { // TODO: 请求头验证 bearer token // 验证必填字段 // 字段缺失时 if (!ctx.request.body) throw new HttpError(ErrorDescEnum.required_fields_missing, 400, ['name', 'address', 'email']) // 有字段,但为空时 const { name, address, email, abn } = ctx.request.body let emptyFields = [] if (!name) emptyFields.push('name') if (!address) emptyFields.push('address') if (!email) emptyFields.push('email') if (emptyFields.length > 0) throw new HttpError(ErrorDescEnum.required_fields_missing, 400, emptyFields) // 验证地址格式,格式应为 string[],最多两行 if (!Array.isArray(address) || address.length > 2 || address.length <= 0) throw new HttpError(ErrorDescEnum.invalid_field_format, 400, ['address']) // 名字应为字符串 if (typeof name !== 'string') throw new HttpError(ErrorDescEnum.invalid_field_format, 400, ['name']) // 验证邮箱格式 const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/ if (!emailRegex.test(email)) throw new HttpError(ErrorDescEnum.invalid_field_format, 400, ['email']) // 验证 ABN 格式 const abnRegex = /^\d{11}$/ if (abn && !abnRegex.test(abn)) throw new HttpError(ErrorDescEnum.invalid_field_format, 400, ['abn']) // 创建新的付款人 await func.createPayer(ctx.prisma, name, address, email, abn) ctx.status = 204 })) app.use(route.post('/invoice', async (ctx) => { // TODO: 请求头验证 bearer token // 提取字段,并验证必填字段 const { payerId, period, items, dueDate, note } = ctx.request.body let emptyFields = [] if (!payerId) emptyFields.push('payerId') if (!period) emptyFields.push('period') if (!items) emptyFields.push('items') if (!dueDate) emptyFields.push('dueDate') if (emptyFields.length > 0) throw new HttpError(ErrorDescEnum.required_fields_missing, 400, emptyFields) // 验证 payerId 必须是数字 if (typeof payerId !== 'number') throw new HttpError(ErrorDescEnum.invalid_field_format, 400, ['payerId']) // 验证 period 必须是两个日期的数组 if (!Array.isArray(period) || period.length !== 2) throw new HttpError(ErrorDescEnum.invalid_field_format, 400, ['period']) // Period 中的日期格式为 YYYY-MM-DD,且第一个日期早于第二个日期 const dateRegex = /^\d{4}-\d{2}-\d{2}$/ if (!dateRegex.test(period[0]) || !dateRegex.test(period[1])) throw new HttpError(ErrorDescEnum.invalid_field_format, 400, ['period']) if (new Date(period[0]) > new Date(period[1])) throw new HttpError(ErrorDescEnum.invalid_field_format, 400, ['period']) // 验证 items 必须是数组 if (!Array.isArray(items)) throw new HttpError(ErrorDescEnum.invalid_field_format, 400, ['items']) // 验证 items 中的每一项必须是指定格式的对象 // description: string, quantity: number, unit: string, unitPrice: Decimal const itemFields = ['description', 'quantity', 'unit', 'unitPrice'] items.forEach((item, index) => { itemFields.forEach((field) => { if (!item[field]) throw new HttpError(ErrorDescEnum.required_fields_missing, 400, [`items[${index}].${field}`]) }) if (typeof item['description'] !== 'string') throw new HttpError(ErrorDescEnum.invalid_field_format, 400, [`items[${index}].description`]) if (typeof item['quantity'] !== 'number') throw new HttpError(ErrorDescEnum.invalid_field_format, 400, [`items[${index}].quantity`]) if (typeof item['unit'] !== 'string') throw new HttpError(ErrorDescEnum.invalid_field_format, 400, [`items[${index}].unit`]) if (typeof item['unitPrice'] !== 'number') throw new HttpError(ErrorDescEnum.invalid_field_format, 400, [`items[${index}].unitPrice`]) }) // 验证 dueDate 必须是日期格式 if (!dateRegex.test(dueDate)) throw new HttpError(ErrorDescEnum.invalid_field_format, 400, ['dueDate']) // 验证 note 必须是字符串 if (note && typeof note !== 'string') throw new HttpError(ErrorDescEnum.invalid_field_format, 400, ['note']) // 创建新的收据 await func.issueInvoice(ctx.prisma, payerId, period, items, dueDate, note) })) const port = parseInt(process.env.PORT ?? '3000') app.listen(port, () => { console.log(`Server is running at http://localhost:${port}`) })