chore: establish workspace and initial asset data model
This commit is contained in:
commit
67220e38ed
20 files changed
+2853
No files matched your search
@@ -0,0 +1,4 @@
|
||||
DATABASE_URL="mysql://USER:PASSWORD@HOST:3306/worthpath"
|
||||
PORT=3000
|
||||
WEB_ORIGIN=http://localhost:5173
|
||||
COOKIE_SECURE=false
|
||||
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"name": "@worthpath/api",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "tsx watch src/main.ts",
|
||||
"build": "tsc",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"test": "tsx --test test/*.test.ts",
|
||||
"db:generate": "prisma generate",
|
||||
"db:migrate": "prisma migrate deploy"
|
||||
},
|
||||
"dependencies": {
|
||||
"@nestjs/common": "^11.0.0",
|
||||
"@nestjs/core": "^11.0.0",
|
||||
"@nestjs/platform-express": "^11.0.0",
|
||||
"@prisma/client": "6.19.0",
|
||||
"bcryptjs": "^3.0.0",
|
||||
"cookie-parser": "^1.4.7",
|
||||
"decimal.js": "^10.6.0",
|
||||
"dotenv": "^17.2.0",
|
||||
"helmet": "^8.1.0",
|
||||
"reflect-metadata": "^0.2.2",
|
||||
"rxjs": "^7.8.2",
|
||||
"zod": "^4.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"prisma": "6.19.0",
|
||||
"tsx": "^4.20.0",
|
||||
"typescript": "^5.9.0",
|
||||
"@types/node": "^24.0.0",
|
||||
"@types/express": "^5.0.0",
|
||||
"@types/cookie-parser": "^1.4.9",
|
||||
"mysql2": "^3.15.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE `User` (
|
||||
`id` CHAR(36) NOT NULL,
|
||||
`username` VARCHAR(64) NOT NULL,
|
||||
`passwordHash` VARCHAR(255) NOT NULL,
|
||||
`baseCurrency` CHAR(3) NOT NULL DEFAULT 'CNY',
|
||||
`createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
`updatedAt` DATETIME(3) NOT NULL,
|
||||
|
||||
UNIQUE INDEX `User_username_key`(`username`),
|
||||
PRIMARY KEY (`id`)
|
||||
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE `Session` (
|
||||
`id` CHAR(64) NOT NULL,
|
||||
`userId` CHAR(36) NOT NULL,
|
||||
`expiresAt` DATETIME(3) NOT NULL,
|
||||
|
||||
INDEX `Session_userId_idx`(`userId`),
|
||||
PRIMARY KEY (`id`)
|
||||
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE `Position` (
|
||||
`id` CHAR(36) NOT NULL,
|
||||
`userId` CHAR(36) NOT NULL,
|
||||
`kind` VARCHAR(16) NOT NULL,
|
||||
`side` VARCHAR(16) NOT NULL,
|
||||
`name` VARCHAR(100) NOT NULL,
|
||||
`category` VARCHAR(40) NOT NULL,
|
||||
`currency` CHAR(3) NOT NULL,
|
||||
`notes` TEXT NOT NULL,
|
||||
`archived` BOOLEAN NOT NULL DEFAULT false,
|
||||
`createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
`updatedAt` DATETIME(3) NOT NULL,
|
||||
|
||||
INDEX `Position_userId_kind_idx`(`userId`, `kind`),
|
||||
PRIMARY KEY (`id`)
|
||||
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE `Revision` (
|
||||
`id` CHAR(36) NOT NULL,
|
||||
`positionId` CHAR(36) NOT NULL,
|
||||
`amount` DECIMAL(24, 8) NOT NULL,
|
||||
`effectiveDate` DATE NOT NULL,
|
||||
`notes` TEXT NOT NULL,
|
||||
`reason` VARCHAR(20) NOT NULL,
|
||||
`createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
`updatedAt` DATETIME(3) NOT NULL,
|
||||
|
||||
UNIQUE INDEX `Revision_positionId_effectiveDate_key`(`positionId`, `effectiveDate`),
|
||||
PRIMARY KEY (`id`)
|
||||
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE `PositionLink` (
|
||||
`id` CHAR(36) NOT NULL,
|
||||
`sourceId` CHAR(36) NOT NULL,
|
||||
`targetId` CHAR(36) NOT NULL,
|
||||
|
||||
UNIQUE INDEX `PositionLink_sourceId_targetId_key`(`sourceId`, `targetId`),
|
||||
PRIMARY KEY (`id`)
|
||||
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE `ExchangeRate` (
|
||||
`id` CHAR(36) NOT NULL,
|
||||
`userId` CHAR(36) NOT NULL,
|
||||
`currency` CHAR(3) NOT NULL,
|
||||
`baseCurrency` CHAR(3) NOT NULL,
|
||||
`date` DATE NOT NULL,
|
||||
`rate` DECIMAL(24, 12) NOT NULL,
|
||||
`source` VARCHAR(30) NOT NULL,
|
||||
|
||||
UNIQUE INDEX `ExchangeRate_userId_currency_baseCurrency_date_key`(`userId`, `currency`, `baseCurrency`, `date`),
|
||||
PRIMARY KEY (`id`)
|
||||
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE `Session` ADD CONSTRAINT `Session_userId_fkey` FOREIGN KEY (`userId`) REFERENCES `User`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE `Position` ADD CONSTRAINT `Position_userId_fkey` FOREIGN KEY (`userId`) REFERENCES `User`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE `Revision` ADD CONSTRAINT `Revision_positionId_fkey` FOREIGN KEY (`positionId`) REFERENCES `Position`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE `PositionLink` ADD CONSTRAINT `PositionLink_sourceId_fkey` FOREIGN KEY (`sourceId`) REFERENCES `Position`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE `PositionLink` ADD CONSTRAINT `PositionLink_targetId_fkey` FOREIGN KEY (`targetId`) REFERENCES `Position`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE `ExchangeRate` ADD CONSTRAINT `ExchangeRate_userId_fkey` FOREIGN KEY (`userId`) REFERENCES `User`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
@@ -0,0 +1 @@
|
||||
provider = "mysql"
|
||||
@@ -0,0 +1,74 @@
|
||||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
}
|
||||
datasource db {
|
||||
provider = "mysql"
|
||||
url = env("DATABASE_URL")
|
||||
}
|
||||
model User {
|
||||
id String @id @default(uuid()) @db.Char(36)
|
||||
username String @unique @db.VarChar(64)
|
||||
passwordHash String @db.VarChar(255)
|
||||
baseCurrency String @default("CNY") @db.Char(3)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
positions Position[]
|
||||
rates ExchangeRate[]
|
||||
sessions Session[]
|
||||
}
|
||||
model Session {
|
||||
id String @id @db.Char(64)
|
||||
userId String @db.Char(36)
|
||||
user User @relation(fields:[userId],references:[id],onDelete:Cascade)
|
||||
expiresAt DateTime
|
||||
@@index([userId])
|
||||
}
|
||||
model Position {
|
||||
id String @id @default(uuid()) @db.Char(36)
|
||||
userId String @db.Char(36)
|
||||
user User @relation(fields:[userId],references:[id],onDelete:Cascade)
|
||||
kind String @db.VarChar(16)
|
||||
side String @db.VarChar(16)
|
||||
name String @db.VarChar(100)
|
||||
category String @db.VarChar(40)
|
||||
currency String @db.Char(3)
|
||||
notes String @db.Text
|
||||
archived Boolean @default(false)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
revisions Revision[]
|
||||
outgoing PositionLink[] @relation("Source")
|
||||
incoming PositionLink[] @relation("Target")
|
||||
@@index([userId,kind])
|
||||
}
|
||||
model Revision {
|
||||
id String @id @default(uuid()) @db.Char(36)
|
||||
positionId String @db.Char(36)
|
||||
position Position @relation(fields:[positionId],references:[id],onDelete:Cascade)
|
||||
amount Decimal @db.Decimal(24,8)
|
||||
effectiveDate DateTime @db.Date
|
||||
notes String @db.Text
|
||||
reason String @db.VarChar(20)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
@@unique([positionId,effectiveDate])
|
||||
}
|
||||
model PositionLink {
|
||||
id String @id @default(uuid()) @db.Char(36)
|
||||
sourceId String @db.Char(36)
|
||||
targetId String @db.Char(36)
|
||||
source Position @relation("Source",fields:[sourceId],references:[id],onDelete:Cascade)
|
||||
target Position @relation("Target",fields:[targetId],references:[id],onDelete:Cascade)
|
||||
@@unique([sourceId,targetId])
|
||||
}
|
||||
model ExchangeRate {
|
||||
id String @id @default(uuid()) @db.Char(36)
|
||||
userId String @db.Char(36)
|
||||
user User @relation(fields:[userId],references:[id],onDelete:Cascade)
|
||||
currency String @db.Char(3)
|
||||
baseCurrency String @db.Char(3)
|
||||
date DateTime @db.Date
|
||||
rate Decimal @db.Decimal(24,12)
|
||||
source String @db.VarChar(30)
|
||||
@@unique([userId,currency,baseCurrency,date])
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
require('dotenv').config({quiet:true});
|
||||
const mysql=require('mysql2/promise');
|
||||
(async()=>{let db;try {const u=new URL(process.env.DATABASE_URL);const name=u.pathname.slice(1);if(!/^[a-zA-Z0-9_]+$/.test(name))throw Error();db=await mysql.createConnection({host:u.hostname,port:Number(u.port||3306),user:decodeURIComponent(u.username),password:decodeURIComponent(u.password)});const [existing]=await db.execute('SELECT SCHEMA_NAME FROM information_schema.SCHEMATA WHERE SCHEMA_NAME=?',[name]);if(existing.length){const [tables]=await db.execute('SELECT TABLE_NAME FROM information_schema.TABLES WHERE TABLE_SCHEMA=?',[name]);console.log('Database already exists; tables:',tables.map(t=>t.TABLE_NAME));}else {await db.query('CREATE DATABASE `'+name+'` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci');console.log('Created new empty project database.');}}catch(e){console.error('Database preflight failed:',e.code||'configuration error');process.exitCode=1;}finally{if(db)await db.end();}})();
|
||||
@@ -0,0 +1,8 @@
|
||||
import 'reflect-metadata';
|
||||
import 'dotenv/config';
|
||||
import {Module,Controller,Get} from '@nestjs/common';
|
||||
import {NestFactory} from '@nestjs/core';
|
||||
import {PrismaClient} from '@prisma/client';
|
||||
@Controller('api') class HealthController {@Get('health') health(){return {status:'ok',app:'WorthPath'};}}
|
||||
@Module({controllers:[HealthController]}) class AppModule {}
|
||||
async function bootstrap(){ const db=new PrismaClient();await db.$connect(); const app=await NestFactory.create(AppModule);await app.listen(Number(process.env.PORT||3000),'0.0.0.0');}void bootstrap();
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "commonjs",
|
||||
"moduleResolution": "node",
|
||||
"outDir": "dist",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"experimentalDecorators": true,
|
||||
"emitDecoratorMetadata": true,
|
||||
"skipLibCheck": true
|
||||
},
|
||||
"include": [
|
||||
"src/**/*.ts"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<!doctype html><html lang="zh-CN"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>WorthPath · 资产之路</title></head><body><div id="root"></div><script type="module" src="/src/main.tsx"></script></body></html>
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"name": "@worthpath/web",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite --host 0.0.0.0",
|
||||
"build": "tsc --noEmit && vite build",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"react": "^19.1.0",
|
||||
"react-dom": "^19.1.0",
|
||||
"lucide-react": "^0.468.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^19.1.0",
|
||||
"@types/react-dom": "^19.1.0",
|
||||
"typescript": "^5.9.0",
|
||||
"vite": "^7.1.0",
|
||||
"@vitejs/plugin-react": "^5.0.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
import React from 'react';import{createRoot}from'react-dom/client';createRoot(document.getElementById('root')!).render(<React.StrictMode><h1>WorthPath</h1><p>资产管理服务正在搭建。</p></React.StrictMode>);
|
||||
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"lib": [
|
||||
"ES2022",
|
||||
"DOM",
|
||||
"DOM.Iterable"
|
||||
],
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"jsx": "react-jsx",
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"noEmit": true,
|
||||
"allowImportingTsExtensions": true
|
||||
},
|
||||
"include": [
|
||||
"src",
|
||||
"vite.config.ts"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
import {defineConfig} from 'vite';
|
||||
import react from '@vitejs/plugin-react';
|
||||
export default defineConfig({plugins:[react()],server:{port:5173,proxy:{'/api':'http://127.0.0.1:3000'}}});
|
||||
Reference in new issue
Block a user