feat: integrate Prisma for database management, add Payer and Invoice models, and enhance error handling in /payer route
This commit is contained in:
parent
be9a4c21ee
commit
ed85337715
83
backend/package-lock.json
generated
83
backend/package-lock.json
generated
|
@ -9,6 +9,7 @@
|
|||
"version": "1.0.0",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@prisma/client": "^6.2.1",
|
||||
"debug": "^4.4.0",
|
||||
"dotenv": "^16.4.7",
|
||||
"koa": "^2.15.3",
|
||||
|
@ -22,6 +23,7 @@
|
|||
"@types/koa-route": "^3.2.8",
|
||||
"@types/node": "^14.0.0",
|
||||
"nodemon": "^3.1.9",
|
||||
"prisma": "^6.2.1",
|
||||
"ts-node": "^10.0.0",
|
||||
"typedoc": "^0.27.6",
|
||||
"typescript": "^5.7.3"
|
||||
|
@ -80,6 +82,68 @@
|
|||
"@jridgewell/sourcemap-codec": "^1.4.10"
|
||||
}
|
||||
},
|
||||
"node_modules/@prisma/client": {
|
||||
"version": "6.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/client/-/client-6.2.1.tgz",
|
||||
"integrity": "sha512-msKY2iRLISN8t5X0Tj7hU0UWet1u0KuxSPHWuf3IRkB4J95mCvGpyQBfQ6ufcmvKNOMQSq90O2iUmJEN2e5fiA==",
|
||||
"hasInstallScript": true,
|
||||
"engines": {
|
||||
"node": ">=18.18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"prisma": "*"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"prisma": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@prisma/debug": {
|
||||
"version": "6.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-6.2.1.tgz",
|
||||
"integrity": "sha512-0KItvt39CmQxWkEw6oW+RQMD6RZ43SJWgEUnzxN8VC9ixMysa7MzZCZf22LCK5DSooiLNf8vM3LHZm/I/Ni7bQ==",
|
||||
"devOptional": true
|
||||
},
|
||||
"node_modules/@prisma/engines": {
|
||||
"version": "6.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-6.2.1.tgz",
|
||||
"integrity": "sha512-lTBNLJBCxVT9iP5I7Mn6GlwqAxTpS5qMERrhebkUhtXpGVkBNd/jHnNJBZQW4kGDCKaQg/r2vlJYkzOHnAb7ZQ==",
|
||||
"devOptional": true,
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"@prisma/debug": "6.2.1",
|
||||
"@prisma/engines-version": "6.2.0-14.4123509d24aa4dede1e864b46351bf2790323b69",
|
||||
"@prisma/fetch-engine": "6.2.1",
|
||||
"@prisma/get-platform": "6.2.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@prisma/engines-version": {
|
||||
"version": "6.2.0-14.4123509d24aa4dede1e864b46351bf2790323b69",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-6.2.0-14.4123509d24aa4dede1e864b46351bf2790323b69.tgz",
|
||||
"integrity": "sha512-7tw1qs/9GWSX6qbZs4He09TOTg1ff3gYsB3ubaVNN0Pp1zLm9NC5C5MZShtkz7TyQjx7blhpknB7HwEhlG+PrQ==",
|
||||
"devOptional": true
|
||||
},
|
||||
"node_modules/@prisma/fetch-engine": {
|
||||
"version": "6.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-6.2.1.tgz",
|
||||
"integrity": "sha512-OO7O9d6Mrx2F9i+Gu1LW+DGXXyUFkP7OE5aj9iBfA/2jjDXEJjqa9X0ZmM9NZNo8Uo7ql6zKm6yjDcbAcRrw1A==",
|
||||
"devOptional": true,
|
||||
"dependencies": {
|
||||
"@prisma/debug": "6.2.1",
|
||||
"@prisma/engines-version": "6.2.0-14.4123509d24aa4dede1e864b46351bf2790323b69",
|
||||
"@prisma/get-platform": "6.2.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@prisma/get-platform": {
|
||||
"version": "6.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-6.2.1.tgz",
|
||||
"integrity": "sha512-zp53yvroPl5m5/gXYLz7tGCNG33bhG+JYCm74ohxOq1pPnrL47VQYFfF3RbTZ7TzGWCrR3EtoiYMywUBw7UK6Q==",
|
||||
"devOptional": true,
|
||||
"dependencies": {
|
||||
"@prisma/debug": "6.2.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@shikijs/engine-oniguruma": {
|
||||
"version": "1.26.1",
|
||||
"resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-1.26.1.tgz",
|
||||
|
@ -1339,6 +1403,25 @@
|
|||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"node_modules/prisma": {
|
||||
"version": "6.2.1",
|
||||
"resolved": "https://registry.npmjs.org/prisma/-/prisma-6.2.1.tgz",
|
||||
"integrity": "sha512-hhyM0H13pQleQ+br4CkzGizS5I0oInoeTw3JfLw1BRZduBSQxPILlJLwi+46wZzj9Je7ndyQEMGw/n5cN2fknA==",
|
||||
"devOptional": true,
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"@prisma/engines": "6.2.1"
|
||||
},
|
||||
"bin": {
|
||||
"prisma": "build/index.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.18"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "2.3.3"
|
||||
}
|
||||
},
|
||||
"node_modules/pstree.remy": {
|
||||
"version": "1.1.8",
|
||||
"resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz",
|
||||
|
|
|
@ -15,6 +15,7 @@
|
|||
"@types/koa-route": "^3.2.8",
|
||||
"@types/node": "^14.0.0",
|
||||
"nodemon": "^3.1.9",
|
||||
"prisma": "^6.2.1",
|
||||
"ts-node": "^10.0.0",
|
||||
"typedoc": "^0.27.6",
|
||||
"typescript": "^5.7.3"
|
||||
|
@ -23,6 +24,7 @@
|
|||
"license": "ISC",
|
||||
"description": "",
|
||||
"dependencies": {
|
||||
"@prisma/client": "^6.2.1",
|
||||
"debug": "^4.4.0",
|
||||
"dotenv": "^16.4.7",
|
||||
"koa": "^2.15.3",
|
||||
|
|
47
backend/prisma/migrations/20250111194859_init/migration.sql
Normal file
47
backend/prisma/migrations/20250111194859_init/migration.sql
Normal file
|
@ -0,0 +1,47 @@
|
|||
-- CreateTable
|
||||
CREATE TABLE `Payer` (
|
||||
`payer_id` INTEGER NOT NULL AUTO_INCREMENT,
|
||||
`payer_name` VARCHAR(100) NOT NULL,
|
||||
`payer_address` VARCHAR(255) NOT NULL,
|
||||
`payer_abn` VARCHAR(15) NULL,
|
||||
`payer_email` VARCHAR(100) NOT NULL,
|
||||
|
||||
PRIMARY KEY (`payer_id`)
|
||||
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE `Invoice` (
|
||||
`payer_id` INTEGER NOT NULL,
|
||||
`invoice_suffix_code` INTEGER NOT NULL,
|
||||
`invoice_payer_name` VARCHAR(100) NOT NULL,
|
||||
`invoice_payer_address` VARCHAR(255) NOT NULL,
|
||||
`invoice_payer_abn` VARCHAR(15) NULL,
|
||||
`invoice_billing_period_start` DATETIME(3) NOT NULL,
|
||||
`invoice_billing_period_end` DATETIME(3) NOT NULL,
|
||||
`invoice_due_date` DATETIME(3) NOT NULL,
|
||||
`invoice_date` DATETIME(3) NOT NULL,
|
||||
`invoice_total_amount` DECIMAL(10, 2) NOT NULL,
|
||||
`invoice_note` TEXT NULL,
|
||||
|
||||
PRIMARY KEY (`invoice_date`, `invoice_suffix_code`)
|
||||
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE `InvoiceItem` (
|
||||
`item_id` INTEGER NOT NULL,
|
||||
`invoice_date` DATETIME(3) NOT NULL,
|
||||
`invoice_suffix_code` INTEGER NOT NULL,
|
||||
`item_description` VARCHAR(255) NOT NULL,
|
||||
`item_quantity` INTEGER NOT NULL,
|
||||
`item_unit_price` DECIMAL(10, 2) NOT NULL,
|
||||
`item_unit` VARCHAR(50) NOT NULL,
|
||||
`invoice_id` INTEGER NOT NULL,
|
||||
|
||||
PRIMARY KEY (`item_id`, `invoice_date`, `invoice_suffix_code`)
|
||||
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE `Invoice` ADD CONSTRAINT `Invoice_payer_id_fkey` FOREIGN KEY (`payer_id`) REFERENCES `Payer`(`payer_id`) ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE `InvoiceItem` ADD CONSTRAINT `InvoiceItem_invoice_date_invoice_suffix_code_fkey` FOREIGN KEY (`invoice_date`, `invoice_suffix_code`) REFERENCES `Invoice`(`invoice_date`, `invoice_suffix_code`) ON DELETE RESTRICT ON UPDATE CASCADE;
|
3
backend/prisma/migrations/migration_lock.toml
Normal file
3
backend/prisma/migrations/migration_lock.toml
Normal file
|
@ -0,0 +1,3 @@
|
|||
# Please do not edit this file manually
|
||||
# It should be added in your version-control system (e.g., Git)
|
||||
provider = "mysql"
|
55
backend/prisma/schema.prisma
Normal file
55
backend/prisma/schema.prisma
Normal file
|
@ -0,0 +1,55 @@
|
|||
// This is your Prisma schema file,
|
||||
// learn more about it in the docs: https://pris.ly/d/prisma-schema
|
||||
|
||||
// Looking for ways to speed up your queries, or scale easily with your serverless or edge functions?
|
||||
// Try Prisma Accelerate: https://pris.ly/cli/accelerate-init
|
||||
|
||||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
}
|
||||
|
||||
datasource db {
|
||||
provider = "mysql"
|
||||
url = env("DB_CONNECTION")
|
||||
}
|
||||
|
||||
model Payer {
|
||||
payer_id Int @id @default(autoincrement())
|
||||
payer_name String @db.VarChar(100)
|
||||
payer_address String @db.VarChar(255)
|
||||
payer_abn String? @db.VarChar(15)
|
||||
payer_email String @db.VarChar(100)
|
||||
invoices Invoice[]
|
||||
}
|
||||
|
||||
model Invoice {
|
||||
payer_id Int
|
||||
invoice_suffix_code Int
|
||||
invoice_payer_name String @db.VarChar(100)
|
||||
invoice_payer_address String @db.VarChar(255)
|
||||
invoice_payer_abn String? @db.VarChar(15)
|
||||
invoice_billing_period_start DateTime
|
||||
invoice_billing_period_end DateTime
|
||||
invoice_due_date DateTime
|
||||
invoice_date DateTime
|
||||
invoice_total_amount Decimal @db.Decimal(10, 2)
|
||||
invoice_note String? @db.Text
|
||||
payer Payer @relation(fields: [payer_id], references: [payer_id])
|
||||
invoice_items InvoiceItem[]
|
||||
|
||||
@@id([invoice_date, invoice_suffix_code])
|
||||
}
|
||||
|
||||
model InvoiceItem {
|
||||
item_id Int
|
||||
invoice_date DateTime @map("invoice_date")
|
||||
invoice_suffix_code Int @map("invoice_suffix_code")
|
||||
item_description String @db.VarChar(255)
|
||||
item_quantity Int
|
||||
item_unit_price Decimal @db.Decimal(10, 2)
|
||||
item_unit String @db.VarChar(50)
|
||||
invoice_id Int
|
||||
invoice Invoice @relation(fields: [invoice_date, invoice_suffix_code], references: [invoice_date, invoice_suffix_code])
|
||||
|
||||
@@id([item_id, invoice_date, invoice_suffix_code])
|
||||
}
|
|
@ -4,6 +4,7 @@ import route from 'koa-route'
|
|||
import Debug from 'debug'
|
||||
import koaBody from 'koa-body'
|
||||
import func from './func'
|
||||
import { ErrorDescEnum, HttpError } from './types/HttpError'
|
||||
|
||||
dotenv.config()
|
||||
console.log = Debug('invoiceIssuer:app.ts')
|
||||
|
@ -11,32 +12,50 @@ console.log = Debug('invoiceIssuer:app.ts')
|
|||
const app = new Koa()
|
||||
app.use(koaBody())
|
||||
|
||||
// 错误处理中间件
|
||||
app.use(async (ctx, next) => {
|
||||
try {
|
||||
await next()
|
||||
} catch (e) {
|
||||
if (e instanceof HttpError) {
|
||||
ctx.status = e.status
|
||||
ctx.body = JSON.stringify({ error: e.message })
|
||||
} 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)
|
||||
|
||||
// 有字段,但为空时
|
||||
const { name, address, email, abn } = ctx.request.body
|
||||
if (!name || !address || !email) ctx.throw(400, 'required_fields_missing')
|
||||
if (!name || !address || !email) throw new HttpError(ErrorDescEnum.required_fields_missing, 400)
|
||||
|
||||
// 验证地址格式,格式应为 string[],最多两行
|
||||
if (!Array.isArray(address) || address.length > 2 || address.length <= 0) throw new HttpError(ErrorDescEnum.invalid_field_format, 400)
|
||||
|
||||
// 名字应为字符串
|
||||
if (typeof name !== 'string') throw new HttpError(ErrorDescEnum.invalid_field_format, 400)
|
||||
|
||||
// 验证邮箱格式
|
||||
const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/
|
||||
if (!emailRegex.test(email)) ctx.throw(400, 'invalid_field_format')
|
||||
if (!emailRegex.test(email)) throw new HttpError(ErrorDescEnum.invalid_field_format, 400)
|
||||
|
||||
try {
|
||||
const payer = await func.createPayer(name, address, email, abn)
|
||||
ctx.body = payer
|
||||
} catch (e) {
|
||||
if (e instanceof HttpError) {
|
||||
ctx.status = e.status
|
||||
ctx.body = e.message
|
||||
} else {
|
||||
console.error(e)
|
||||
ctx.throw(500, 'unknown_issues')
|
||||
}
|
||||
}
|
||||
// 创建新的付款人
|
||||
await func.createPayer(name, address, email, abn)
|
||||
ctx.status = 204
|
||||
}))
|
||||
|
||||
const port = parseInt(process.env.PORT ?? '3000')
|
||||
|
|
|
@ -1,3 +1,8 @@
|
|||
import Debug from 'debug'
|
||||
import { PrismaClient } from '@prisma/client'
|
||||
|
||||
console.log = Debug('invoiceIssuer:func/createPayer.ts')
|
||||
|
||||
/**
|
||||
* POST /payer
|
||||
* @summary 创建一个新的付款人。这个付款人与收据上标识的收款人信息分离,但内部数据库关联(以便检索)。
|
||||
|
@ -7,4 +12,23 @@
|
|||
* @param {string} abn - 付款人的澳大利亚商业号码
|
||||
*/
|
||||
export default async (name: string, address: string[], email: string, abn?: string) => {
|
||||
const prisma = new PrismaClient()
|
||||
|
||||
try {
|
||||
// 创建新的付款人
|
||||
const payer = await prisma.payer.create({
|
||||
data: {
|
||||
payer_name: name,
|
||||
payer_address: address.join('\n'),
|
||||
payer_email: email,
|
||||
payer_abn: abn
|
||||
}
|
||||
})
|
||||
return payer
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
throw new Error('unknown_issues')
|
||||
} finally {
|
||||
await prisma.$disconnect()
|
||||
}
|
||||
}
|
|
@ -8,7 +8,14 @@ class HttpError extends Error {
|
|||
* @param message 错误的消息。
|
||||
* @param status 错误的状态码。
|
||||
*/
|
||||
constructor(message: ErrorDescEnum, status: number);
|
||||
constructor(message: ErrorDescEnum, status: 400 | 401 | 403 | 404 | 500) {
|
||||
super(message);
|
||||
this.status = status;
|
||||
}
|
||||
|
||||
/**
|
||||
* 错误的状态码。
|
||||
*/
|
||||
status: 400 | 401 | 403 | 404 | 500;
|
||||
}
|
||||
|
||||
|
@ -28,4 +35,6 @@ enum ErrorDescEnum {
|
|||
* 未知错误。通常与 HTTP 状态码 500 Internal Server Error 一起使用。
|
||||
*/
|
||||
unknown_issues = 'unknown_issues'
|
||||
}
|
||||
}
|
||||
|
||||
export { HttpError, ErrorDescEnum }
|
Loading…
Reference in New Issue
Block a user