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' import InvoiceItem from './classes/InvoiceItem' 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) => { // 请求头验证 bearer token const bearerToken = ctx.request.headers['authorization']?.split(' ') if (!bearerToken) throw new HttpError(ErrorDescEnum.unauthorized, 401) if (bearerToken[0] !== 'Bearer') throw new HttpError(ErrorDescEnum.unauthorized, 401) if (!bearerToken[1]) throw new HttpError(ErrorDescEnum.unauthorized, 401) await func.verifyBearerToken(bearerToken[1]) // 验证必填字段 // 字段缺失时 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.get('/payer', async (ctx) => { // 请求头验证 bearer token const bearerToken = ctx.request.headers['authorization']?.split(' ') if (!bearerToken) throw new HttpError(ErrorDescEnum.unauthorized, 401) if (bearerToken[0] !== 'Bearer') throw new HttpError(ErrorDescEnum.unauthorized, 401) if (!bearerToken[1]) throw new HttpError(ErrorDescEnum.unauthorized, 401) await func.verifyBearerToken(bearerToken[1]) // 获取 pager 和 limit const pageQuery = ctx.request.query.page if (Array.isArray(pageQuery)) throw new HttpError(ErrorDescEnum.invalid_field_format, 400, ['page']) const page = pageQuery ? parseInt(pageQuery) : 1 const limitQuery = ctx.request.query.limit if (Array.isArray(limitQuery)) throw new HttpError(ErrorDescEnum.invalid_field_format, 400, ['limit']) const limit = limitQuery ? parseInt(limitQuery) : 10 if (page <= 0 || limit < 5) throw new HttpError(ErrorDescEnum.invalid_field_format, 400, ['page', 'limit']) // 获取付款人 const payers = await func.getPayers(ctx.prisma, page, limit) ctx.body = payers })) app.use(route.put('/payer/:id', async (ctx, id) => { // 请求头验证 bearer token const bearerToken = ctx.request.headers['authorization']?.split(' ') if (!bearerToken) throw new HttpError(ErrorDescEnum.unauthorized, 401) if (bearerToken[0] !== 'Bearer') throw new HttpError(ErrorDescEnum.unauthorized, 401) if (!bearerToken[1]) throw new HttpError(ErrorDescEnum.unauthorized, 401) await func.verifyBearerToken(bearerToken[1]) // 验证 id 必须是数字 if (isNaN(parseInt(id))) throw new HttpError(ErrorDescEnum.invalid_field_format, 400, ['id']) // 验证必填字段 // 字段缺失时 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.updatePayer(ctx.prisma, id, name, address, email, abn) ctx.status = 204 })) app.use(route.delete('/payer/:id', async (ctx, id) => { // 请求头验证 bearer token const bearerToken = ctx.request.headers['authorization']?.split(' ') if (!bearerToken) throw new HttpError(ErrorDescEnum.unauthorized, 401) if (bearerToken[0] !== 'Bearer') throw new HttpError(ErrorDescEnum.unauthorized, 401) if (!bearerToken[1]) throw new HttpError(ErrorDescEnum.unauthorized, 401) await func.verifyBearerToken(bearerToken[1]) if (isNaN(parseInt(id))) throw new HttpError(ErrorDescEnum.invalid_field_format, 400, ['id']) // 删除付款人 await func.deletePayer(ctx.prisma, id) ctx.status = 204 })) app.use(route.post('/invoice', async (ctx) => { // 请求头验证 bearer token const bearerToken = ctx.request.headers['authorization']?.split(' ') if (!bearerToken) throw new HttpError(ErrorDescEnum.unauthorized, 401) if (bearerToken[0] !== 'Bearer') throw new HttpError(ErrorDescEnum.unauthorized, 401) if (!bearerToken[1]) throw new HttpError(ErrorDescEnum.unauthorized, 401) await func.verifyBearerToken(bearerToken[1]) // 提取字段,并验证必填字段 if (!ctx.request.body) throw new HttpError(ErrorDescEnum.required_fields_missing, 400, ['payerId', 'period', 'items', 'dueDate']) const { payerId, period: periodRaw, items: itemsRaw, dueDate: dueDateRaw, note } = ctx.request.body let emptyFields = [] if (!payerId) emptyFields.push('payerId') if (!periodRaw) emptyFields.push('period') if (!itemsRaw) emptyFields.push('items') if (!dueDateRaw) 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(periodRaw) || periodRaw.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(periodRaw[0]) || !dateRegex.test(periodRaw[1])) throw new HttpError(ErrorDescEnum.invalid_field_format, 400, ['period']) if (new Date(periodRaw[0]) > new Date(periodRaw[1])) throw new HttpError(ErrorDescEnum.invalid_field_format, 400, ['period']) const period = [ new Date(periodRaw[0].split('-')[0], periodRaw[0].split('-')[1] - 1, periodRaw[0].split('-')[2]), new Date(periodRaw[1].split('-')[0], periodRaw[1].split('-')[1] - 1, periodRaw[1].split('-')[2]) ] // 验证 items 必须是数组 if (!Array.isArray(itemsRaw)) throw new HttpError(ErrorDescEnum.invalid_field_format, 400, ['items']) // 验证 items 中的每一项必须是指定格式的对象 // description: string, quantity: number, unit: string, unitPrice: Decimal const itemFields = ['description', 'quantity', 'unit', 'unitPrice'] itemsRaw.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(dueDateRaw)) throw new HttpError(ErrorDescEnum.invalid_field_format, 400, ['dueDate']) const dueDate = new Date(dueDateRaw.split('-')[0], dueDateRaw.split('-')[1] - 1, dueDateRaw.split('-')[2]) // 验证 note 必须是字符串 if (note && typeof note !== 'string') throw new HttpError(ErrorDescEnum.invalid_field_format, 400, ['note']) // 创建新的收据 const items = itemsRaw.map(item => { return new InvoiceItem(0, item['description'], item['quantity'], item['unit'], item['unitPrice']) }) await func.issueInvoice(ctx.prisma, payerId, period, items, dueDate, note) ctx.status = 204 })) app.use(route.get('/invoice/INV-:date(\\d{8})-:suffix(\\d+)', async (ctx, invoiceId, suffix) => { // 请求头验证 bearer token const bearerToken = ctx.request.headers['authorization']?.split(' ') if (!bearerToken) throw new HttpError(ErrorDescEnum.unauthorized, 401) if (bearerToken[0] !== 'Bearer') throw new HttpError(ErrorDescEnum.unauthorized, 401) if (!bearerToken[1]) throw new HttpError(ErrorDescEnum.unauthorized, 401) await func.verifyBearerToken(bearerToken[1]) // 验证 invoiceId 格式 const date = invoiceId.slice(0, 4) + '-' + invoiceId.slice(4, 6) + '-' + invoiceId.slice(6, 8) // 获取特定收据 const invoice = await func.getSpecificInvoice(ctx.prisma, date, parseInt(suffix)) console.log(invoice.exportJSON()) ctx.body = invoice.exportJSON() })) app.use(route.get('/invoice', async (ctx) => { // 请求头验证 bearer token const bearerToken = ctx.request.headers['authorization']?.split(' ') if (!bearerToken) throw new HttpError(ErrorDescEnum.unauthorized, 401) if (bearerToken[0] !== 'Bearer') throw new HttpError(ErrorDescEnum.unauthorized, 401) if (!bearerToken[1]) throw new HttpError(ErrorDescEnum.unauthorized, 401) await func.verifyBearerToken(bearerToken[1]) // 获取 pager 和 limit const pageQuery = ctx.request.query.page if (Array.isArray(pageQuery)) throw new HttpError(ErrorDescEnum.invalid_field_format, 400, ['page']) const page = pageQuery ? parseInt(pageQuery) : 1 const limitQuery = ctx.request.query.limit if (Array.isArray(limitQuery)) throw new HttpError(ErrorDescEnum.invalid_field_format, 400, ['limit']) const limit = limitQuery ? parseInt(limitQuery) : 10 if (page <= 0 || limit < 5) throw new HttpError(ErrorDescEnum.invalid_field_format, 400, ['page', 'limit']) // 获取收据 const invoices = await func.getInvoices(ctx.prisma, page, limit) ctx.body = await Promise.all(invoices.map(async invoice => (await invoice).exportJSON())) })) app.use(route.put('/invoice/INV-:date(\\d{8})-:suffix(\\d+)/note', async (ctx, invoiceId, suffix) => { // 请求头验证 bearer token const bearerToken = ctx.request.headers['authorization']?.split(' ') if (!bearerToken) throw new HttpError(ErrorDescEnum.unauthorized, 401) if (bearerToken[0] !== 'Bearer') throw new HttpError(ErrorDescEnum.unauthorized, 401) if (!bearerToken[1]) throw new HttpError(ErrorDescEnum.unauthorized, 401) await func.verifyBearerToken(bearerToken[1]) // 验证 invoiceId 格式 const date = invoiceId.slice(0, 4) + '-' + invoiceId.slice(4, 6) + '-' + invoiceId.slice(6, 8) // 验证必填字段 if (!ctx.request.body) throw new HttpError(ErrorDescEnum.required_fields_missing, 400, ['note']) console.log(typeof ctx.request.body.note) if (typeof ctx.request.body.note !== 'string' && typeof ctx.request.body.note !== 'undefined') throw new HttpError(ErrorDescEnum.invalid_field_format, 400, ['note']) // 更新收据备注 await func.updateInvoiceNote(ctx.prisma, date, suffix, ctx.request.body.note) ctx.status = 204 })) app.use(route.delete('/invoice/INV-:date(\\d{8})-:suffix(\\d+)', async (ctx, date, suffix) => { // 请求头验证 bearer token const bearerToken = ctx.request.headers['authorization']?.split(' ') if (!bearerToken) throw new HttpError(ErrorDescEnum.unauthorized, 401) if (bearerToken[0] !== 'Bearer') throw new HttpError(ErrorDescEnum.unauthorized, 401) if (!bearerToken[1]) throw new HttpError(ErrorDescEnum.unauthorized, 401) await func.verifyBearerToken(bearerToken[1]) // 删除收据 await func.deleteInvoice(ctx.prisma, date, suffix) ctx.status = 204 })) const port = parseInt(process.env.PORT ?? '3000') app.listen(port, () => { console.log(`Server is running at http://localhost:${port}`) })