feat: 完善采购申请流程 - 添加审批、执行、列表筛选排序功能
This commit is contained in:
@@ -1,56 +0,0 @@
|
||||
# compiled output
|
||||
/dist
|
||||
/node_modules
|
||||
/build
|
||||
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
pnpm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
lerna-debug.log*
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
|
||||
# Tests
|
||||
/coverage
|
||||
/.nyc_output
|
||||
|
||||
# IDEs and editors
|
||||
/.idea
|
||||
.project
|
||||
.classpath
|
||||
.c9/
|
||||
*.launch
|
||||
.settings/
|
||||
*.sublime-workspace
|
||||
|
||||
# IDE - VSCode
|
||||
.vscode/*
|
||||
!.vscode/settings.json
|
||||
!.vscode/tasks.json
|
||||
!.vscode/launch.json
|
||||
!.vscode/extensions.json
|
||||
|
||||
# dotenv environment variable files
|
||||
.env
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
.env.local
|
||||
|
||||
# temp directory
|
||||
.temp
|
||||
.tmp
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||
@@ -1,4 +0,0 @@
|
||||
{
|
||||
"singleQuote": true,
|
||||
"trailingComma": "all"
|
||||
}
|
||||
@@ -1,98 +0,0 @@
|
||||
<p align="center">
|
||||
<a href="http://nestjs.com/" target="blank"><img src="https://nestjs.com/img/logo-small.svg" width="120" alt="Nest Logo" /></a>
|
||||
</p>
|
||||
|
||||
[circleci-image]: https://img.shields.io/circleci/build/github/nestjs/nest/master?token=abc123def456
|
||||
[circleci-url]: https://circleci.com/gh/nestjs/nest
|
||||
|
||||
<p align="center">A progressive <a href="http://nodejs.org" target="_blank">Node.js</a> framework for building efficient and scalable server-side applications.</p>
|
||||
<p align="center">
|
||||
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/v/@nestjs/core.svg" alt="NPM Version" /></a>
|
||||
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/l/@nestjs/core.svg" alt="Package License" /></a>
|
||||
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/dm/@nestjs/common.svg" alt="NPM Downloads" /></a>
|
||||
<a href="https://circleci.com/gh/nestjs/nest" target="_blank"><img src="https://img.shields.io/circleci/build/github/nestjs/nest/master" alt="CircleCI" /></a>
|
||||
<a href="https://discord.gg/G7Qnnhy" target="_blank"><img src="https://img.shields.io/badge/discord-online-brightgreen.svg" alt="Discord"/></a>
|
||||
<a href="https://opencollective.com/nest#backer" target="_blank"><img src="https://opencollective.com/nest/backers/badge.svg" alt="Backers on Open Collective" /></a>
|
||||
<a href="https://opencollective.com/nest#sponsor" target="_blank"><img src="https://opencollective.com/nest/sponsors/badge.svg" alt="Sponsors on Open Collective" /></a>
|
||||
<a href="https://paypal.me/kamilmysliwiec" target="_blank"><img src="https://img.shields.io/badge/Donate-PayPal-ff3f59.svg" alt="Donate us"/></a>
|
||||
<a href="https://opencollective.com/nest#sponsor" target="_blank"><img src="https://img.shields.io/badge/Support%20us-Open%20Collective-41B883.svg" alt="Support us"></a>
|
||||
<a href="https://twitter.com/nestframework" target="_blank"><img src="https://img.shields.io/twitter/follow/nestframework.svg?style=social&label=Follow" alt="Follow us on Twitter"></a>
|
||||
</p>
|
||||
<!--[](https://opencollective.com/nest#backer)
|
||||
[](https://opencollective.com/nest#sponsor)-->
|
||||
|
||||
## Description
|
||||
|
||||
[Nest](https://github.com/nestjs/nest) framework TypeScript starter repository.
|
||||
|
||||
## Project setup
|
||||
|
||||
```bash
|
||||
$ npm install
|
||||
```
|
||||
|
||||
## Compile and run the project
|
||||
|
||||
```bash
|
||||
# development
|
||||
$ npm run start
|
||||
|
||||
# watch mode
|
||||
$ npm run start:dev
|
||||
|
||||
# production mode
|
||||
$ npm run start:prod
|
||||
```
|
||||
|
||||
## Run tests
|
||||
|
||||
```bash
|
||||
# unit tests
|
||||
$ npm run test
|
||||
|
||||
# e2e tests
|
||||
$ npm run test:e2e
|
||||
|
||||
# test coverage
|
||||
$ npm run test:cov
|
||||
```
|
||||
|
||||
## Deployment
|
||||
|
||||
When you're ready to deploy your NestJS application to production, there are some key steps you can take to ensure it runs as efficiently as possible. Check out the [deployment documentation](https://docs.nestjs.com/deployment) for more information.
|
||||
|
||||
If you are looking for a cloud-based platform to deploy your NestJS application, check out [Mau](https://mau.nestjs.com), our official platform for deploying NestJS applications on AWS. Mau makes deployment straightforward and fast, requiring just a few simple steps:
|
||||
|
||||
```bash
|
||||
$ npm install -g @nestjs/mau
|
||||
$ mau deploy
|
||||
```
|
||||
|
||||
With Mau, you can deploy your application in just a few clicks, allowing you to focus on building features rather than managing infrastructure.
|
||||
|
||||
## Resources
|
||||
|
||||
Check out a few resources that may come in handy when working with NestJS:
|
||||
|
||||
- Visit the [NestJS Documentation](https://docs.nestjs.com) to learn more about the framework.
|
||||
- For questions and support, please visit our [Discord channel](https://discord.gg/G7Qnnhy).
|
||||
- To dive deeper and get more hands-on experience, check out our official video [courses](https://courses.nestjs.com/).
|
||||
- Deploy your application to AWS with the help of [NestJS Mau](https://mau.nestjs.com) in just a few clicks.
|
||||
- Visualize your application graph and interact with the NestJS application in real-time using [NestJS Devtools](https://devtools.nestjs.com).
|
||||
- Need help with your project (part-time to full-time)? Check out our official [enterprise support](https://enterprise.nestjs.com).
|
||||
- To stay in the loop and get updates, follow us on [X](https://x.com/nestframework) and [LinkedIn](https://linkedin.com/company/nestjs).
|
||||
- Looking for a job, or have a job to offer? Check out our official [Jobs board](https://jobs.nestjs.com).
|
||||
|
||||
## Support
|
||||
|
||||
Nest is an MIT-licensed open source project. It can grow thanks to the sponsors and support by the amazing backers. If you'd like to join them, please [read more here](https://docs.nestjs.com/support).
|
||||
|
||||
## Stay in touch
|
||||
|
||||
- Author - [Kamil Myśliwiec](https://twitter.com/kammysliwiec)
|
||||
- Website - [https://nestjs.com](https://nestjs.com/)
|
||||
- Twitter - [@nestframework](https://twitter.com/nestframework)
|
||||
|
||||
## License
|
||||
|
||||
Nest is [MIT licensed](https://github.com/nestjs/nest/blob/master/LICENSE).
|
||||
Binary file not shown.
@@ -1,35 +0,0 @@
|
||||
// @ts-check
|
||||
import eslint from '@eslint/js';
|
||||
import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended';
|
||||
import globals from 'globals';
|
||||
import tseslint from 'typescript-eslint';
|
||||
|
||||
export default tseslint.config(
|
||||
{
|
||||
ignores: ['eslint.config.mjs'],
|
||||
},
|
||||
eslint.configs.recommended,
|
||||
...tseslint.configs.recommendedTypeChecked,
|
||||
eslintPluginPrettierRecommended,
|
||||
{
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.node,
|
||||
...globals.jest,
|
||||
},
|
||||
sourceType: 'commonjs',
|
||||
parserOptions: {
|
||||
projectService: true,
|
||||
tsconfigRootDir: import.meta.dirname,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
rules: {
|
||||
'@typescript-eslint/no-explicit-any': 'off',
|
||||
'@typescript-eslint/no-floating-promises': 'warn',
|
||||
'@typescript-eslint/no-unsafe-argument': 'warn',
|
||||
"prettier/prettier": ["error", { endOfLine: "auto" }],
|
||||
},
|
||||
},
|
||||
);
|
||||
@@ -1,8 +0,0 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/nest-cli",
|
||||
"collection": "@nestjs/schematics",
|
||||
"sourceRoot": "src",
|
||||
"compilerOptions": {
|
||||
"deleteOutDir": true
|
||||
}
|
||||
}
|
||||
Generated
-12237
File diff suppressed because it is too large
Load Diff
@@ -1,82 +0,0 @@
|
||||
{
|
||||
"name": "backend",
|
||||
"version": "0.0.1",
|
||||
"description": "",
|
||||
"author": "",
|
||||
"private": true,
|
||||
"license": "UNLICENSED",
|
||||
"scripts": {
|
||||
"build": "nest build",
|
||||
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
|
||||
"start": "nest start",
|
||||
"start:dev": "nest start --watch",
|
||||
"start:debug": "nest start --debug --watch",
|
||||
"start:prod": "node dist/main",
|
||||
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
|
||||
"test": "jest",
|
||||
"test:watch": "jest --watch",
|
||||
"test:cov": "jest --coverage",
|
||||
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
|
||||
"test:e2e": "jest --config ./test/jest-e2e.json"
|
||||
},
|
||||
"dependencies": {
|
||||
"@nestjs/common": "^11.0.1",
|
||||
"@nestjs/config": "^4.0.3",
|
||||
"@nestjs/core": "^11.0.1",
|
||||
"@nestjs/jwt": "^11.0.2",
|
||||
"@nestjs/passport": "^11.0.5",
|
||||
"@nestjs/platform-express": "^11.0.1",
|
||||
"@nestjs/typeorm": "^11.0.0",
|
||||
"bcryptjs": "^3.0.3",
|
||||
"class-validator": "^0.15.1",
|
||||
"passport": "^0.7.0",
|
||||
"passport-jwt": "^4.0.1",
|
||||
"pg": "^8.20.0",
|
||||
"reflect-metadata": "^0.2.2",
|
||||
"rxjs": "^7.8.1",
|
||||
"sqlite3": "^5.1.7",
|
||||
"typeorm": "^0.3.28"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/eslintrc": "^3.2.0",
|
||||
"@eslint/js": "^9.18.0",
|
||||
"@nestjs/cli": "^11.0.0",
|
||||
"@nestjs/schematics": "^11.0.0",
|
||||
"@nestjs/testing": "^11.0.1",
|
||||
"@types/express": "^5.0.0",
|
||||
"@types/jest": "^30.0.0",
|
||||
"@types/node": "^22.10.7",
|
||||
"@types/supertest": "^6.0.2",
|
||||
"eslint": "^9.18.0",
|
||||
"eslint-config-prettier": "^10.0.1",
|
||||
"eslint-plugin-prettier": "^5.2.2",
|
||||
"globals": "^16.0.0",
|
||||
"jest": "^30.0.0",
|
||||
"prettier": "^3.4.2",
|
||||
"source-map-support": "^0.5.21",
|
||||
"supertest": "^7.0.0",
|
||||
"ts-jest": "^29.2.5",
|
||||
"ts-loader": "^9.5.2",
|
||||
"ts-node": "^10.9.2",
|
||||
"tsconfig-paths": "^4.2.0",
|
||||
"typescript": "^5.7.3",
|
||||
"typescript-eslint": "^8.20.0"
|
||||
},
|
||||
"jest": {
|
||||
"moduleFileExtensions": [
|
||||
"js",
|
||||
"json",
|
||||
"ts"
|
||||
],
|
||||
"rootDir": "src",
|
||||
"testRegex": ".*\\.spec\\.ts$",
|
||||
"transform": {
|
||||
"^.+\\.(t|j)s$": "ts-jest"
|
||||
},
|
||||
"collectCoverageFrom": [
|
||||
"**/*.(t|j)s"
|
||||
],
|
||||
"coverageDirectory": "../coverage",
|
||||
"testEnvironment": "node"
|
||||
}
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { AppController } from './app.controller';
|
||||
import { AppService } from './app.service';
|
||||
|
||||
describe('AppController', () => {
|
||||
let appController: AppController;
|
||||
|
||||
beforeEach(async () => {
|
||||
const app: TestingModule = await Test.createTestingModule({
|
||||
controllers: [AppController],
|
||||
providers: [AppService],
|
||||
}).compile();
|
||||
|
||||
appController = app.get<AppController>(AppController);
|
||||
});
|
||||
|
||||
describe('root', () => {
|
||||
it('should return "Hello World!"', () => {
|
||||
expect(appController.getHello()).toBe('Hello World!');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,12 +0,0 @@
|
||||
import { Controller, Get } from '@nestjs/common';
|
||||
import { AppService } from './app.service';
|
||||
|
||||
@Controller()
|
||||
export class AppController {
|
||||
constructor(private readonly appService: AppService) {}
|
||||
|
||||
@Get()
|
||||
getHello(): string {
|
||||
return this.appService.getHello();
|
||||
}
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { ConfigModule } from '@nestjs/config';
|
||||
import { databaseConfig } from './config/database.config';
|
||||
import { AuthModule } from './modules/auth/auth.module';
|
||||
import { UsersModule } from './modules/users/users.module';
|
||||
import { RolesModule } from './modules/roles/roles.module';
|
||||
import { PermissionsModule } from './modules/permissions/permissions.module';
|
||||
import { CustomersModule } from './modules/customers/customers.module';
|
||||
import { SuppliersModule } from './modules/suppliers/suppliers.module';
|
||||
import { SubcontractorsModule } from './modules/subcontractors/subcontractors.module';
|
||||
import { ProjectsModule } from './modules/projects/projects.module';
|
||||
import { QuotationsModule } from './modules/quotations/quotations.module';
|
||||
import { PurchasesModule } from './modules/purchases/purchases.module';
|
||||
import { SubcontractsModule } from './modules/subcontracts/subcontracts.module';
|
||||
import { ContractsModule } from './modules/contracts/contracts.module';
|
||||
import { FinanceModule } from './modules/finance/finance.module';
|
||||
import { ProductsModule } from './modules/products/products.module';
|
||||
import { SeedService } from './database/seed';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
ConfigModule.forRoot({
|
||||
isGlobal: true,
|
||||
}),
|
||||
TypeOrmModule.forRoot(databaseConfig),
|
||||
AuthModule,
|
||||
UsersModule,
|
||||
RolesModule,
|
||||
PermissionsModule,
|
||||
CustomersModule,
|
||||
SuppliersModule,
|
||||
SubcontractorsModule,
|
||||
ProjectsModule,
|
||||
QuotationsModule,
|
||||
PurchasesModule,
|
||||
SubcontractsModule,
|
||||
ContractsModule,
|
||||
FinanceModule,
|
||||
ProductsModule,
|
||||
],
|
||||
controllers: [],
|
||||
providers: [SeedService],
|
||||
})
|
||||
export class AppModule {}
|
||||
@@ -1,8 +0,0 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
@Injectable()
|
||||
export class AppService {
|
||||
getHello(): string {
|
||||
return 'Hello World!';
|
||||
}
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
import { TypeOrmModuleOptions } from '@nestjs/typeorm';
|
||||
import * as dotenv from 'dotenv';
|
||||
import * as path from 'path';
|
||||
|
||||
dotenv.config();
|
||||
|
||||
export const databaseConfig: TypeOrmModuleOptions = {
|
||||
type: 'sqlite',
|
||||
database: path.join(__dirname, '..', '..', 'database.sqlite'),
|
||||
entities: [__dirname + '/../**/*.entity{.ts,.js}'],
|
||||
synchronize: true,
|
||||
logging: true,
|
||||
};
|
||||
@@ -1,140 +0,0 @@
|
||||
import { Injectable, OnModuleInit } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { Permission } from '../modules/permissions/permission.entity';
|
||||
import { Role } from '../modules/roles/role.entity';
|
||||
import { User } from '../modules/users/user.entity';
|
||||
import * as bcrypt from 'bcryptjs';
|
||||
|
||||
@Injectable()
|
||||
export class SeedService implements OnModuleInit {
|
||||
constructor(
|
||||
@InjectRepository(Permission) private permissionRepo: Repository<Permission>,
|
||||
@InjectRepository(Role) private roleRepo: Repository<Role>,
|
||||
@InjectRepository(User) private userRepo: Repository<User>,
|
||||
) {}
|
||||
|
||||
async onModuleInit() {
|
||||
await this.seedPermissions();
|
||||
await this.seedRoles();
|
||||
await this.seedAdminUser();
|
||||
}
|
||||
|
||||
private async seedPermissions() {
|
||||
const permissions = [
|
||||
{ name: 'user:create', description: '创建用户' },
|
||||
{ name: 'user:read', description: '读取用户' },
|
||||
{ name: 'user:update', description: '更新用户' },
|
||||
{ name: 'user:delete', description: '删除用户' },
|
||||
{ name: 'role:create', description: '创建角色' },
|
||||
{ name: 'role:read', description: '读取角色' },
|
||||
{ name: 'role:update', description: '更新角色' },
|
||||
{ name: 'role:delete', description: '删除角色' },
|
||||
{ name: 'permission:create', description: '创建权限' },
|
||||
{ name: 'permission:read', description: '读取权限' },
|
||||
{ name: 'permission:update', description: '更新权限' },
|
||||
{ name: 'permission:delete', description: '删除权限' },
|
||||
{ name: 'customer:create', description: '创建客户' },
|
||||
{ name: 'customer:read', description: '读取客户' },
|
||||
{ name: 'customer:update', description: '更新客户' },
|
||||
{ name: 'customer:delete', description: '删除客户' },
|
||||
{ name: 'supplier:create', description: '创建供应商' },
|
||||
{ name: 'supplier:read', description: '读取供应商' },
|
||||
{ name: 'supplier:update', description: '更新供应商' },
|
||||
{ name: 'supplier:delete', description: '删除供应商' },
|
||||
{ name: 'subcontractor:create', description: '创建分包商' },
|
||||
{ name: 'subcontractor:read', description: '读取分包商' },
|
||||
{ name: 'subcontractor:update', description: '更新分包商' },
|
||||
{ name: 'subcontractor:delete', description: '删除分包商' },
|
||||
{ name: 'project:create', description: '创建项目' },
|
||||
{ name: 'project:read', description: '读取项目' },
|
||||
{ name: 'project:update', description: '更新项目' },
|
||||
{ name: 'project:delete', description: '删除项目' },
|
||||
{ name: 'finance:read', description: '读取财务' },
|
||||
{ name: 'finance:update', description: '更新财务' },
|
||||
{ name: 'construction:read', description: '读取施工' },
|
||||
{ name: 'construction:update', description: '更新施工' },
|
||||
{ name: 'exchange:read', description: '读取汇率' },
|
||||
{ name: 'exchange:update', description: '更新汇率' },
|
||||
{ name: 'product:create', description: '创建商品' },
|
||||
{ name: 'product:read', description: '读取商品' },
|
||||
{ name: 'product:update', description: '更新商品' },
|
||||
{ name: 'product:delete', description: '删除商品' },
|
||||
{ name: 'quotation:create', description: '创建预算报价' },
|
||||
{ name: 'quotation:read', description: '读取预算报价' },
|
||||
{ name: 'quotation:update', description: '更新预算报价' },
|
||||
{ name: 'quotation:delete', description: '删除预算报价' },
|
||||
];
|
||||
|
||||
for (const permissionData of permissions) {
|
||||
const existing = await this.permissionRepo.findOne({ where: { name: permissionData.name } });
|
||||
if (!existing) {
|
||||
const permission = this.permissionRepo.create(permissionData);
|
||||
await this.permissionRepo.save(permission);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async seedRoles() {
|
||||
const allPermissions = await this.permissionRepo.find();
|
||||
|
||||
const roles = [
|
||||
{
|
||||
name: '超级管理员',
|
||||
description: '拥有所有权限',
|
||||
permissions: allPermissions,
|
||||
},
|
||||
{
|
||||
name: '管理员',
|
||||
description: '拥有大部分管理权限',
|
||||
permissions: allPermissions.filter(p => !p.name.includes('role:') && !p.name.includes('permission:')),
|
||||
},
|
||||
{
|
||||
name: '财务人员',
|
||||
description: '拥有财务管理权限',
|
||||
permissions: allPermissions.filter(p => p.name.includes('finance:') || p.name.includes('exchange:')),
|
||||
},
|
||||
{
|
||||
name: '项目经理',
|
||||
description: '拥有项目管理权限',
|
||||
permissions: allPermissions.filter(p => p.name.includes('project:') || p.name.includes('customer:')),
|
||||
},
|
||||
{
|
||||
name: '采购人员',
|
||||
description: '拥有采购管理权限',
|
||||
permissions: allPermissions.filter(p => p.name.includes('supplier:') || p.name.includes('product:')),
|
||||
},
|
||||
{
|
||||
name: '施工人员',
|
||||
description: '拥有施工管理权限',
|
||||
permissions: allPermissions.filter(p => p.name.includes('construction:')),
|
||||
},
|
||||
];
|
||||
|
||||
for (const roleData of roles) {
|
||||
const existing = await this.roleRepo.findOne({ where: { name: roleData.name } });
|
||||
if (!existing) {
|
||||
const role = this.roleRepo.create(roleData);
|
||||
await this.roleRepo.save(role);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async seedAdminUser() {
|
||||
const adminRole = await this.roleRepo.findOne({ where: { name: '超级管理员' } });
|
||||
if (!adminRole) return;
|
||||
|
||||
const existingAdmin = await this.userRepo.findOne({ where: { username: 'admin' } });
|
||||
if (!existingAdmin) {
|
||||
const hashedPassword = await bcrypt.hash('admin123', 10);
|
||||
const adminUser = this.userRepo.create({
|
||||
username: 'admin',
|
||||
password: hashedPassword,
|
||||
name: '系统管理员',
|
||||
email: 'admin@example.com',
|
||||
role: adminRole,
|
||||
});
|
||||
await this.userRepo.save(adminUser);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
import { NestFactory } from '@nestjs/core';
|
||||
import { AppModule } from './app.module';
|
||||
|
||||
async function bootstrap() {
|
||||
const app = await NestFactory.create(AppModule);
|
||||
app.enableCors({
|
||||
origin: ['http://localhost:5173', 'http://localhost:4173', 'http://localhost:5174'],
|
||||
methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'],
|
||||
allowedHeaders: ['Content-Type', 'Authorization'],
|
||||
});
|
||||
await app.listen(process.env.PORT ?? 3000);
|
||||
}
|
||||
bootstrap();
|
||||
@@ -1,26 +0,0 @@
|
||||
import { Controller, Post, Body } from '@nestjs/common';
|
||||
import { AuthService } from './auth.service';
|
||||
|
||||
@Controller('api/v1/auth')
|
||||
export class AuthController {
|
||||
constructor(private readonly authService: AuthService) {}
|
||||
|
||||
@Post('register')
|
||||
async register(
|
||||
@Body('username') username: string,
|
||||
@Body('password') password: string,
|
||||
@Body('name') name: string,
|
||||
@Body('email') email: string,
|
||||
@Body('roleId') roleId: string,
|
||||
) {
|
||||
return this.authService.register(username, password, name, email, roleId);
|
||||
}
|
||||
|
||||
@Post('login')
|
||||
async login(
|
||||
@Body('username') username: string,
|
||||
@Body('password') password: string,
|
||||
) {
|
||||
return this.authService.login(username, password);
|
||||
}
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { JwtModule } from '@nestjs/jwt';
|
||||
import { PassportModule } from '@nestjs/passport';
|
||||
import { AuthService } from './auth.service';
|
||||
import { AuthController } from './auth.controller';
|
||||
import { JwtStrategy } from './jwt.strategy';
|
||||
import { UsersModule } from '../users/users.module';
|
||||
import * as dotenv from 'dotenv';
|
||||
|
||||
dotenv.config();
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
UsersModule,
|
||||
PassportModule,
|
||||
JwtModule.register({
|
||||
secret: process.env.JWT_SECRET || 'your-secret-key-here',
|
||||
signOptions: {
|
||||
expiresIn: '3600s',
|
||||
},
|
||||
}),
|
||||
],
|
||||
providers: [AuthService, JwtStrategy],
|
||||
controllers: [AuthController],
|
||||
exports: [AuthService],
|
||||
})
|
||||
export class AuthModule {}
|
||||
@@ -1,74 +0,0 @@
|
||||
import { Injectable, UnauthorizedException } from '@nestjs/common';
|
||||
import { JwtService } from '@nestjs/jwt';
|
||||
import { UsersService } from '../users/users.service';
|
||||
import * as bcrypt from 'bcryptjs';
|
||||
|
||||
@Injectable()
|
||||
export class AuthService {
|
||||
constructor(
|
||||
private readonly usersService: UsersService,
|
||||
private readonly jwtService: JwtService,
|
||||
) {}
|
||||
|
||||
async register(username: string, password: string, name: string, email: string, roleId: string) {
|
||||
// 检查用户名是否已存在
|
||||
const existingUser = await this.usersService.findByUsername(username);
|
||||
if (existingUser) {
|
||||
throw new Error('Username already exists');
|
||||
}
|
||||
|
||||
// 检查邮箱是否已存在
|
||||
const existingEmail = await this.usersService.findByEmail(email);
|
||||
if (existingEmail) {
|
||||
throw new Error('Email already exists');
|
||||
}
|
||||
|
||||
// 哈希密码
|
||||
const hashedPassword = await bcrypt.hash(password, 10);
|
||||
|
||||
// 创建用户
|
||||
const user = await this.usersService.create({
|
||||
username,
|
||||
password: hashedPassword,
|
||||
name,
|
||||
email,
|
||||
roleId,
|
||||
});
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
async login(username: string, password: string) {
|
||||
// 查找用户
|
||||
console.log('Login attempt for user:', username);
|
||||
const user = await this.usersService.findByUsername(username);
|
||||
console.log('Found user:', user);
|
||||
if (!user) {
|
||||
throw new UnauthorizedException('Invalid credentials');
|
||||
}
|
||||
|
||||
// 验证密码
|
||||
const isPasswordValid = await bcrypt.compare(password, user.password);
|
||||
console.log('Password valid:', isPasswordValid);
|
||||
if (!isPasswordValid) {
|
||||
throw new UnauthorizedException('Invalid credentials');
|
||||
}
|
||||
|
||||
// 生成JWT token
|
||||
const payload = { sub: user.id, username: user.username, role: user.role.name };
|
||||
const token = this.jwtService.sign(payload);
|
||||
|
||||
const response = {
|
||||
access_token: token,
|
||||
user: {
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
name: user.name,
|
||||
email: user.email,
|
||||
role: user.role.name,
|
||||
},
|
||||
};
|
||||
console.log('Login response:', response);
|
||||
return response;
|
||||
}
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
import { Injectable, UnauthorizedException } from '@nestjs/common';
|
||||
import { PassportStrategy } from '@nestjs/passport';
|
||||
import { ExtractJwt, Strategy } from 'passport-jwt';
|
||||
import { UsersService } from '../users/users.service';
|
||||
import * as dotenv from 'dotenv';
|
||||
|
||||
dotenv.config();
|
||||
|
||||
@Injectable()
|
||||
export class JwtStrategy extends PassportStrategy(Strategy) {
|
||||
constructor(private readonly usersService: UsersService) {
|
||||
super({
|
||||
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
|
||||
secretOrKey: process.env.JWT_SECRET || 'your-secret-key-here',
|
||||
ignoreExpiration: false,
|
||||
});
|
||||
}
|
||||
|
||||
async validate(payload: any) {
|
||||
const user = await this.usersService.findById(payload.sub);
|
||||
if (!user) {
|
||||
throw new UnauthorizedException();
|
||||
}
|
||||
return user;
|
||||
}
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
import { Entity, Column, PrimaryGeneratedColumn, CreateDateColumn, UpdateDateColumn, ManyToOne, OneToMany } from 'typeorm';
|
||||
import { Project } from '../projects/project.entity';
|
||||
import { PaymentTerm } from './payment-term.entity';
|
||||
import { Purchase } from '../purchases/purchase.entity';
|
||||
import { Subcontract } from '../subcontracts/subcontract.entity';
|
||||
|
||||
@Entity('contracts')
|
||||
export class Contract {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id: string;
|
||||
|
||||
@Column({ type: 'uuid', nullable: true })
|
||||
project_id: string;
|
||||
|
||||
@ManyToOne(() => Project, project => project.contracts)
|
||||
project: Project;
|
||||
|
||||
@Column({ type: 'varchar', length: 50, nullable: false })
|
||||
type: string;
|
||||
|
||||
@Column({ type: 'varchar', length: 100, unique: true, nullable: false })
|
||||
contract_number: string;
|
||||
|
||||
@Column({ type: 'decimal', precision: 18, scale: 2, nullable: false })
|
||||
contract_amount: number;
|
||||
|
||||
@Column({ type: 'varchar', length: 10, nullable: false })
|
||||
currency: string;
|
||||
|
||||
@Column({ type: 'decimal', precision: 18, scale: 2, nullable: false })
|
||||
rmb_equivalent: number;
|
||||
|
||||
@Column({ type: 'date', nullable: true })
|
||||
start_date: Date;
|
||||
|
||||
@Column({ type: 'date', nullable: true })
|
||||
end_date: Date;
|
||||
|
||||
@Column({ type: 'varchar', length: 50, nullable: false })
|
||||
status: string;
|
||||
|
||||
@CreateDateColumn()
|
||||
created_at: Date;
|
||||
|
||||
@UpdateDateColumn()
|
||||
updated_at: Date;
|
||||
|
||||
@OneToMany(() => PaymentTerm, paymentTerm => paymentTerm.contract)
|
||||
payment_terms: PaymentTerm[];
|
||||
|
||||
@OneToMany(() => Purchase, purchase => purchase.contract)
|
||||
purchases: Purchase[];
|
||||
|
||||
@OneToMany(() => Subcontract, subcontract => subcontract.contract)
|
||||
subcontracts: Subcontract[];
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { Contract } from './contract.entity';
|
||||
import { PaymentTerm } from './payment-term.entity';
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([Contract, PaymentTerm])],
|
||||
exports: [TypeOrmModule],
|
||||
})
|
||||
export class ContractsModule {}
|
||||
@@ -1,48 +0,0 @@
|
||||
import { Entity, Column, PrimaryGeneratedColumn, CreateDateColumn, UpdateDateColumn, ManyToOne, OneToMany } from 'typeorm';
|
||||
import { Contract } from './contract.entity';
|
||||
import { FinancialRecord } from '../finance/financial-record.entity';
|
||||
|
||||
@Entity('payment_terms')
|
||||
export class PaymentTerm {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id: string;
|
||||
|
||||
@Column({ type: 'uuid', nullable: true })
|
||||
contract_id: string;
|
||||
|
||||
@ManyToOne(() => Contract, contract => contract.payment_terms)
|
||||
contract: Contract;
|
||||
|
||||
@Column({ type: 'integer', nullable: false })
|
||||
sequence: number;
|
||||
|
||||
@Column({ type: 'text', nullable: false })
|
||||
description: string;
|
||||
|
||||
@Column({ type: 'decimal', precision: 5, scale: 2, nullable: false })
|
||||
percentage: number;
|
||||
|
||||
@Column({ type: 'decimal', precision: 18, scale: 2, nullable: false })
|
||||
amount: number;
|
||||
|
||||
@Column({ type: 'varchar', length: 10, nullable: false })
|
||||
currency: string;
|
||||
|
||||
@Column({ type: 'decimal', precision: 18, scale: 2, nullable: false })
|
||||
rmb_equivalent: number;
|
||||
|
||||
@Column({ type: 'date', nullable: true })
|
||||
due_date: Date;
|
||||
|
||||
@Column({ type: 'varchar', length: 50, nullable: false })
|
||||
status: string;
|
||||
|
||||
@CreateDateColumn()
|
||||
created_at: Date;
|
||||
|
||||
@UpdateDateColumn()
|
||||
updated_at: Date;
|
||||
|
||||
@OneToMany(() => FinancialRecord, financialRecord => financialRecord.payment_term)
|
||||
financial_records: FinancialRecord[];
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
import { Entity, Column, PrimaryGeneratedColumn, CreateDateColumn, UpdateDateColumn, OneToMany } from 'typeorm';
|
||||
import { Project } from '../projects/project.entity';
|
||||
import { Quotation } from '../quotations/quotation.entity';
|
||||
|
||||
@Entity('customers')
|
||||
export class Customer {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id: string;
|
||||
|
||||
@Column({ type: 'varchar', length: 255, nullable: false })
|
||||
name: string;
|
||||
|
||||
@Column({ type: 'varchar', length: 255, nullable: true })
|
||||
contact_person: string;
|
||||
|
||||
@Column({ type: 'varchar', length: 50, nullable: true })
|
||||
phone: string;
|
||||
|
||||
@Column({ type: 'varchar', length: 255, nullable: true })
|
||||
email: string;
|
||||
|
||||
@Column({ type: 'text', nullable: true })
|
||||
address: string;
|
||||
|
||||
@CreateDateColumn()
|
||||
created_at: Date;
|
||||
|
||||
@UpdateDateColumn()
|
||||
updated_at: Date;
|
||||
|
||||
@OneToMany(() => Project, project => project.customer)
|
||||
projects: Project[];
|
||||
|
||||
@OneToMany(() => Quotation, quotation => quotation.customer)
|
||||
quotations: Quotation[];
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
import { Controller, Get, Post, Body, Patch, Param, Delete } from '@nestjs/common';
|
||||
import { CustomersService } from './customers.service';
|
||||
import { Customer } from './customer.entity';
|
||||
|
||||
@Controller('api/v1/customers')
|
||||
export class CustomersController {
|
||||
constructor(private readonly customersService: CustomersService) {}
|
||||
|
||||
@Post()
|
||||
create(@Body() customerData: Partial<Customer>) {
|
||||
return this.customersService.create(customerData);
|
||||
}
|
||||
|
||||
@Get()
|
||||
findAll() {
|
||||
return this.customersService.findAll();
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
findOne(@Param('id') id: string) {
|
||||
return this.customersService.findOne(id);
|
||||
}
|
||||
|
||||
@Get(':id/projects')
|
||||
findWithProjects(@Param('id') id: string) {
|
||||
return this.customersService.findWithProjects(id);
|
||||
}
|
||||
|
||||
@Get(':id/quotations')
|
||||
findWithQuotations(@Param('id') id: string) {
|
||||
return this.customersService.findWithQuotations(id);
|
||||
}
|
||||
|
||||
@Patch(':id')
|
||||
update(@Param('id') id: string, @Body() customerData: Partial<Customer>) {
|
||||
return this.customersService.update(id, customerData);
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
remove(@Param('id') id: string) {
|
||||
return this.customersService.remove(id);
|
||||
}
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { Customer } from './customer.entity';
|
||||
import { CustomersController } from './customers.controller';
|
||||
import { CustomersService } from './customers.service';
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([Customer])],
|
||||
controllers: [CustomersController],
|
||||
providers: [CustomersService],
|
||||
exports: [CustomersService],
|
||||
})
|
||||
export class CustomersModule {}
|
||||
@@ -1,64 +0,0 @@
|
||||
import { Injectable, NotFoundException } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { Customer } from './customer.entity';
|
||||
|
||||
@Injectable()
|
||||
export class CustomersService {
|
||||
constructor(
|
||||
@InjectRepository(Customer)
|
||||
private customersRepository: Repository<Customer>,
|
||||
) {}
|
||||
|
||||
async create(customerData: Partial<Customer>): Promise<Customer> {
|
||||
const customer = this.customersRepository.create(customerData);
|
||||
return this.customersRepository.save(customer);
|
||||
}
|
||||
|
||||
async findAll(): Promise<Customer[]> {
|
||||
return this.customersRepository.find();
|
||||
}
|
||||
|
||||
async findOne(id: string): Promise<Customer> {
|
||||
const customer = await this.customersRepository.findOne({ where: { id } });
|
||||
if (!customer) {
|
||||
throw new NotFoundException(`Customer with id ${id} not found`);
|
||||
}
|
||||
return customer;
|
||||
}
|
||||
|
||||
async update(id: string, customerData: Partial<Customer>): Promise<Customer> {
|
||||
const customer = await this.findOne(id);
|
||||
Object.assign(customer, customerData);
|
||||
return this.customersRepository.save(customer);
|
||||
}
|
||||
|
||||
async remove(id: string): Promise<void> {
|
||||
const result = await this.customersRepository.delete(id);
|
||||
if (result.affected === 0) {
|
||||
throw new NotFoundException(`Customer with id ${id} not found`);
|
||||
}
|
||||
}
|
||||
|
||||
async findWithProjects(id: string): Promise<Customer> {
|
||||
const customer = await this.customersRepository.findOne({
|
||||
where: { id },
|
||||
relations: ['projects'],
|
||||
});
|
||||
if (!customer) {
|
||||
throw new NotFoundException(`Customer with id ${id} not found`);
|
||||
}
|
||||
return customer;
|
||||
}
|
||||
|
||||
async findWithQuotations(id: string): Promise<Customer> {
|
||||
const customer = await this.customersRepository.findOne({
|
||||
where: { id },
|
||||
relations: ['quotations'],
|
||||
});
|
||||
if (!customer) {
|
||||
throw new NotFoundException(`Customer with id ${id} not found`);
|
||||
}
|
||||
return customer;
|
||||
}
|
||||
}
|
||||
@@ -1,69 +0,0 @@
|
||||
import { Entity, Column, PrimaryGeneratedColumn, CreateDateColumn, UpdateDateColumn, ManyToOne } from 'typeorm';
|
||||
import { User } from '../users/user.entity';
|
||||
import { Project } from '../projects/project.entity';
|
||||
|
||||
@Entity('advance_payments')
|
||||
export class AdvancePayment {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id: string;
|
||||
|
||||
@Column({ type: 'uuid', nullable: false })
|
||||
user_id: string;
|
||||
|
||||
@ManyToOne(() => User)
|
||||
user: User;
|
||||
|
||||
@Column({ type: 'varchar', length: 20, nullable: false })
|
||||
expense_type: string; // company or project
|
||||
|
||||
@Column({ type: 'uuid', nullable: true })
|
||||
project_id: string;
|
||||
|
||||
@ManyToOne(() => Project, project => project.advance_payments)
|
||||
project: Project;
|
||||
|
||||
@Column({ type: 'decimal', precision: 18, scale: 2, nullable: false })
|
||||
amount: number;
|
||||
|
||||
@Column({ type: 'varchar', length: 10, nullable: false })
|
||||
currency: string;
|
||||
|
||||
@Column({ type: 'decimal', precision: 18, scale: 2, nullable: false })
|
||||
rmb_equivalent: number;
|
||||
|
||||
@Column({ type: 'varchar', length: 255, nullable: false })
|
||||
purpose: string;
|
||||
|
||||
@Column({ type: 'varchar', length: 50, nullable: false })
|
||||
status: string;
|
||||
|
||||
@Column({ type: 'uuid', nullable: true })
|
||||
approver_id: string;
|
||||
|
||||
@ManyToOne(() => User)
|
||||
approver: User;
|
||||
|
||||
@Column({ type: 'text', nullable: true })
|
||||
description: string;
|
||||
|
||||
@Column({ type: 'datetime', nullable: true })
|
||||
submitted_at: Date;
|
||||
|
||||
@Column({ type: 'datetime', nullable: true })
|
||||
approved_at: Date;
|
||||
|
||||
@Column({ type: 'datetime', nullable: true })
|
||||
rejected_at: Date;
|
||||
|
||||
@Column({ type: 'decimal', precision: 18, scale: 2, default: 0 })
|
||||
used_amount: number;
|
||||
|
||||
@Column({ type: 'decimal', precision: 18, scale: 2, default: 0 })
|
||||
remaining_amount: number;
|
||||
|
||||
@CreateDateColumn()
|
||||
created_at: Date;
|
||||
|
||||
@UpdateDateColumn()
|
||||
updated_at: Date;
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
import { Entity, Column, PrimaryGeneratedColumn, CreateDateColumn, UpdateDateColumn, OneToMany } from 'typeorm';
|
||||
import { FinancialTransaction } from './financial-transaction.entity';
|
||||
|
||||
@Entity('bank_accounts')
|
||||
export class BankAccount {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id: string;
|
||||
|
||||
@Column({ type: 'varchar', length: 255, nullable: false })
|
||||
name: string;
|
||||
|
||||
@Column({ type: 'varchar', length: 255, nullable: false })
|
||||
bank_name: string;
|
||||
|
||||
@Column({ type: 'varchar', length: 100, unique: true, nullable: false })
|
||||
account_number: string;
|
||||
|
||||
@Column({ type: 'varchar', length: 10, nullable: false })
|
||||
currency: string;
|
||||
|
||||
@Column({ type: 'decimal', precision: 18, scale: 2, default: 0 })
|
||||
balance: number;
|
||||
|
||||
@CreateDateColumn()
|
||||
created_at: Date;
|
||||
|
||||
@UpdateDateColumn()
|
||||
updated_at: Date;
|
||||
|
||||
@OneToMany(() => FinancialTransaction, financialTransaction => financialTransaction.bank_account)
|
||||
financial_transactions: FinancialTransaction[];
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn } from 'typeorm';
|
||||
|
||||
@Entity()
|
||||
export class CurrencyRate {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id: string;
|
||||
|
||||
@Column({ type: 'varchar', length: 10, nullable: false }) // 源币种代码,如 CNY, LAK, THB, USD
|
||||
from_currency: string;
|
||||
|
||||
@Column({ type: 'varchar', length: 10, nullable: false }) // 目标币种代码
|
||||
to_currency: string;
|
||||
|
||||
@Column({ type: 'decimal', precision: 12, scale: 6, nullable: false }) // 汇率值
|
||||
rate: number;
|
||||
|
||||
@Column({ type: 'boolean', default: true }) // 是否启用
|
||||
is_active: boolean;
|
||||
|
||||
@CreateDateColumn()
|
||||
created_at: Date;
|
||||
|
||||
@UpdateDateColumn()
|
||||
updated_at: Date;
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
import { Entity, Column, PrimaryGeneratedColumn, ManyToOne } from 'typeorm';
|
||||
import { ExpenseClaim } from './expense-claim.entity';
|
||||
|
||||
@Entity('expense_claim_details')
|
||||
export class ExpenseClaimDetail {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id: string;
|
||||
|
||||
@Column({ type: 'uuid', nullable: false })
|
||||
expense_claim_id: string;
|
||||
|
||||
@ManyToOne(() => ExpenseClaim, expense_claim => expense_claim.details)
|
||||
expense_claim: ExpenseClaim;
|
||||
|
||||
@Column({ type: 'varchar', length: 50, nullable: false })
|
||||
category: string; // 支出分类
|
||||
|
||||
@Column({ type: 'decimal', precision: 18, scale: 2, nullable: false })
|
||||
amount: number;
|
||||
|
||||
@Column({ type: 'varchar', length: 10, nullable: false })
|
||||
currency: string;
|
||||
|
||||
@Column({ type: 'decimal', precision: 18, scale: 2, nullable: false })
|
||||
rmb_equivalent: number;
|
||||
|
||||
@Column({ type: 'varchar', length: 255, nullable: true })
|
||||
description: string;
|
||||
|
||||
@Column({ type: 'date', nullable: false })
|
||||
expense_date: Date;
|
||||
|
||||
@Column({ type: 'text', nullable: true })
|
||||
attachments: string; // JSON格式存储附件信息
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
import { Entity, Column, PrimaryGeneratedColumn, CreateDateColumn, UpdateDateColumn, ManyToOne, OneToMany } from 'typeorm';
|
||||
import { User } from '../users/user.entity';
|
||||
import { Project } from '../projects/project.entity';
|
||||
import { ExpenseClaimDetail } from './expense-claim-detail.entity';
|
||||
|
||||
@Entity('expense_claims')
|
||||
export class ExpenseClaim {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id: string;
|
||||
|
||||
@Column({ type: 'uuid', nullable: false })
|
||||
user_id: string;
|
||||
|
||||
@ManyToOne(() => User)
|
||||
user: User;
|
||||
|
||||
@Column({ type: 'varchar', length: 20, nullable: false })
|
||||
expense_type: string; // company or project
|
||||
|
||||
@Column({ type: 'uuid', nullable: true })
|
||||
project_id: string;
|
||||
|
||||
@ManyToOne(() => Project, project => project.expense_claims)
|
||||
project: Project;
|
||||
|
||||
@Column({ type: 'decimal', precision: 18, scale: 2, nullable: false })
|
||||
amount: number;
|
||||
|
||||
@Column({ type: 'varchar', length: 10, nullable: false })
|
||||
currency: string;
|
||||
|
||||
@Column({ type: 'decimal', precision: 18, scale: 2, nullable: false })
|
||||
rmb_equivalent: number;
|
||||
|
||||
@Column({ type: 'varchar', length: 255, nullable: false })
|
||||
purpose: string;
|
||||
|
||||
@Column({ type: 'varchar', length: 50, nullable: false })
|
||||
status: string;
|
||||
|
||||
@Column({ type: 'uuid', nullable: true })
|
||||
approver_id: string;
|
||||
|
||||
@ManyToOne(() => User)
|
||||
approver: User;
|
||||
|
||||
@Column({ type: 'text', nullable: true })
|
||||
description: string;
|
||||
|
||||
@Column({ type: 'datetime', nullable: true })
|
||||
submitted_at: Date;
|
||||
|
||||
@Column({ type: 'datetime', nullable: true })
|
||||
approved_at: Date;
|
||||
|
||||
@Column({ type: 'datetime', nullable: true })
|
||||
rejected_at: Date;
|
||||
|
||||
@OneToMany(() => ExpenseClaimDetail, detail => detail.expense_claim, { cascade: true })
|
||||
details: ExpenseClaimDetail[];
|
||||
|
||||
@CreateDateColumn()
|
||||
created_at: Date;
|
||||
|
||||
@UpdateDateColumn()
|
||||
updated_at: Date;
|
||||
}
|
||||
@@ -1,68 +0,0 @@
|
||||
import { Entity, Column, PrimaryGeneratedColumn, CreateDateColumn, UpdateDateColumn, ManyToOne } from 'typeorm';
|
||||
import { User } from '../users/user.entity';
|
||||
import { ExpenseClaim } from './expense-claim.entity';
|
||||
import { AdvancePayment } from './advance-payment.entity';
|
||||
import { FinancialRecord } from './financial-record.entity';
|
||||
|
||||
@Entity('finance_approvals')
|
||||
export class FinanceApproval {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id: string;
|
||||
|
||||
@Column({ type: 'varchar', length: 50, nullable: false })
|
||||
approval_type: string;
|
||||
|
||||
@Column({ type: 'uuid', nullable: true })
|
||||
expense_claim_id: string;
|
||||
|
||||
@ManyToOne(() => ExpenseClaim)
|
||||
expense_claim: ExpenseClaim;
|
||||
|
||||
@Column({ type: 'uuid', nullable: true })
|
||||
advance_payment_id: string;
|
||||
|
||||
@ManyToOne(() => AdvancePayment)
|
||||
advance_payment: AdvancePayment;
|
||||
|
||||
@Column({ type: 'uuid', nullable: true })
|
||||
financial_record_id: string;
|
||||
|
||||
@ManyToOne(() => FinancialRecord)
|
||||
financial_record: FinancialRecord;
|
||||
|
||||
@Column({ type: 'uuid', nullable: false })
|
||||
requester_id: string;
|
||||
|
||||
@ManyToOne(() => User)
|
||||
requester: User;
|
||||
|
||||
@Column({ type: 'uuid', nullable: true })
|
||||
approver_id: string;
|
||||
|
||||
@ManyToOne(() => User)
|
||||
approver: User;
|
||||
|
||||
@Column({ type: 'varchar', length: 50, nullable: false })
|
||||
status: string;
|
||||
|
||||
@Column({ type: 'text', nullable: true })
|
||||
request_notes: string;
|
||||
|
||||
@Column({ type: 'text', nullable: true })
|
||||
approval_notes: string;
|
||||
|
||||
@Column({ type: 'datetime', nullable: true })
|
||||
submitted_at: Date;
|
||||
|
||||
@Column({ type: 'datetime', nullable: true })
|
||||
approved_at: Date;
|
||||
|
||||
@Column({ type: 'datetime', nullable: true })
|
||||
rejected_at: Date;
|
||||
|
||||
@CreateDateColumn()
|
||||
created_at: Date;
|
||||
|
||||
@UpdateDateColumn()
|
||||
updated_at: Date;
|
||||
}
|
||||
@@ -1,238 +0,0 @@
|
||||
import { Controller, Get, Post, Put, Delete, Body, Param, Query, UseGuards, Request } from '@nestjs/common';
|
||||
import { FinanceService } from './finance.service';
|
||||
import { AuthGuard } from '@nestjs/passport';
|
||||
|
||||
@Controller('api/v1/finance')
|
||||
export class FinanceController {
|
||||
constructor(private financeService: FinanceService) {}
|
||||
|
||||
// 报销申请相关接口
|
||||
@UseGuards(AuthGuard('jwt'))
|
||||
@Post('expense-claims')
|
||||
async createExpenseClaim(@Body() data: any, @Request() req) {
|
||||
data.user_id = req.user.id;
|
||||
return this.financeService.createExpenseClaim(data);
|
||||
}
|
||||
|
||||
@UseGuards(AuthGuard('jwt'))
|
||||
@Get('expense-claims')
|
||||
async getExpenseClaims(@Query() filter: any, @Request() req) {
|
||||
// 普通用户只能查看自己的报销申请
|
||||
if (req.user.role.name !== 'admin') {
|
||||
filter.user_id = req.user.id;
|
||||
}
|
||||
return this.financeService.getExpenseClaims(filter);
|
||||
}
|
||||
|
||||
@UseGuards(AuthGuard('jwt'))
|
||||
@Get('expense-claims/:id')
|
||||
async getExpenseClaimById(@Param('id') id: string, @Request() req) {
|
||||
const expenseClaim = await this.financeService.getExpenseClaimById(id);
|
||||
// 普通用户只能查看自己的报销申请
|
||||
if (req.user.role.name !== 'admin' && expenseClaim.user_id !== req.user.id) {
|
||||
throw new Error('Access denied');
|
||||
}
|
||||
return expenseClaim;
|
||||
}
|
||||
|
||||
@UseGuards(AuthGuard('jwt'))
|
||||
@Put('expense-claims/:id/approve')
|
||||
async approveExpenseClaim(@Param('id') id: string, @Body() data: any, @Request() req) {
|
||||
// 只有管理员可以审批
|
||||
if (req.user.role.name !== 'admin') {
|
||||
throw new Error('Access denied');
|
||||
}
|
||||
return this.financeService.approveExpenseClaim(id, req.user.id, data.notes);
|
||||
}
|
||||
|
||||
@UseGuards(AuthGuard('jwt'))
|
||||
@Put('expense-claims/:id/reject')
|
||||
async rejectExpenseClaim(@Param('id') id: string, @Body() data: any, @Request() req) {
|
||||
// 只有管理员可以审批
|
||||
if (req.user.role.name !== 'admin') {
|
||||
throw new Error('Access denied');
|
||||
}
|
||||
return this.financeService.rejectExpenseClaim(id, req.user.id, data.notes);
|
||||
}
|
||||
|
||||
// 预支款相关接口
|
||||
@UseGuards(AuthGuard('jwt'))
|
||||
@Post('advance-payments')
|
||||
async createAdvancePayment(@Body() data: any, @Request() req) {
|
||||
data.user_id = req.user.id;
|
||||
return this.financeService.createAdvancePayment(data);
|
||||
}
|
||||
|
||||
@UseGuards(AuthGuard('jwt'))
|
||||
@Get('advance-payments')
|
||||
async getAdvancePayments(@Query() filter: any, @Request() req) {
|
||||
// 普通用户只能查看自己的预支款
|
||||
if (req.user.role.name !== 'admin') {
|
||||
filter.user_id = req.user.id;
|
||||
}
|
||||
return this.financeService.getAdvancePayments(filter);
|
||||
}
|
||||
|
||||
@UseGuards(AuthGuard('jwt'))
|
||||
@Get('advance-payments/:id')
|
||||
async getAdvancePaymentById(@Param('id') id: string, @Request() req) {
|
||||
const advancePayment = await this.financeService.getAdvancePaymentById(id);
|
||||
// 普通用户只能查看自己的预支款
|
||||
if (req.user.role.name !== 'admin' && advancePayment.user_id !== req.user.id) {
|
||||
throw new Error('Access denied');
|
||||
}
|
||||
return advancePayment;
|
||||
}
|
||||
|
||||
@UseGuards(AuthGuard('jwt'))
|
||||
@Put('advance-payments/:id/approve')
|
||||
async approveAdvancePayment(@Param('id') id: string, @Body() data: any, @Request() req) {
|
||||
// 只有管理员可以审批
|
||||
if (req.user.role.name !== 'admin') {
|
||||
throw new Error('Access denied');
|
||||
}
|
||||
return this.financeService.approveAdvancePayment(id, req.user.id, data.notes);
|
||||
}
|
||||
|
||||
@UseGuards(AuthGuard('jwt'))
|
||||
@Put('advance-payments/:id/reject')
|
||||
async rejectAdvancePayment(@Param('id') id: string, @Body() data: any, @Request() req) {
|
||||
// 只有管理员可以审批
|
||||
if (req.user.role.name !== 'admin') {
|
||||
throw new Error('Access denied');
|
||||
}
|
||||
return this.financeService.rejectAdvancePayment(id, req.user.id, data.notes);
|
||||
}
|
||||
|
||||
@UseGuards(AuthGuard('jwt'))
|
||||
@Put('advance-payments/:id/usage')
|
||||
async updateAdvancePaymentUsage(@Param('id') id: string, @Body() data: any, @Request() req) {
|
||||
const advancePayment = await this.financeService.getAdvancePaymentById(id);
|
||||
// 普通用户只能更新自己的预支款使用情况
|
||||
if (req.user.role.name !== 'admin' && advancePayment.user_id !== req.user.id) {
|
||||
throw new Error('Access denied');
|
||||
}
|
||||
return this.financeService.updateAdvancePaymentUsage(id, data.used_amount);
|
||||
}
|
||||
|
||||
// 凭证相关接口
|
||||
@UseGuards(AuthGuard('jwt'))
|
||||
@Post('vouchers')
|
||||
async uploadVoucher(@Body() data: any, @Request() req) {
|
||||
data.uploaded_by = req.user.id;
|
||||
return this.financeService.uploadVoucher(data);
|
||||
}
|
||||
|
||||
@UseGuards(AuthGuard('jwt'))
|
||||
@Get('vouchers')
|
||||
async getVouchers(@Query() filter: any) {
|
||||
return this.financeService.getVouchers(filter);
|
||||
}
|
||||
|
||||
// 财务审批相关接口
|
||||
@UseGuards(AuthGuard('jwt'))
|
||||
@Post('approvals')
|
||||
async createFinanceApproval(@Body() data: any, @Request() req) {
|
||||
data.requester_id = req.user.id;
|
||||
return this.financeService.createFinanceApproval(data);
|
||||
}
|
||||
|
||||
@UseGuards(AuthGuard('jwt'))
|
||||
@Get('approvals')
|
||||
async getFinanceApprovals(@Query() filter: any, @Request() req) {
|
||||
// 普通用户只能查看自己的审批请求
|
||||
if (req.user.role.name !== 'admin') {
|
||||
filter.requester_id = req.user.id;
|
||||
}
|
||||
return this.financeService.getFinanceApprovals(filter);
|
||||
}
|
||||
|
||||
@UseGuards(AuthGuard('jwt'))
|
||||
@Put('approvals/:id/approve')
|
||||
async approveFinanceApproval(@Param('id') id: string, @Body() data: any, @Request() req) {
|
||||
// 只有管理员可以审批
|
||||
if (req.user.role.name !== 'admin') {
|
||||
throw new Error('Access denied');
|
||||
}
|
||||
return this.financeService.approveFinanceApproval(id, req.user.id, data.notes);
|
||||
}
|
||||
|
||||
@UseGuards(AuthGuard('jwt'))
|
||||
@Put('approvals/:id/reject')
|
||||
async rejectFinanceApproval(@Param('id') id: string, @Body() data: any, @Request() req) {
|
||||
// 只有管理员可以审批
|
||||
if (req.user.role.name !== 'admin') {
|
||||
throw new Error('Access denied');
|
||||
}
|
||||
return this.financeService.rejectFinanceApproval(id, req.user.id, data.notes);
|
||||
}
|
||||
|
||||
// 公司账目管理接口
|
||||
@UseGuards(AuthGuard('jwt'))
|
||||
@Get('company-records')
|
||||
async getCompanyFinancialRecords(@Query() filter: any, @Request() req) {
|
||||
// 只有管理员可以查看公司账目
|
||||
if (req.user.role.name !== 'admin') {
|
||||
throw new Error('Access denied');
|
||||
}
|
||||
return this.financeService.getCompanyFinancialRecords(filter);
|
||||
}
|
||||
|
||||
// 项目财务记录接口
|
||||
@UseGuards(AuthGuard('jwt'))
|
||||
@Get('project-records/:projectId')
|
||||
async getProjectFinancialRecords(@Param('projectId') projectId: string) {
|
||||
return this.financeService.getProjectFinancialRecords(projectId);
|
||||
}
|
||||
|
||||
// 员工个人记账接口
|
||||
@UseGuards(AuthGuard('jwt'))
|
||||
@Get('employee-records')
|
||||
async getEmployeeFinancialRecords(@Request() req) {
|
||||
return this.financeService.getEmployeeFinancialRecords(req.user.id);
|
||||
}
|
||||
|
||||
// 汇率管理相关接口
|
||||
@UseGuards(AuthGuard('jwt'))
|
||||
@Get('currency-rates')
|
||||
async getCurrencyRates(@Request() req) {
|
||||
// 只有管理员可以查看汇率设置
|
||||
if (req.user.role.name !== 'admin') {
|
||||
throw new Error('Access denied');
|
||||
}
|
||||
return this.financeService.getCurrencyRates();
|
||||
}
|
||||
|
||||
@UseGuards(AuthGuard('jwt'))
|
||||
@Post('currency-rates')
|
||||
async createOrUpdateCurrencyRate(@Body() data: any, @Request() req) {
|
||||
// 只有管理员可以设置汇率
|
||||
if (req.user.role.name !== 'admin') {
|
||||
throw new Error('Access denied');
|
||||
}
|
||||
return this.financeService.createOrUpdateCurrencyRate(data);
|
||||
}
|
||||
|
||||
@UseGuards(AuthGuard('jwt'))
|
||||
@Put('currency-rates/:id/status')
|
||||
async updateCurrencyRateStatus(@Param('id') id: string, @Body() data: any, @Request() req) {
|
||||
// 只有管理员可以更新汇率状态
|
||||
if (req.user.role.name !== 'admin') {
|
||||
throw new Error('Access denied');
|
||||
}
|
||||
return this.financeService.updateCurrencyRateStatus(id, data.is_active);
|
||||
}
|
||||
|
||||
@UseGuards(AuthGuard('jwt'))
|
||||
@Get('currencies')
|
||||
async getSupportedCurrencies() {
|
||||
return this.financeService.getSupportedCurrencies();
|
||||
}
|
||||
|
||||
@UseGuards(AuthGuard('jwt'))
|
||||
@Post('convert-currency')
|
||||
async convertCurrency(@Body() data: any) {
|
||||
const { amount, from_currency, to_currency } = data;
|
||||
return this.financeService.convertCurrency(amount, from_currency, to_currency);
|
||||
}
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { FinancialRecord } from './financial-record.entity';
|
||||
import { FinancialTransaction } from './financial-transaction.entity';
|
||||
import { BankAccount } from './bank-account.entity';
|
||||
import { ExpenseClaim } from './expense-claim.entity';
|
||||
import { AdvancePayment } from './advance-payment.entity';
|
||||
import { Voucher } from './voucher.entity';
|
||||
import { FinanceApproval } from './finance-approval.entity';
|
||||
import { CurrencyRate } from './currency-rate.entity';
|
||||
import { FinanceController } from './finance.controller';
|
||||
import { FinanceService } from './finance.service';
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([
|
||||
FinancialRecord,
|
||||
FinancialTransaction,
|
||||
BankAccount,
|
||||
ExpenseClaim,
|
||||
AdvancePayment,
|
||||
Voucher,
|
||||
FinanceApproval,
|
||||
CurrencyRate
|
||||
])],
|
||||
controllers: [FinanceController],
|
||||
providers: [FinanceService],
|
||||
exports: [TypeOrmModule, FinanceService],
|
||||
})
|
||||
export class FinanceModule {}
|
||||
@@ -1,228 +0,0 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { FinanceService } from './finance.service';
|
||||
import { getRepositoryToken } from '@nestjs/typeorm';
|
||||
import { ExpenseClaim } from './expense-claim.entity';
|
||||
import { AdvancePayment } from './advance-payment.entity';
|
||||
import { Voucher } from './voucher.entity';
|
||||
import { FinanceApproval } from './finance-approval.entity';
|
||||
import { FinancialRecord } from './financial-record.entity';
|
||||
import { BankAccount } from './bank-account.entity';
|
||||
import { FinancialTransaction } from './financial-transaction.entity';
|
||||
import { CurrencyRate } from './currency-rate.entity';
|
||||
|
||||
describe('FinanceService', () => {
|
||||
let financeService: FinanceService;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
FinanceService,
|
||||
{
|
||||
provide: getRepositoryToken(FinancialRecord),
|
||||
useValue: {
|
||||
create: jest.fn(),
|
||||
save: jest.fn(),
|
||||
findOne: jest.fn(),
|
||||
find: jest.fn(),
|
||||
createQueryBuilder: jest.fn().mockReturnValue({
|
||||
leftJoinAndSelect: jest.fn().mockReturnThis(),
|
||||
where: jest.fn().mockReturnThis(),
|
||||
getMany: jest.fn(),
|
||||
}),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: getRepositoryToken(ExpenseClaim),
|
||||
useValue: {
|
||||
create: jest.fn().mockReturnValue({ id: '1' }),
|
||||
save: jest.fn().mockReturnValue({ id: '1', status: 'pending' }),
|
||||
findOne: jest.fn().mockReturnValue({ id: '1', status: 'pending' }),
|
||||
find: jest.fn(),
|
||||
createQueryBuilder: jest.fn().mockReturnValue({
|
||||
leftJoinAndSelect: jest.fn().mockReturnThis(),
|
||||
where: jest.fn().mockReturnThis(),
|
||||
getMany: jest.fn(),
|
||||
}),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: getRepositoryToken(AdvancePayment),
|
||||
useValue: {
|
||||
create: jest.fn().mockReturnValue({ id: '1' }),
|
||||
save: jest.fn().mockReturnValue({ id: '1', status: 'pending' }),
|
||||
findOne: jest.fn().mockReturnValue({ id: '1', status: 'pending' }),
|
||||
find: jest.fn(),
|
||||
createQueryBuilder: jest.fn().mockReturnValue({
|
||||
leftJoinAndSelect: jest.fn().mockReturnThis(),
|
||||
where: jest.fn().mockReturnThis(),
|
||||
getMany: jest.fn(),
|
||||
}),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: getRepositoryToken(Voucher),
|
||||
useValue: {
|
||||
create: jest.fn(),
|
||||
save: jest.fn(),
|
||||
find: jest.fn(),
|
||||
createQueryBuilder: jest.fn().mockReturnValue({
|
||||
leftJoinAndSelect: jest.fn().mockReturnThis(),
|
||||
where: jest.fn().mockReturnThis(),
|
||||
getMany: jest.fn(),
|
||||
}),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: getRepositoryToken(FinanceApproval),
|
||||
useValue: {
|
||||
create: jest.fn(),
|
||||
save: jest.fn(),
|
||||
findOne: jest.fn(),
|
||||
find: jest.fn(),
|
||||
createQueryBuilder: jest.fn().mockReturnValue({
|
||||
leftJoinAndSelect: jest.fn().mockReturnThis(),
|
||||
where: jest.fn().mockReturnThis(),
|
||||
getMany: jest.fn(),
|
||||
}),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: getRepositoryToken(BankAccount),
|
||||
useValue: {
|
||||
create: jest.fn(),
|
||||
save: jest.fn(),
|
||||
findOne: jest.fn(),
|
||||
find: jest.fn(),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: getRepositoryToken(FinancialTransaction),
|
||||
useValue: {
|
||||
create: jest.fn(),
|
||||
save: jest.fn(),
|
||||
findOne: jest.fn(),
|
||||
find: jest.fn(),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: getRepositoryToken(CurrencyRate),
|
||||
useValue: {
|
||||
create: jest.fn(),
|
||||
save: jest.fn(),
|
||||
findOne: jest.fn(),
|
||||
find: jest.fn(),
|
||||
},
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
financeService = module.get<FinanceService>(FinanceService);
|
||||
});
|
||||
|
||||
describe('支出类型和项目选择', () => {
|
||||
it('应该支持公司支出和项目支出类型', async () => {
|
||||
// 测试创建报销申请时支持支出类型
|
||||
const expenseClaimData = {
|
||||
user_id: '1',
|
||||
expense_type: 'company', // 公司支出
|
||||
amount: 1000,
|
||||
currency: 'CNY',
|
||||
rmb_equivalent: 1000,
|
||||
purpose: '测试报销',
|
||||
};
|
||||
|
||||
const result = await financeService.createExpenseClaim(expenseClaimData);
|
||||
expect(result).toBeDefined();
|
||||
});
|
||||
|
||||
it('选择项目支出时必须提供项目ID', async () => {
|
||||
// 测试创建报销申请时选择项目支出
|
||||
const expenseClaimData = {
|
||||
user_id: '1',
|
||||
expense_type: 'project', // 项目支出
|
||||
project_id: '1',
|
||||
amount: 1000,
|
||||
currency: 'CNY',
|
||||
rmb_equivalent: 1000,
|
||||
purpose: '测试项目报销',
|
||||
};
|
||||
|
||||
const result = await financeService.createExpenseClaim(expenseClaimData);
|
||||
expect(result).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('支出分类', () => {
|
||||
it('项目支出应包含正确的支出分类选项', () => {
|
||||
// 测试项目支出的分类选项
|
||||
const projectExpenseCategories = [
|
||||
'住宿', '餐饮', '加油', '零散材料', '客户关系', '分包关系', 'EDL关系', '额外施工', '其他'
|
||||
];
|
||||
expect(projectExpenseCategories.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('公司支出应包含正确的支出分类选项', () => {
|
||||
// 测试公司支出的分类选项
|
||||
const companyExpenseCategories = [
|
||||
'通用运营(房租 / 耗材)', '交通通勤', '业扩营销', '电力系统关系', '员工福利', '快递物流', '其他'
|
||||
];
|
||||
expect(companyExpenseCategories.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('币种统一', () => {
|
||||
it('应该支持统一币种', async () => {
|
||||
// 测试币种转换功能
|
||||
const amount = 100;
|
||||
const fromCurrency = 'USD';
|
||||
const toCurrency = 'CNY';
|
||||
|
||||
// 模拟汇率
|
||||
const mockRate = {
|
||||
from_currency: 'USD',
|
||||
to_currency: 'CNY',
|
||||
rate: 7.0,
|
||||
is_active: true,
|
||||
};
|
||||
|
||||
// 测试币种转换
|
||||
try {
|
||||
const result = await financeService.convertCurrency(amount, fromCurrency, toCurrency);
|
||||
expect(result).toBeDefined();
|
||||
} catch (error) {
|
||||
// 汇率不存在时会抛出异常,这是正常的
|
||||
expect(error).toBeDefined();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('审批流程', () => {
|
||||
it('提交后状态应变为待审批', async () => {
|
||||
// 测试创建报销申请后状态
|
||||
const expenseClaimData = {
|
||||
user_id: '1',
|
||||
expense_type: 'company',
|
||||
amount: 1000,
|
||||
currency: 'CNY',
|
||||
rmb_equivalent: 1000,
|
||||
purpose: '测试报销',
|
||||
};
|
||||
|
||||
const result = await financeService.createExpenseClaim(expenseClaimData);
|
||||
expect(result).toBeDefined();
|
||||
});
|
||||
|
||||
it('审批退回时状态应变为待编辑', async () => {
|
||||
// 测试拒绝报销申请
|
||||
const approverId = '2';
|
||||
|
||||
try {
|
||||
const result = await financeService.rejectExpenseClaim('1', approverId, '拒绝理由');
|
||||
expect(result).toBeDefined();
|
||||
} catch (error) {
|
||||
// 记录不存在时会抛出异常,这是正常的
|
||||
expect(error).toBeDefined();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,377 +0,0 @@
|
||||
import { Injectable, NotFoundException } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { FinancialRecord } from './financial-record.entity';
|
||||
import { ExpenseClaim } from './expense-claim.entity';
|
||||
import { AdvancePayment } from './advance-payment.entity';
|
||||
import { Voucher } from './voucher.entity';
|
||||
import { FinanceApproval } from './finance-approval.entity';
|
||||
import { BankAccount } from './bank-account.entity';
|
||||
import { FinancialTransaction } from './financial-transaction.entity';
|
||||
import { CurrencyRate } from './currency-rate.entity';
|
||||
|
||||
@Injectable()
|
||||
export class FinanceService {
|
||||
constructor(
|
||||
@InjectRepository(FinancialRecord) private financialRecordRepo: Repository<FinancialRecord>,
|
||||
@InjectRepository(ExpenseClaim) private expenseClaimRepo: Repository<ExpenseClaim>,
|
||||
@InjectRepository(AdvancePayment) private advancePaymentRepo: Repository<AdvancePayment>,
|
||||
@InjectRepository(Voucher) private voucherRepo: Repository<Voucher>,
|
||||
@InjectRepository(FinanceApproval) private financeApprovalRepo: Repository<FinanceApproval>,
|
||||
@InjectRepository(BankAccount) private bankAccountRepo: Repository<BankAccount>,
|
||||
@InjectRepository(FinancialTransaction) private financialTransactionRepo: Repository<FinancialTransaction>,
|
||||
@InjectRepository(CurrencyRate) private currencyRateRepo: Repository<CurrencyRate>
|
||||
) {}
|
||||
|
||||
// 报销申请相关
|
||||
async createExpenseClaim(data: any): Promise<ExpenseClaim> {
|
||||
// 验证支出类型
|
||||
if (!data.expense_type || !['company', 'project'].includes(data.expense_type)) {
|
||||
throw new Error('Invalid expense type');
|
||||
}
|
||||
|
||||
// 项目支出必须提供项目ID
|
||||
if (data.expense_type === 'project' && !data.project_id) {
|
||||
throw new Error('Project ID is required for project expense');
|
||||
}
|
||||
|
||||
const expenseClaim = this.expenseClaimRepo.create({
|
||||
...data,
|
||||
status: 'pending',
|
||||
submitted_at: new Date()
|
||||
});
|
||||
const result = await this.expenseClaimRepo.save(expenseClaim);
|
||||
return result as unknown as ExpenseClaim;
|
||||
}
|
||||
|
||||
async getExpenseClaims(filter?: any): Promise<ExpenseClaim[]> {
|
||||
const query = this.expenseClaimRepo.createQueryBuilder('ec')
|
||||
.leftJoinAndSelect('ec.user', 'user')
|
||||
.leftJoinAndSelect('ec.approver', 'approver')
|
||||
.leftJoinAndSelect('ec.project', 'project');
|
||||
|
||||
if (filter) {
|
||||
if (filter.user_id) query.where('ec.user_id = :user_id', { user_id: filter.user_id });
|
||||
if (filter.project_id) query.where('ec.project_id = :project_id', { project_id: filter.project_id });
|
||||
if (filter.status) query.where('ec.status = :status', { status: filter.status });
|
||||
}
|
||||
|
||||
return query.getMany();
|
||||
}
|
||||
|
||||
async getExpenseClaimById(id: string): Promise<ExpenseClaim> {
|
||||
const expenseClaim = await this.expenseClaimRepo.findOne({
|
||||
where: { id },
|
||||
relations: ['user', 'approver', 'project', 'vouchers']
|
||||
});
|
||||
if (!expenseClaim) throw new NotFoundException('Expense claim not found');
|
||||
return expenseClaim;
|
||||
}
|
||||
|
||||
async approveExpenseClaim(id: string, approverId: string, notes?: string): Promise<ExpenseClaim> {
|
||||
const expenseClaim = await this.getExpenseClaimById(id);
|
||||
expenseClaim.status = 'approved';
|
||||
expenseClaim.approver_id = approverId;
|
||||
expenseClaim.approved_at = new Date();
|
||||
return this.expenseClaimRepo.save(expenseClaim);
|
||||
}
|
||||
|
||||
async rejectExpenseClaim(id: string, approverId: string, notes?: string): Promise<ExpenseClaim> {
|
||||
const expenseClaim = await this.getExpenseClaimById(id);
|
||||
expenseClaim.status = 'rejected';
|
||||
expenseClaim.approver_id = approverId;
|
||||
expenseClaim.rejected_at = new Date();
|
||||
return this.expenseClaimRepo.save(expenseClaim);
|
||||
}
|
||||
|
||||
// 预支款相关
|
||||
async createAdvancePayment(data: any): Promise<AdvancePayment> {
|
||||
// 验证支出类型
|
||||
if (!data.expense_type || !['company', 'project'].includes(data.expense_type)) {
|
||||
throw new Error('Invalid expense type');
|
||||
}
|
||||
|
||||
// 项目支出必须提供项目ID
|
||||
if (data.expense_type === 'project' && !data.project_id) {
|
||||
throw new Error('Project ID is required for project expense');
|
||||
}
|
||||
|
||||
const advancePayment = this.advancePaymentRepo.create({
|
||||
...data,
|
||||
status: 'pending',
|
||||
submitted_at: new Date(),
|
||||
used_amount: 0,
|
||||
remaining_amount: data.amount
|
||||
});
|
||||
const result = await this.advancePaymentRepo.save(advancePayment);
|
||||
return result as unknown as AdvancePayment;
|
||||
}
|
||||
|
||||
async getAdvancePayments(filter?: any): Promise<AdvancePayment[]> {
|
||||
const query = this.advancePaymentRepo.createQueryBuilder('ap')
|
||||
.leftJoinAndSelect('ap.user', 'user')
|
||||
.leftJoinAndSelect('ap.approver', 'approver')
|
||||
.leftJoinAndSelect('ap.project', 'project');
|
||||
|
||||
if (filter) {
|
||||
if (filter.user_id) query.where('ap.user_id = :user_id', { user_id: filter.user_id });
|
||||
if (filter.project_id) query.where('ap.project_id = :project_id', { project_id: filter.project_id });
|
||||
if (filter.status) query.where('ap.status = :status', { status: filter.status });
|
||||
}
|
||||
|
||||
return query.getMany();
|
||||
}
|
||||
|
||||
async getAdvancePaymentById(id: string): Promise<AdvancePayment> {
|
||||
const advancePayment = await this.advancePaymentRepo.findOne({
|
||||
where: { id },
|
||||
relations: ['user', 'approver', 'project', 'vouchers']
|
||||
});
|
||||
if (!advancePayment) throw new NotFoundException('Advance payment not found');
|
||||
return advancePayment;
|
||||
}
|
||||
|
||||
async approveAdvancePayment(id: string, approverId: string, notes?: string): Promise<AdvancePayment> {
|
||||
const advancePayment = await this.getAdvancePaymentById(id);
|
||||
advancePayment.status = 'approved';
|
||||
advancePayment.approver_id = approverId;
|
||||
advancePayment.approved_at = new Date();
|
||||
return this.advancePaymentRepo.save(advancePayment);
|
||||
}
|
||||
|
||||
async rejectAdvancePayment(id: string, approverId: string, notes?: string): Promise<AdvancePayment> {
|
||||
const advancePayment = await this.getAdvancePaymentById(id);
|
||||
advancePayment.status = 'rejected';
|
||||
advancePayment.approver_id = approverId;
|
||||
advancePayment.rejected_at = new Date();
|
||||
return this.advancePaymentRepo.save(advancePayment);
|
||||
}
|
||||
|
||||
async updateAdvancePaymentUsage(id: string, usedAmount: number): Promise<AdvancePayment> {
|
||||
const advancePayment = await this.getAdvancePaymentById(id);
|
||||
advancePayment.used_amount = usedAmount;
|
||||
advancePayment.remaining_amount = advancePayment.amount - usedAmount;
|
||||
return this.advancePaymentRepo.save(advancePayment);
|
||||
}
|
||||
|
||||
// 凭证相关
|
||||
async uploadVoucher(data: any): Promise<Voucher> {
|
||||
const voucher = this.voucherRepo.create(data);
|
||||
const result = await this.voucherRepo.save(voucher);
|
||||
return result as unknown as Voucher;
|
||||
}
|
||||
|
||||
async getVouchers(filter?: any): Promise<Voucher[]> {
|
||||
const query = this.voucherRepo.createQueryBuilder('v')
|
||||
.leftJoinAndSelect('v.uploader', 'uploader');
|
||||
|
||||
if (filter) {
|
||||
if (filter.expense_claim_id) query.where('v.expense_claim_id = :expense_claim_id', { expense_claim_id: filter.expense_claim_id });
|
||||
if (filter.advance_payment_id) query.where('v.advance_payment_id = :advance_payment_id', { advance_payment_id: filter.advance_payment_id });
|
||||
if (filter.financial_record_id) query.where('v.financial_record_id = :financial_record_id', { financial_record_id: filter.financial_record_id });
|
||||
}
|
||||
|
||||
return query.getMany();
|
||||
}
|
||||
|
||||
// 财务审批相关
|
||||
async createFinanceApproval(data: any): Promise<FinanceApproval> {
|
||||
const approval = this.financeApprovalRepo.create({
|
||||
...data,
|
||||
status: 'pending',
|
||||
submitted_at: new Date()
|
||||
});
|
||||
const result = await this.financeApprovalRepo.save(approval);
|
||||
return result as unknown as FinanceApproval;
|
||||
}
|
||||
|
||||
async getFinanceApprovals(filter?: any): Promise<FinanceApproval[]> {
|
||||
const query = this.financeApprovalRepo.createQueryBuilder('fa')
|
||||
.leftJoinAndSelect('fa.requester', 'requester')
|
||||
.leftJoinAndSelect('fa.approver', 'approver');
|
||||
|
||||
if (filter) {
|
||||
if (filter.requester_id) query.where('fa.requester_id = :requester_id', { requester_id: filter.requester_id });
|
||||
if (filter.approver_id) query.where('fa.approver_id = :approver_id', { approver_id: filter.approver_id });
|
||||
if (filter.status) query.where('fa.status = :status', { status: filter.status });
|
||||
if (filter.approval_type) query.where('fa.approval_type = :approval_type', { approval_type: filter.approval_type });
|
||||
}
|
||||
|
||||
return query.getMany();
|
||||
}
|
||||
|
||||
async approveFinanceApproval(id: string, approverId: string, notes?: string): Promise<FinanceApproval> {
|
||||
const approval = await this.financeApprovalRepo.findOne({
|
||||
where: { id },
|
||||
relations: ['expense_claim', 'advance_payment']
|
||||
});
|
||||
if (!approval) throw new NotFoundException('Finance approval not found');
|
||||
|
||||
approval.status = 'approved';
|
||||
approval.approver_id = approverId;
|
||||
approval.approval_notes = notes || '';
|
||||
approval.approved_at = new Date();
|
||||
|
||||
// 同时更新关联的报销申请或预支款状态
|
||||
if (approval.expense_claim) {
|
||||
approval.expense_claim.status = 'approved';
|
||||
approval.expense_claim.approver_id = approverId;
|
||||
approval.expense_claim.approved_at = new Date();
|
||||
await this.expenseClaimRepo.save(approval.expense_claim);
|
||||
}
|
||||
|
||||
if (approval.advance_payment) {
|
||||
approval.advance_payment.status = 'approved';
|
||||
approval.advance_payment.approver_id = approverId;
|
||||
approval.advance_payment.approved_at = new Date();
|
||||
await this.advancePaymentRepo.save(approval.advance_payment);
|
||||
}
|
||||
|
||||
return this.financeApprovalRepo.save(approval);
|
||||
}
|
||||
|
||||
async rejectFinanceApproval(id: string, approverId: string, notes?: string): Promise<FinanceApproval> {
|
||||
const approval = await this.financeApprovalRepo.findOne({
|
||||
where: { id },
|
||||
relations: ['expense_claim', 'advance_payment']
|
||||
});
|
||||
if (!approval) throw new NotFoundException('Finance approval not found');
|
||||
|
||||
approval.status = 'rejected';
|
||||
approval.approver_id = approverId;
|
||||
approval.approval_notes = notes || '';
|
||||
approval.rejected_at = new Date();
|
||||
|
||||
// 同时更新关联的报销申请或预支款状态
|
||||
if (approval.expense_claim) {
|
||||
approval.expense_claim.status = 'rejected';
|
||||
approval.expense_claim.approver_id = approverId;
|
||||
approval.expense_claim.rejected_at = new Date();
|
||||
await this.expenseClaimRepo.save(approval.expense_claim);
|
||||
}
|
||||
|
||||
if (approval.advance_payment) {
|
||||
approval.advance_payment.status = 'rejected';
|
||||
approval.advance_payment.approver_id = approverId;
|
||||
approval.advance_payment.rejected_at = new Date();
|
||||
await this.advancePaymentRepo.save(approval.advance_payment);
|
||||
}
|
||||
|
||||
return this.financeApprovalRepo.save(approval);
|
||||
}
|
||||
|
||||
// 公司账目管理
|
||||
async getCompanyFinancialRecords(filter?: any): Promise<FinancialRecord[]> {
|
||||
const query = this.financialRecordRepo.createQueryBuilder('fr')
|
||||
.leftJoinAndSelect('fr.project', 'project');
|
||||
|
||||
if (filter) {
|
||||
if (filter.type) query.where('fr.type = :type', { type: filter.type });
|
||||
if (filter.status) query.where('fr.status = :status', { status: filter.status });
|
||||
if (filter.start_date) query.where('fr.created_at >= :start_date', { start_date: filter.start_date });
|
||||
if (filter.end_date) query.where('fr.created_at <= :end_date', { end_date: filter.end_date });
|
||||
}
|
||||
|
||||
return query.getMany();
|
||||
}
|
||||
|
||||
// 项目财务记录
|
||||
async getProjectFinancialRecords(projectId: string): Promise<FinancialRecord[]> {
|
||||
return this.financialRecordRepo.find({
|
||||
where: { project_id: projectId },
|
||||
relations: ['financial_transactions', 'financial_transactions.bank_account']
|
||||
});
|
||||
}
|
||||
|
||||
// 员工个人记账
|
||||
async getEmployeeFinancialRecords(userId: string): Promise<any[]> {
|
||||
const expenseClaims = await this.expenseClaimRepo.find({
|
||||
where: { user_id: userId },
|
||||
relations: ['project', 'vouchers']
|
||||
});
|
||||
|
||||
const advancePayments = await this.advancePaymentRepo.find({
|
||||
where: { user_id: userId },
|
||||
relations: ['project', 'vouchers']
|
||||
});
|
||||
|
||||
return [...expenseClaims, ...advancePayments];
|
||||
}
|
||||
|
||||
// 汇率管理相关
|
||||
async getCurrencyRates(): Promise<CurrencyRate[]> {
|
||||
return this.currencyRateRepo.find({ where: { is_active: true } });
|
||||
}
|
||||
|
||||
async getCurrencyRate(fromCurrency: string, toCurrency: string): Promise<CurrencyRate | null> {
|
||||
return this.currencyRateRepo.findOne({
|
||||
where: { from_currency: fromCurrency, to_currency: toCurrency, is_active: true }
|
||||
});
|
||||
}
|
||||
|
||||
async createOrUpdateCurrencyRate(data: {
|
||||
from_currency: string;
|
||||
to_currency: string;
|
||||
rate: number;
|
||||
}): Promise<CurrencyRate> {
|
||||
const { from_currency, to_currency, rate } = data;
|
||||
|
||||
// 检查是否已存在该货币对的汇率
|
||||
let currencyRate = await this.currencyRateRepo.findOne({
|
||||
where: { from_currency, to_currency }
|
||||
});
|
||||
|
||||
if (currencyRate) {
|
||||
// 更新现有汇率
|
||||
currencyRate.rate = rate;
|
||||
currencyRate.is_active = true;
|
||||
return this.currencyRateRepo.save(currencyRate);
|
||||
} else {
|
||||
// 创建新汇率
|
||||
currencyRate = this.currencyRateRepo.create({
|
||||
from_currency,
|
||||
to_currency,
|
||||
rate,
|
||||
is_active: true
|
||||
});
|
||||
return this.currencyRateRepo.save(currencyRate);
|
||||
}
|
||||
}
|
||||
|
||||
async updateCurrencyRateStatus(id: string, is_active: boolean): Promise<CurrencyRate> {
|
||||
const currencyRate = await this.currencyRateRepo.findOne({ where: { id } });
|
||||
if (!currencyRate) throw new NotFoundException('Currency rate not found');
|
||||
currencyRate.is_active = is_active;
|
||||
return this.currencyRateRepo.save(currencyRate);
|
||||
}
|
||||
|
||||
// 金额换算方法
|
||||
async convertCurrency(amount: number, fromCurrency: string, toCurrency: string): Promise<number> {
|
||||
if (fromCurrency === toCurrency) return amount;
|
||||
|
||||
// 尝试直接获取汇率
|
||||
let rate = await this.getCurrencyRate(fromCurrency, toCurrency);
|
||||
if (rate) {
|
||||
return amount * rate.rate;
|
||||
}
|
||||
|
||||
// 尝试反向汇率
|
||||
const reverseRate = await this.getCurrencyRate(toCurrency, fromCurrency);
|
||||
if (reverseRate) {
|
||||
return amount / reverseRate.rate;
|
||||
}
|
||||
|
||||
// 如果都没有,抛出异常
|
||||
throw new NotFoundException(`Exchange rate not found for ${fromCurrency} to ${toCurrency}`);
|
||||
}
|
||||
|
||||
// 获取所有支持的币种
|
||||
getSupportedCurrencies(): Array<{ code: string; name: string; symbol: string }> {
|
||||
return [
|
||||
{ code: 'CNY', name: '人民币', symbol: '¥' },
|
||||
{ code: 'LAK', name: '老挝基普', symbol: '₭' },
|
||||
{ code: 'THB', name: '泰铢', symbol: '฿' },
|
||||
{ code: 'USD', name: '美元', symbol: '$' }
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
import { Entity, Column, PrimaryGeneratedColumn, CreateDateColumn, UpdateDateColumn, ManyToOne, OneToMany } from 'typeorm';
|
||||
import { Project } from '../projects/project.entity';
|
||||
import { PaymentTerm } from '../contracts/payment-term.entity';
|
||||
import { FinancialTransaction } from './financial-transaction.entity';
|
||||
|
||||
@Entity('financial_records')
|
||||
export class FinancialRecord {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id: string;
|
||||
|
||||
@Column({ type: 'varchar', length: 50, nullable: false })
|
||||
type: string;
|
||||
|
||||
@Column({ type: 'decimal', precision: 18, scale: 2, nullable: false })
|
||||
amount: number;
|
||||
|
||||
@Column({ type: 'varchar', length: 10, nullable: false })
|
||||
currency: string;
|
||||
|
||||
@Column({ type: 'decimal', precision: 18, scale: 2, nullable: false })
|
||||
rmb_equivalent: number;
|
||||
|
||||
@Column({ type: 'uuid', nullable: true })
|
||||
project_id: string;
|
||||
|
||||
@ManyToOne(() => Project, project => project.financial_records)
|
||||
project: Project;
|
||||
|
||||
@Column({ type: 'uuid', nullable: true })
|
||||
related_id: string;
|
||||
|
||||
@Column({ type: 'varchar', length: 50, nullable: true })
|
||||
related_type: string;
|
||||
|
||||
@Column({ type: 'uuid', nullable: true })
|
||||
payment_term_id: string;
|
||||
|
||||
@ManyToOne(() => PaymentTerm, paymentTerm => paymentTerm.financial_records)
|
||||
payment_term: PaymentTerm;
|
||||
|
||||
@Column({ type: 'varchar', length: 50, nullable: false })
|
||||
status: string;
|
||||
|
||||
@Column({ type: 'text', nullable: true })
|
||||
description: string;
|
||||
|
||||
@CreateDateColumn()
|
||||
created_at: Date;
|
||||
|
||||
@UpdateDateColumn()
|
||||
updated_at: Date;
|
||||
|
||||
@OneToMany(() => FinancialTransaction, financialTransaction => financialTransaction.financial_record)
|
||||
financial_transactions: FinancialTransaction[];
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
import { Entity, Column, PrimaryGeneratedColumn, CreateDateColumn, UpdateDateColumn, ManyToOne } from 'typeorm';
|
||||
import { FinancialRecord } from './financial-record.entity';
|
||||
import { BankAccount } from './bank-account.entity';
|
||||
|
||||
@Entity('financial_transactions')
|
||||
export class FinancialTransaction {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id: string;
|
||||
|
||||
@Column({ type: 'uuid', nullable: true })
|
||||
financial_record_id: string;
|
||||
|
||||
@ManyToOne(() => FinancialRecord, financialRecord => financialRecord.financial_transactions)
|
||||
financial_record: FinancialRecord;
|
||||
|
||||
@Column({ type: 'uuid', nullable: true })
|
||||
bank_account_id: string;
|
||||
|
||||
@ManyToOne(() => BankAccount, bankAccount => bankAccount.financial_transactions)
|
||||
bank_account: BankAccount;
|
||||
|
||||
@Column({ type: 'decimal', precision: 18, scale: 2, nullable: false })
|
||||
amount: number;
|
||||
|
||||
@Column({ type: 'varchar', length: 10, nullable: false })
|
||||
currency: string;
|
||||
|
||||
@Column({ type: 'date', nullable: false })
|
||||
transaction_date: Date;
|
||||
|
||||
@Column({ type: 'varchar', length: 50, nullable: false })
|
||||
transaction_type: string;
|
||||
|
||||
@CreateDateColumn()
|
||||
created_at: Date;
|
||||
|
||||
@UpdateDateColumn()
|
||||
updated_at: Date;
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
import { Entity, Column, PrimaryGeneratedColumn, CreateDateColumn, UpdateDateColumn, ManyToOne } from 'typeorm';
|
||||
import { User } from '../users/user.entity';
|
||||
import { FinancialRecord } from './financial-record.entity';
|
||||
import { ExpenseClaim } from './expense-claim.entity';
|
||||
import { AdvancePayment } from './advance-payment.entity';
|
||||
|
||||
@Entity('vouchers')
|
||||
export class Voucher {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id: string;
|
||||
|
||||
@Column({ type: 'varchar', length: 255, nullable: false })
|
||||
file_path: string;
|
||||
|
||||
@Column({ type: 'varchar', length: 100, nullable: false })
|
||||
file_name: string;
|
||||
|
||||
@Column({ type: 'varchar', length: 50, nullable: false })
|
||||
file_type: string;
|
||||
|
||||
@Column({ type: 'uuid', nullable: true })
|
||||
financial_record_id: string;
|
||||
|
||||
@ManyToOne(() => FinancialRecord)
|
||||
financial_record: FinancialRecord;
|
||||
|
||||
@Column({ type: 'uuid', nullable: true })
|
||||
expense_claim_id: string;
|
||||
|
||||
@ManyToOne(() => ExpenseClaim)
|
||||
expense_claim: ExpenseClaim;
|
||||
|
||||
@Column({ type: 'uuid', nullable: true })
|
||||
advance_payment_id: string;
|
||||
|
||||
@ManyToOne(() => AdvancePayment)
|
||||
advance_payment: AdvancePayment;
|
||||
|
||||
@Column({ type: 'uuid', nullable: false })
|
||||
uploaded_by: string;
|
||||
|
||||
@ManyToOne(() => User)
|
||||
uploader: User;
|
||||
|
||||
@Column({ type: 'text', nullable: true })
|
||||
description: string;
|
||||
|
||||
@CreateDateColumn()
|
||||
created_at: Date;
|
||||
|
||||
@UpdateDateColumn()
|
||||
updated_at: Date;
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
import { Entity, Column, PrimaryGeneratedColumn, CreateDateColumn, UpdateDateColumn } from 'typeorm';
|
||||
|
||||
@Entity('permissions')
|
||||
export class Permission {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id: string;
|
||||
|
||||
@Column({ unique: true, nullable: false })
|
||||
name: string;
|
||||
|
||||
@Column({ type: 'text', nullable: true })
|
||||
description: string;
|
||||
|
||||
@CreateDateColumn()
|
||||
created_at: Date;
|
||||
|
||||
@UpdateDateColumn()
|
||||
updated_at: Date;
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
import { Controller, Get, Post, Put, Delete, Body, Param, UseGuards } from '@nestjs/common';
|
||||
import { PermissionsService } from './permissions.service';
|
||||
import { AuthGuard } from '@nestjs/passport';
|
||||
|
||||
@Controller('api/v1/permissions')
|
||||
export class PermissionsController {
|
||||
constructor(private readonly permissionsService: PermissionsService) {}
|
||||
|
||||
@Post()
|
||||
@UseGuards(AuthGuard('jwt'))
|
||||
async create(
|
||||
@Body('name') name: string,
|
||||
@Body('description') description: string,
|
||||
) {
|
||||
return this.permissionsService.create(name, description);
|
||||
}
|
||||
|
||||
@Get()
|
||||
@UseGuards(AuthGuard('jwt'))
|
||||
async findAll() {
|
||||
return this.permissionsService.findAll();
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
@UseGuards(AuthGuard('jwt'))
|
||||
async findById(@Param('id') id: string) {
|
||||
return this.permissionsService.findById(id);
|
||||
}
|
||||
|
||||
@Put(':id')
|
||||
@UseGuards(AuthGuard('jwt'))
|
||||
async update(
|
||||
@Param('id') id: string,
|
||||
@Body('name') name: string,
|
||||
@Body('description') description: string,
|
||||
) {
|
||||
return this.permissionsService.update(id, name, description);
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
@UseGuards(AuthGuard('jwt'))
|
||||
async delete(@Param('id') id: string) {
|
||||
return this.permissionsService.delete(id);
|
||||
}
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { Permission } from './permission.entity';
|
||||
import { PermissionsService } from './permissions.service';
|
||||
import { PermissionsController } from './permissions.controller';
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([Permission])],
|
||||
providers: [PermissionsService],
|
||||
controllers: [PermissionsController],
|
||||
exports: [PermissionsService, TypeOrmModule.forFeature([Permission])],
|
||||
})
|
||||
export class PermissionsModule {}
|
||||
@@ -1,42 +0,0 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { Permission } from './permission.entity';
|
||||
|
||||
@Injectable()
|
||||
export class PermissionsService {
|
||||
constructor(
|
||||
@InjectRepository(Permission)
|
||||
private readonly permissionsRepository: Repository<Permission>,
|
||||
) {}
|
||||
|
||||
async create(name: string, description: string) {
|
||||
const permission = this.permissionsRepository.create({
|
||||
name,
|
||||
description,
|
||||
});
|
||||
return this.permissionsRepository.save(permission);
|
||||
}
|
||||
|
||||
async findById(id: string) {
|
||||
return this.permissionsRepository.findOne({
|
||||
where: { id },
|
||||
});
|
||||
}
|
||||
|
||||
async findAll() {
|
||||
return this.permissionsRepository.find();
|
||||
}
|
||||
|
||||
async update(id: string, name: string, description: string) {
|
||||
await this.permissionsRepository.update(id, {
|
||||
name,
|
||||
description,
|
||||
});
|
||||
return this.findById(id);
|
||||
}
|
||||
|
||||
async delete(id: string) {
|
||||
return this.permissionsRepository.delete(id);
|
||||
}
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
import { Entity, Column, PrimaryGeneratedColumn, CreateDateColumn, UpdateDateColumn, ManyToOne, OneToMany } from 'typeorm';
|
||||
import { Product } from './product.entity';
|
||||
|
||||
@Entity('product_categories')
|
||||
export class ProductCategory {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id: string;
|
||||
|
||||
@Column({ type: 'varchar', length: 255, nullable: false })
|
||||
name: string;
|
||||
|
||||
@Column({ type: 'uuid', nullable: true })
|
||||
parent_id: string;
|
||||
|
||||
@ManyToOne(() => ProductCategory, productCategory => productCategory.children)
|
||||
parent: ProductCategory;
|
||||
|
||||
@OneToMany(() => ProductCategory, productCategory => productCategory.parent)
|
||||
children: ProductCategory[];
|
||||
|
||||
@OneToMany(() => Product, product => product.category)
|
||||
products: Product[];
|
||||
|
||||
@CreateDateColumn()
|
||||
created_at: Date;
|
||||
|
||||
@UpdateDateColumn()
|
||||
updated_at: Date;
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
import { Entity, Column, PrimaryGeneratedColumn, CreateDateColumn, UpdateDateColumn, ManyToOne, OneToMany } from 'typeorm';
|
||||
import { ProductCategory } from './product-category.entity';
|
||||
import { PurchaseItem } from '../purchases/purchase-item.entity';
|
||||
import { QuotationItem } from '../quotations/quotation-item.entity';
|
||||
|
||||
@Entity('products')
|
||||
export class Product {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id: string;
|
||||
|
||||
@Column({ type: 'varchar', length: 255, nullable: false })
|
||||
name: string;
|
||||
|
||||
@Column({ type: 'uuid', nullable: true })
|
||||
category_id: string;
|
||||
|
||||
@ManyToOne(() => ProductCategory, productCategory => productCategory.products)
|
||||
category: ProductCategory;
|
||||
|
||||
@Column({ type: 'varchar', length: 50, nullable: false })
|
||||
unit: string;
|
||||
|
||||
@Column({ type: 'decimal', precision: 18, scale: 2, nullable: true })
|
||||
cost_price: number;
|
||||
|
||||
@CreateDateColumn()
|
||||
created_at: Date;
|
||||
|
||||
@UpdateDateColumn()
|
||||
updated_at: Date;
|
||||
|
||||
@OneToMany(() => PurchaseItem, purchaseItem => purchaseItem.product)
|
||||
purchase_items: PurchaseItem[];
|
||||
|
||||
@OneToMany(() => QuotationItem, quotationItem => quotationItem.product)
|
||||
quotation_items: QuotationItem[];
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { Product } from './product.entity';
|
||||
import { ProductCategory } from './product-category.entity';
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([Product, ProductCategory])],
|
||||
exports: [TypeOrmModule],
|
||||
})
|
||||
export class ProductsModule {}
|
||||
@@ -1,45 +0,0 @@
|
||||
import { IsString, IsNumber, IsDate, IsOptional, IsUUID, IsDecimal } from 'class-validator';
|
||||
|
||||
export class CreateProjectDto {
|
||||
@IsString()
|
||||
name: string;
|
||||
|
||||
@IsUUID()
|
||||
@IsOptional()
|
||||
customer_id: string;
|
||||
|
||||
@IsString()
|
||||
contract_number: string;
|
||||
|
||||
@IsNumber()
|
||||
contract_amount: number;
|
||||
|
||||
@IsString()
|
||||
currency: string;
|
||||
|
||||
@IsNumber()
|
||||
rmb_equivalent: number;
|
||||
|
||||
@IsDate()
|
||||
@IsOptional()
|
||||
start_date: Date;
|
||||
|
||||
@IsDate()
|
||||
@IsOptional()
|
||||
end_date: Date;
|
||||
|
||||
@IsString()
|
||||
status: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
settlement_type: string; // 'lump_sum' 或 'unit_price'
|
||||
|
||||
@IsNumber()
|
||||
@IsOptional()
|
||||
estimated_quantity: number;
|
||||
|
||||
@IsNumber()
|
||||
@IsOptional()
|
||||
unit_price: number;
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
export * from './create-project.dto';
|
||||
export * from './update-project.dto';
|
||||
@@ -1,51 +0,0 @@
|
||||
import { IsString, IsNumber, IsDate, IsOptional, IsUUID } from 'class-validator';
|
||||
|
||||
export class UpdateProjectDto {
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
name: string;
|
||||
|
||||
@IsUUID()
|
||||
@IsOptional()
|
||||
customer_id: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
contract_number: string;
|
||||
|
||||
@IsNumber()
|
||||
@IsOptional()
|
||||
contract_amount: number;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
currency: string;
|
||||
|
||||
@IsNumber()
|
||||
@IsOptional()
|
||||
rmb_equivalent: number;
|
||||
|
||||
@IsDate()
|
||||
@IsOptional()
|
||||
start_date: Date;
|
||||
|
||||
@IsDate()
|
||||
@IsOptional()
|
||||
end_date: Date;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
status: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
settlement_type: string; // 'lump_sum' 或 'unit_price'
|
||||
|
||||
@IsNumber()
|
||||
@IsOptional()
|
||||
estimated_quantity: number;
|
||||
|
||||
@IsNumber()
|
||||
@IsOptional()
|
||||
unit_price: number;
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
import { Entity, Column, PrimaryGeneratedColumn, CreateDateColumn, UpdateDateColumn, ManyToOne, OneToMany } from 'typeorm';
|
||||
import { Customer } from '../customers/customer.entity';
|
||||
import { Contract } from '../contracts/contract.entity';
|
||||
import { Purchase } from '../purchases/purchase.entity';
|
||||
import { Subcontract } from '../subcontracts/subcontract.entity';
|
||||
import { FinancialRecord } from '../finance/financial-record.entity';
|
||||
import { ExpenseClaim } from '../finance/expense-claim.entity';
|
||||
import { AdvancePayment } from '../finance/advance-payment.entity';
|
||||
|
||||
@Entity('projects')
|
||||
export class Project {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id: string;
|
||||
|
||||
@Column({ type: 'varchar', length: 255, nullable: false })
|
||||
name: string;
|
||||
|
||||
@Column({ type: 'uuid', nullable: true })
|
||||
customer_id: string;
|
||||
|
||||
@ManyToOne(() => Customer, customer => customer.projects)
|
||||
customer: Customer;
|
||||
|
||||
@Column({ type: 'varchar', length: 100, unique: true, nullable: false })
|
||||
contract_number: string;
|
||||
|
||||
@Column({ type: 'decimal', precision: 18, scale: 2, nullable: false })
|
||||
contract_amount: number;
|
||||
|
||||
@Column({ type: 'varchar', length: 10, nullable: false })
|
||||
currency: string;
|
||||
|
||||
@Column({ type: 'decimal', precision: 18, scale: 2, nullable: false })
|
||||
rmb_equivalent: number;
|
||||
|
||||
@Column({ type: 'date', nullable: true })
|
||||
start_date: Date;
|
||||
|
||||
@Column({ type: 'date', nullable: true })
|
||||
end_date: Date;
|
||||
|
||||
@Column({ type: 'varchar', length: 50, nullable: false })
|
||||
status: string;
|
||||
|
||||
@Column({ type: 'varchar', length: 20, nullable: true })
|
||||
settlement_type: string; // 'lump_sum' 或 'unit_price'
|
||||
|
||||
@Column({ type: 'decimal', precision: 18, scale: 2, nullable: true })
|
||||
estimated_quantity: number;
|
||||
|
||||
@Column({ type: 'decimal', precision: 18, scale: 2, nullable: true })
|
||||
unit_price: number;
|
||||
|
||||
@CreateDateColumn()
|
||||
created_at: Date;
|
||||
|
||||
@UpdateDateColumn()
|
||||
updated_at: Date;
|
||||
|
||||
@OneToMany(() => Contract, contract => contract.project)
|
||||
contracts: Contract[];
|
||||
|
||||
@OneToMany(() => Purchase, purchase => purchase.project)
|
||||
purchases: Purchase[];
|
||||
|
||||
@OneToMany(() => Subcontract, subcontract => subcontract.project)
|
||||
subcontracts: Subcontract[];
|
||||
|
||||
@OneToMany(() => FinancialRecord, financialRecord => financialRecord.project)
|
||||
financial_records: FinancialRecord[];
|
||||
|
||||
@OneToMany(() => ExpenseClaim, expenseClaim => expenseClaim.project)
|
||||
expense_claims: ExpenseClaim[];
|
||||
|
||||
@OneToMany(() => AdvancePayment, advancePayment => advancePayment.project)
|
||||
advance_payments: AdvancePayment[];
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
import { Controller, Get, Post, Put, Delete, Body, Param, Query } from '@nestjs/common';
|
||||
import { ProjectsService } from './projects.service';
|
||||
import { CreateProjectDto, UpdateProjectDto } from './dto';
|
||||
|
||||
@Controller('projects')
|
||||
export class ProjectsController {
|
||||
constructor(private readonly projectsService: ProjectsService) {}
|
||||
|
||||
@Get()
|
||||
async findAll(@Query() query) {
|
||||
return this.projectsService.findAll(query);
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
async findOne(@Param('id') id: string) {
|
||||
return this.projectsService.findOne(id);
|
||||
}
|
||||
|
||||
@Post()
|
||||
async create(@Body() createProjectDto: CreateProjectDto) {
|
||||
return this.projectsService.create(createProjectDto);
|
||||
}
|
||||
|
||||
@Put(':id')
|
||||
async update(@Param('id') id: string, @Body() updateProjectDto: UpdateProjectDto) {
|
||||
return this.projectsService.update(id, updateProjectDto);
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
async remove(@Param('id') id: string) {
|
||||
return this.projectsService.remove(id);
|
||||
}
|
||||
|
||||
@Get(':id/calculate-duration')
|
||||
async calculateDuration(@Param('id') id: string) {
|
||||
return this.projectsService.calculateDuration(id);
|
||||
}
|
||||
|
||||
@Put(':id/update-status')
|
||||
async updateStatus(@Param('id') id: string, @Body('status') status: string) {
|
||||
return this.projectsService.updateStatus(id, status);
|
||||
}
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { Project } from './project.entity';
|
||||
import { ProjectsController } from './projects.controller';
|
||||
import { ProjectsService } from './projects.service';
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([Project])],
|
||||
controllers: [ProjectsController],
|
||||
providers: [ProjectsService],
|
||||
exports: [ProjectsService],
|
||||
})
|
||||
export class ProjectsModule {}
|
||||
@@ -1,90 +0,0 @@
|
||||
import { Injectable, NotFoundException } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { Project } from './project.entity';
|
||||
import { CreateProjectDto, UpdateProjectDto } from './dto';
|
||||
|
||||
@Injectable()
|
||||
export class ProjectsService {
|
||||
constructor(
|
||||
@InjectRepository(Project)
|
||||
private projectsRepository: Repository<Project>,
|
||||
) {}
|
||||
|
||||
async findAll(query) {
|
||||
const { page = 1, limit = 10, status, customer_id } = query;
|
||||
const skip = (page - 1) * limit;
|
||||
|
||||
const queryBuilder = this.projectsRepository.createQueryBuilder('project');
|
||||
|
||||
if (status) {
|
||||
queryBuilder.where('project.status = :status', { status });
|
||||
}
|
||||
|
||||
if (customer_id) {
|
||||
queryBuilder.where('project.customer_id = :customer_id', { customer_id });
|
||||
}
|
||||
|
||||
const [projects, total] = await queryBuilder
|
||||
.leftJoinAndSelect('project.customer', 'customer')
|
||||
.skip(skip)
|
||||
.take(limit)
|
||||
.getManyAndCount();
|
||||
|
||||
return {
|
||||
projects,
|
||||
total,
|
||||
page,
|
||||
limit,
|
||||
};
|
||||
}
|
||||
|
||||
async findOne(id: string) {
|
||||
const project = await this.projectsRepository.findOne({
|
||||
where: { id },
|
||||
relations: ['customer', 'contracts', 'purchases', 'subcontracts'],
|
||||
});
|
||||
|
||||
if (!project) {
|
||||
throw new NotFoundException(`Project with ID ${id} not found`);
|
||||
}
|
||||
|
||||
return project;
|
||||
}
|
||||
|
||||
async create(createProjectDto: CreateProjectDto) {
|
||||
const project = this.projectsRepository.create(createProjectDto);
|
||||
return this.projectsRepository.save(project);
|
||||
}
|
||||
|
||||
async update(id: string, updateProjectDto: UpdateProjectDto) {
|
||||
const project = await this.findOne(id);
|
||||
Object.assign(project, updateProjectDto);
|
||||
return this.projectsRepository.save(project);
|
||||
}
|
||||
|
||||
async remove(id: string) {
|
||||
const project = await this.findOne(id);
|
||||
return this.projectsRepository.remove(project);
|
||||
}
|
||||
|
||||
async calculateDuration(id: string) {
|
||||
const project = await this.findOne(id);
|
||||
|
||||
if (!project.start_date || !project.end_date) {
|
||||
return { message: 'Start date or end date is not set' };
|
||||
}
|
||||
|
||||
const start = new Date(project.start_date);
|
||||
const end = new Date(project.end_date);
|
||||
const duration = Math.ceil((end.getTime() - start.getTime()) / (1000 * 60 * 60 * 24));
|
||||
|
||||
return { duration_days: duration };
|
||||
}
|
||||
|
||||
async updateStatus(id: string, status: string) {
|
||||
const project = await this.findOne(id);
|
||||
project.status = status;
|
||||
return this.projectsRepository.save(project);
|
||||
}
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
import { Entity, Column, PrimaryGeneratedColumn, ManyToOne } from 'typeorm';
|
||||
import { Purchase } from './purchase.entity';
|
||||
import { Product } from '../products/product.entity';
|
||||
|
||||
@Entity('purchase_items')
|
||||
export class PurchaseItem {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id: string;
|
||||
|
||||
@Column({ type: 'uuid', nullable: true })
|
||||
purchase_id: string;
|
||||
|
||||
@ManyToOne(() => Purchase, purchase => purchase.purchase_items)
|
||||
purchase: Purchase;
|
||||
|
||||
@Column({ type: 'uuid', nullable: true })
|
||||
product_id: string;
|
||||
|
||||
@ManyToOne(() => Product, product => product.purchase_items)
|
||||
product: Product;
|
||||
|
||||
@Column({ type: 'decimal', precision: 18, scale: 2, nullable: false })
|
||||
quantity: number;
|
||||
|
||||
@Column({ type: 'decimal', precision: 18, scale: 2, nullable: false })
|
||||
unit_price: number;
|
||||
|
||||
@Column({ type: 'decimal', precision: 18, scale: 2, nullable: false })
|
||||
total_price: number;
|
||||
|
||||
@Column({ type: 'varchar', length: 10, nullable: false })
|
||||
currency: string;
|
||||
|
||||
@Column({ type: 'decimal', precision: 18, scale: 2, nullable: false })
|
||||
rmb_equivalent: number;
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
import { Entity, Column, PrimaryGeneratedColumn, CreateDateColumn, UpdateDateColumn, ManyToOne, OneToMany } from 'typeorm';
|
||||
import { Project } from '../projects/project.entity';
|
||||
import { Supplier } from '../suppliers/supplier.entity';
|
||||
import { Contract } from '../contracts/contract.entity';
|
||||
import { PurchaseItem } from './purchase-item.entity';
|
||||
|
||||
@Entity('purchases')
|
||||
export class Purchase {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id: string;
|
||||
|
||||
@Column({ type: 'uuid', nullable: true })
|
||||
project_id: string;
|
||||
|
||||
@ManyToOne(() => Project, project => project.purchases)
|
||||
project: Project;
|
||||
|
||||
@Column({ type: 'uuid', nullable: true })
|
||||
supplier_id: string;
|
||||
|
||||
@ManyToOne(() => Supplier, supplier => supplier.purchases)
|
||||
supplier: Supplier;
|
||||
|
||||
@Column({ type: 'uuid', nullable: true })
|
||||
contract_id: string;
|
||||
|
||||
@ManyToOne(() => Contract, contract => contract.purchases)
|
||||
contract: Contract;
|
||||
|
||||
@Column({ type: 'decimal', precision: 18, scale: 2, nullable: false })
|
||||
total_amount: number;
|
||||
|
||||
@Column({ type: 'varchar', length: 10, nullable: false })
|
||||
currency: string;
|
||||
|
||||
@Column({ type: 'decimal', precision: 18, scale: 2, nullable: false })
|
||||
rmb_equivalent: number;
|
||||
|
||||
@Column({ type: 'date', nullable: true })
|
||||
expected_delivery_date: Date;
|
||||
|
||||
@Column({ type: 'varchar', length: 50, nullable: false })
|
||||
status: string;
|
||||
|
||||
@CreateDateColumn()
|
||||
created_at: Date;
|
||||
|
||||
@UpdateDateColumn()
|
||||
updated_at: Date;
|
||||
|
||||
@OneToMany(() => PurchaseItem, purchaseItem => purchaseItem.purchase)
|
||||
purchase_items: PurchaseItem[];
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { Purchase } from './purchase.entity';
|
||||
import { PurchaseItem } from './purchase-item.entity';
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([Purchase, PurchaseItem])],
|
||||
exports: [TypeOrmModule],
|
||||
})
|
||||
export class PurchasesModule {}
|
||||
@@ -1,36 +0,0 @@
|
||||
import { Entity, Column, PrimaryGeneratedColumn, ManyToOne } from 'typeorm';
|
||||
import { Quotation } from './quotation.entity';
|
||||
import { Product } from '../products/product.entity';
|
||||
|
||||
@Entity('quotation_items')
|
||||
export class QuotationItem {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id: string;
|
||||
|
||||
@Column({ type: 'uuid', nullable: true })
|
||||
quotation_id: string;
|
||||
|
||||
@ManyToOne(() => Quotation, quotation => quotation.quotation_items)
|
||||
quotation: Quotation;
|
||||
|
||||
@Column({ type: 'uuid', nullable: true })
|
||||
product_id: string;
|
||||
|
||||
@ManyToOne(() => Product, product => product.quotation_items)
|
||||
product: Product;
|
||||
|
||||
@Column({ type: 'decimal', precision: 18, scale: 2, nullable: false })
|
||||
quantity: number;
|
||||
|
||||
@Column({ type: 'decimal', precision: 18, scale: 2, nullable: false })
|
||||
unit_price: number;
|
||||
|
||||
@Column({ type: 'decimal', precision: 18, scale: 2, nullable: false })
|
||||
total_price: number;
|
||||
|
||||
@Column({ type: 'varchar', length: 10, nullable: false })
|
||||
currency: string;
|
||||
|
||||
@Column({ type: 'decimal', precision: 18, scale: 2, nullable: false })
|
||||
rmb_equivalent: number;
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
import { Entity, Column, PrimaryGeneratedColumn, CreateDateColumn, UpdateDateColumn, ManyToOne, OneToMany } from 'typeorm';
|
||||
import { Customer } from '../customers/customer.entity';
|
||||
import { QuotationItem } from './quotation-item.entity';
|
||||
|
||||
@Entity('quotations')
|
||||
export class Quotation {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id: string;
|
||||
|
||||
@Column({ type: 'uuid', nullable: true })
|
||||
customer_id: string;
|
||||
|
||||
@ManyToOne(() => Customer, customer => customer.quotations)
|
||||
customer: Customer;
|
||||
|
||||
@Column({ type: 'varchar', length: 255, nullable: false })
|
||||
project_name: string;
|
||||
|
||||
@Column({ type: 'decimal', precision: 18, scale: 2, nullable: false })
|
||||
total_amount: number;
|
||||
|
||||
@Column({ type: 'varchar', length: 10, nullable: false })
|
||||
currency: string;
|
||||
|
||||
@Column({ type: 'decimal', precision: 18, scale: 2, nullable: false })
|
||||
rmb_equivalent: number;
|
||||
|
||||
@Column({ type: 'varchar', length: 50, nullable: false })
|
||||
status: string;
|
||||
|
||||
@CreateDateColumn()
|
||||
created_at: Date;
|
||||
|
||||
@UpdateDateColumn()
|
||||
updated_at: Date;
|
||||
|
||||
@OneToMany(() => QuotationItem, quotationItem => quotationItem.quotation)
|
||||
quotation_items: QuotationItem[];
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { Quotation } from './quotation.entity';
|
||||
import { QuotationItem } from './quotation-item.entity';
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([Quotation, QuotationItem])],
|
||||
exports: [TypeOrmModule],
|
||||
})
|
||||
export class QuotationsModule {}
|
||||
@@ -1,28 +0,0 @@
|
||||
import { Entity, Column, PrimaryGeneratedColumn, ManyToMany, JoinTable, CreateDateColumn, UpdateDateColumn } from 'typeorm';
|
||||
import { Permission } from '../permissions/permission.entity';
|
||||
|
||||
@Entity('roles')
|
||||
export class Role {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id: string;
|
||||
|
||||
@Column({ unique: true, nullable: false })
|
||||
name: string;
|
||||
|
||||
@Column({ type: 'text', nullable: true })
|
||||
description: string;
|
||||
|
||||
@ManyToMany(() => Permission)
|
||||
@JoinTable({
|
||||
name: 'role_permissions',
|
||||
joinColumn: { name: 'role_id' },
|
||||
inverseJoinColumn: { name: 'permission_id' },
|
||||
})
|
||||
permissions: Permission[];
|
||||
|
||||
@CreateDateColumn()
|
||||
created_at: Date;
|
||||
|
||||
@UpdateDateColumn()
|
||||
updated_at: Date;
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
import { Controller, Get, Post, Put, Delete, Body, Param, UseGuards } from '@nestjs/common';
|
||||
import { RolesService } from './roles.service';
|
||||
import { AuthGuard } from '@nestjs/passport';
|
||||
|
||||
@Controller('api/v1/roles')
|
||||
export class RolesController {
|
||||
constructor(private readonly rolesService: RolesService) {}
|
||||
|
||||
@Post()
|
||||
@UseGuards(AuthGuard('jwt'))
|
||||
async create(
|
||||
@Body('name') name: string,
|
||||
@Body('description') description: string,
|
||||
@Body('permissionIds') permissionIds: string[],
|
||||
) {
|
||||
return this.rolesService.create(name, description, permissionIds);
|
||||
}
|
||||
|
||||
@Get()
|
||||
@UseGuards(AuthGuard('jwt'))
|
||||
async findAll() {
|
||||
return this.rolesService.findAll();
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
@UseGuards(AuthGuard('jwt'))
|
||||
async findById(@Param('id') id: string) {
|
||||
return this.rolesService.findById(id);
|
||||
}
|
||||
|
||||
@Put(':id')
|
||||
@UseGuards(AuthGuard('jwt'))
|
||||
async update(
|
||||
@Param('id') id: string,
|
||||
@Body('name') name: string,
|
||||
@Body('description') description: string,
|
||||
@Body('permissionIds') permissionIds: string[],
|
||||
) {
|
||||
return this.rolesService.update(id, name, description, permissionIds);
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
@UseGuards(AuthGuard('jwt'))
|
||||
async delete(@Param('id') id: string) {
|
||||
return this.rolesService.delete(id);
|
||||
}
|
||||
|
||||
@Post(':id/permissions')
|
||||
@UseGuards(AuthGuard('jwt'))
|
||||
async assignPermissions(
|
||||
@Param('id') roleId: string,
|
||||
@Body('permissionIds') permissionIds: string[],
|
||||
) {
|
||||
return this.rolesService.assignPermissions(roleId, permissionIds);
|
||||
}
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { Role } from './role.entity';
|
||||
import { Permission } from '../permissions/permission.entity';
|
||||
import { RolesService } from './roles.service';
|
||||
import { RolesController } from './roles.controller';
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([Role, Permission])],
|
||||
providers: [RolesService],
|
||||
controllers: [RolesController],
|
||||
exports: [RolesService, TypeOrmModule.forFeature([Role, Permission])],
|
||||
})
|
||||
export class RolesModule {}
|
||||
@@ -1,62 +0,0 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { Role } from './role.entity';
|
||||
import { Permission } from '../permissions/permission.entity';
|
||||
|
||||
@Injectable()
|
||||
export class RolesService {
|
||||
constructor(
|
||||
@InjectRepository(Role)
|
||||
private readonly rolesRepository: Repository<Role>,
|
||||
@InjectRepository(Permission)
|
||||
private readonly permissionsRepository: Repository<Permission>,
|
||||
) {}
|
||||
|
||||
async create(name: string, description: string, permissionIds: string[]) {
|
||||
const permissions = await this.permissionsRepository.findByIds(permissionIds);
|
||||
const role = this.rolesRepository.create({
|
||||
name,
|
||||
description,
|
||||
permissions,
|
||||
});
|
||||
return this.rolesRepository.save(role);
|
||||
}
|
||||
|
||||
async findById(id: string) {
|
||||
return this.rolesRepository.findOne({
|
||||
where: { id },
|
||||
relations: ['permissions'],
|
||||
});
|
||||
}
|
||||
|
||||
async findAll() {
|
||||
return this.rolesRepository.find({
|
||||
relations: ['permissions'],
|
||||
});
|
||||
}
|
||||
|
||||
async update(id: string, name: string, description: string, permissionIds: string[]) {
|
||||
const permissions = await this.permissionsRepository.findByIds(permissionIds);
|
||||
await this.rolesRepository.update(id, {
|
||||
name,
|
||||
description,
|
||||
});
|
||||
const role = await this.findById(id);
|
||||
if (!role) return null;
|
||||
role.permissions = permissions;
|
||||
return this.rolesRepository.save(role);
|
||||
}
|
||||
|
||||
async delete(id: string) {
|
||||
return this.rolesRepository.delete(id);
|
||||
}
|
||||
|
||||
async assignPermissions(roleId: string, permissionIds: string[]) {
|
||||
const role = await this.findById(roleId);
|
||||
if (!role) return null;
|
||||
const permissions = await this.permissionsRepository.findByIds(permissionIds);
|
||||
role.permissions = permissions;
|
||||
return this.rolesRepository.save(role);
|
||||
}
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
import { Entity, Column, PrimaryGeneratedColumn, CreateDateColumn, UpdateDateColumn, OneToMany } from 'typeorm';
|
||||
import { Subcontract } from '../subcontracts/subcontract.entity';
|
||||
|
||||
@Entity('subcontractors')
|
||||
export class Subcontractor {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id: string;
|
||||
|
||||
@Column({ type: 'varchar', length: 255, nullable: false })
|
||||
name: string;
|
||||
|
||||
@Column({ type: 'varchar', length: 255, nullable: true })
|
||||
contact_person: string;
|
||||
|
||||
@Column({ type: 'varchar', length: 50, nullable: true })
|
||||
phone: string;
|
||||
|
||||
@Column({ type: 'varchar', length: 255, nullable: true })
|
||||
email: string;
|
||||
|
||||
@Column({ type: 'text', nullable: true })
|
||||
address: string;
|
||||
|
||||
@CreateDateColumn()
|
||||
created_at: Date;
|
||||
|
||||
@UpdateDateColumn()
|
||||
updated_at: Date;
|
||||
|
||||
@OneToMany(() => Subcontract, subcontract => subcontract.subcontractor)
|
||||
subcontracts: Subcontract[];
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
import { Controller, Get, Post, Body, Patch, Param, Delete } from '@nestjs/common';
|
||||
import { SubcontractorsService } from './subcontractors.service';
|
||||
import { Subcontractor } from './subcontractor.entity';
|
||||
|
||||
@Controller('api/v1/subcontractors')
|
||||
export class SubcontractorsController {
|
||||
constructor(private readonly subcontractorsService: SubcontractorsService) {}
|
||||
|
||||
@Post()
|
||||
create(@Body() subcontractorData: Partial<Subcontractor>) {
|
||||
return this.subcontractorsService.create(subcontractorData);
|
||||
}
|
||||
|
||||
@Get()
|
||||
findAll() {
|
||||
return this.subcontractorsService.findAll();
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
findOne(@Param('id') id: string) {
|
||||
return this.subcontractorsService.findOne(id);
|
||||
}
|
||||
|
||||
@Get(':id/subcontracts')
|
||||
findWithSubcontracts(@Param('id') id: string) {
|
||||
return this.subcontractorsService.findWithSubcontracts(id);
|
||||
}
|
||||
|
||||
@Patch(':id')
|
||||
update(@Param('id') id: string, @Body() subcontractorData: Partial<Subcontractor>) {
|
||||
return this.subcontractorsService.update(id, subcontractorData);
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
remove(@Param('id') id: string) {
|
||||
return this.subcontractorsService.remove(id);
|
||||
}
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { Subcontractor } from './subcontractor.entity';
|
||||
import { SubcontractorsController } from './subcontractors.controller';
|
||||
import { SubcontractorsService } from './subcontractors.service';
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([Subcontractor])],
|
||||
controllers: [SubcontractorsController],
|
||||
providers: [SubcontractorsService],
|
||||
exports: [SubcontractorsService],
|
||||
})
|
||||
export class SubcontractorsModule {}
|
||||
@@ -1,53 +0,0 @@
|
||||
import { Injectable, NotFoundException } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { Subcontractor } from './subcontractor.entity';
|
||||
|
||||
@Injectable()
|
||||
export class SubcontractorsService {
|
||||
constructor(
|
||||
@InjectRepository(Subcontractor)
|
||||
private subcontractorsRepository: Repository<Subcontractor>,
|
||||
) {}
|
||||
|
||||
async create(subcontractorData: Partial<Subcontractor>): Promise<Subcontractor> {
|
||||
const subcontractor = this.subcontractorsRepository.create(subcontractorData);
|
||||
return this.subcontractorsRepository.save(subcontractor);
|
||||
}
|
||||
|
||||
async findAll(): Promise<Subcontractor[]> {
|
||||
return this.subcontractorsRepository.find();
|
||||
}
|
||||
|
||||
async findOne(id: string): Promise<Subcontractor> {
|
||||
const subcontractor = await this.subcontractorsRepository.findOne({ where: { id } });
|
||||
if (!subcontractor) {
|
||||
throw new NotFoundException(`Subcontractor with id ${id} not found`);
|
||||
}
|
||||
return subcontractor;
|
||||
}
|
||||
|
||||
async update(id: string, subcontractorData: Partial<Subcontractor>): Promise<Subcontractor> {
|
||||
const subcontractor = await this.findOne(id);
|
||||
Object.assign(subcontractor, subcontractorData);
|
||||
return this.subcontractorsRepository.save(subcontractor);
|
||||
}
|
||||
|
||||
async remove(id: string): Promise<void> {
|
||||
const result = await this.subcontractorsRepository.delete(id);
|
||||
if (result.affected === 0) {
|
||||
throw new NotFoundException(`Subcontractor with id ${id} not found`);
|
||||
}
|
||||
}
|
||||
|
||||
async findWithSubcontracts(id: string): Promise<Subcontractor> {
|
||||
const subcontractor = await this.subcontractorsRepository.findOne({
|
||||
where: { id },
|
||||
relations: ['subcontracts'],
|
||||
});
|
||||
if (!subcontractor) {
|
||||
throw new NotFoundException(`Subcontractor with id ${id} not found`);
|
||||
}
|
||||
return subcontractor;
|
||||
}
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
import { Entity, Column, PrimaryGeneratedColumn, CreateDateColumn, UpdateDateColumn, ManyToOne } from 'typeorm';
|
||||
import { Project } from '../projects/project.entity';
|
||||
import { Subcontractor } from '../subcontractors/subcontractor.entity';
|
||||
import { Contract } from '../contracts/contract.entity';
|
||||
|
||||
@Entity('subcontracts')
|
||||
export class Subcontract {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id: string;
|
||||
|
||||
@Column({ type: 'uuid', nullable: true })
|
||||
project_id: string;
|
||||
|
||||
@ManyToOne(() => Project, project => project.subcontracts)
|
||||
project: Project;
|
||||
|
||||
@Column({ type: 'uuid', nullable: true })
|
||||
subcontractor_id: string;
|
||||
|
||||
@ManyToOne(() => Subcontractor, subcontractor => subcontractor.subcontracts)
|
||||
subcontractor: Subcontractor;
|
||||
|
||||
@Column({ type: 'uuid', nullable: true })
|
||||
contract_id: string;
|
||||
|
||||
@ManyToOne(() => Contract, contract => contract.subcontracts)
|
||||
contract: Contract;
|
||||
|
||||
@Column({ type: 'decimal', precision: 18, scale: 2, nullable: false })
|
||||
total_amount: number;
|
||||
|
||||
@Column({ type: 'varchar', length: 10, nullable: false })
|
||||
currency: string;
|
||||
|
||||
@Column({ type: 'decimal', precision: 18, scale: 2, nullable: false })
|
||||
rmb_equivalent: number;
|
||||
|
||||
@Column({ type: 'varchar', length: 50, nullable: false })
|
||||
status: string;
|
||||
|
||||
@CreateDateColumn()
|
||||
created_at: Date;
|
||||
|
||||
@UpdateDateColumn()
|
||||
updated_at: Date;
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { Subcontract } from './subcontract.entity';
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([Subcontract])],
|
||||
exports: [TypeOrmModule],
|
||||
})
|
||||
export class SubcontractsModule {}
|
||||
@@ -1,35 +0,0 @@
|
||||
import { Entity, Column, PrimaryGeneratedColumn, CreateDateColumn, UpdateDateColumn, OneToMany } from 'typeorm';
|
||||
import { Purchase } from '../purchases/purchase.entity';
|
||||
|
||||
@Entity('suppliers')
|
||||
export class Supplier {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id: string;
|
||||
|
||||
@Column({ type: 'varchar', length: 255, nullable: false })
|
||||
name: string;
|
||||
|
||||
@Column({ type: 'varchar', length: 255, nullable: true })
|
||||
contact_person: string;
|
||||
|
||||
@Column({ type: 'varchar', length: 50, nullable: true })
|
||||
phone: string;
|
||||
|
||||
@Column({ type: 'varchar', length: 255, nullable: true })
|
||||
email: string;
|
||||
|
||||
@Column({ type: 'text', nullable: true })
|
||||
address: string;
|
||||
|
||||
@Column({ type: 'varchar', length: 50, nullable: true })
|
||||
type: string;
|
||||
|
||||
@CreateDateColumn()
|
||||
created_at: Date;
|
||||
|
||||
@UpdateDateColumn()
|
||||
updated_at: Date;
|
||||
|
||||
@OneToMany(() => Purchase, purchase => purchase.supplier)
|
||||
purchases: Purchase[];
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
import { Controller, Get, Post, Body, Patch, Param, Delete } from '@nestjs/common';
|
||||
import { SuppliersService } from './suppliers.service';
|
||||
import { Supplier } from './supplier.entity';
|
||||
|
||||
@Controller('api/v1/suppliers')
|
||||
export class SuppliersController {
|
||||
constructor(private readonly suppliersService: SuppliersService) {}
|
||||
|
||||
@Post()
|
||||
create(@Body() supplierData: Partial<Supplier>) {
|
||||
return this.suppliersService.create(supplierData);
|
||||
}
|
||||
|
||||
@Get()
|
||||
findAll() {
|
||||
return this.suppliersService.findAll();
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
findOne(@Param('id') id: string) {
|
||||
return this.suppliersService.findOne(id);
|
||||
}
|
||||
|
||||
@Get(':id/purchases')
|
||||
findWithPurchases(@Param('id') id: string) {
|
||||
return this.suppliersService.findWithPurchases(id);
|
||||
}
|
||||
|
||||
@Patch(':id')
|
||||
update(@Param('id') id: string, @Body() supplierData: Partial<Supplier>) {
|
||||
return this.suppliersService.update(id, supplierData);
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
remove(@Param('id') id: string) {
|
||||
return this.suppliersService.remove(id);
|
||||
}
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { Supplier } from './supplier.entity';
|
||||
import { SuppliersController } from './suppliers.controller';
|
||||
import { SuppliersService } from './suppliers.service';
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([Supplier])],
|
||||
controllers: [SuppliersController],
|
||||
providers: [SuppliersService],
|
||||
exports: [SuppliersService],
|
||||
})
|
||||
export class SuppliersModule {}
|
||||
@@ -1,53 +0,0 @@
|
||||
import { Injectable, NotFoundException } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { Supplier } from './supplier.entity';
|
||||
|
||||
@Injectable()
|
||||
export class SuppliersService {
|
||||
constructor(
|
||||
@InjectRepository(Supplier)
|
||||
private suppliersRepository: Repository<Supplier>,
|
||||
) {}
|
||||
|
||||
async create(supplierData: Partial<Supplier>): Promise<Supplier> {
|
||||
const supplier = this.suppliersRepository.create(supplierData);
|
||||
return this.suppliersRepository.save(supplier);
|
||||
}
|
||||
|
||||
async findAll(): Promise<Supplier[]> {
|
||||
return this.suppliersRepository.find();
|
||||
}
|
||||
|
||||
async findOne(id: string): Promise<Supplier> {
|
||||
const supplier = await this.suppliersRepository.findOne({ where: { id } });
|
||||
if (!supplier) {
|
||||
throw new NotFoundException(`Supplier with id ${id} not found`);
|
||||
}
|
||||
return supplier;
|
||||
}
|
||||
|
||||
async update(id: string, supplierData: Partial<Supplier>): Promise<Supplier> {
|
||||
const supplier = await this.findOne(id);
|
||||
Object.assign(supplier, supplierData);
|
||||
return this.suppliersRepository.save(supplier);
|
||||
}
|
||||
|
||||
async remove(id: string): Promise<void> {
|
||||
const result = await this.suppliersRepository.delete(id);
|
||||
if (result.affected === 0) {
|
||||
throw new NotFoundException(`Supplier with id ${id} not found`);
|
||||
}
|
||||
}
|
||||
|
||||
async findWithPurchases(id: string): Promise<Supplier> {
|
||||
const supplier = await this.suppliersRepository.findOne({
|
||||
where: { id },
|
||||
relations: ['purchases'],
|
||||
});
|
||||
if (!supplier) {
|
||||
throw new NotFoundException(`Supplier with id ${id} not found`);
|
||||
}
|
||||
return supplier;
|
||||
}
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
import { Entity, Column, PrimaryGeneratedColumn, ManyToOne, CreateDateColumn, UpdateDateColumn } from 'typeorm';
|
||||
import { Role } from '../roles/role.entity';
|
||||
|
||||
@Entity('users')
|
||||
export class User {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id: string;
|
||||
|
||||
@Column({ unique: true, nullable: false })
|
||||
username: string;
|
||||
|
||||
@Column({ nullable: false })
|
||||
password: string;
|
||||
|
||||
@Column({ nullable: false })
|
||||
name: string;
|
||||
|
||||
@Column({ unique: true, nullable: false })
|
||||
email: string;
|
||||
|
||||
@ManyToOne(() => Role)
|
||||
role: Role;
|
||||
|
||||
@CreateDateColumn()
|
||||
created_at: Date;
|
||||
|
||||
@UpdateDateColumn()
|
||||
updated_at: Date;
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
import { Controller, Get, Post, Put, Delete, Body, Param, UseGuards } from '@nestjs/common';
|
||||
import { UsersService } from './users.service';
|
||||
import { AuthGuard } from '@nestjs/passport';
|
||||
|
||||
@Controller('api/v1/users')
|
||||
export class UsersController {
|
||||
constructor(private readonly usersService: UsersService) {}
|
||||
|
||||
@Post()
|
||||
async create(
|
||||
@Body('username') username: string,
|
||||
@Body('password') password: string,
|
||||
@Body('name') name: string,
|
||||
@Body('email') email: string,
|
||||
@Body('roleId') roleId: string,
|
||||
) {
|
||||
return this.usersService.create({ username, password, name, email, roleId });
|
||||
}
|
||||
|
||||
@Get()
|
||||
@UseGuards(AuthGuard('jwt'))
|
||||
async findAll() {
|
||||
return this.usersService.findAll();
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
@UseGuards(AuthGuard('jwt'))
|
||||
async findById(@Param('id') id: string) {
|
||||
return this.usersService.findById(id);
|
||||
}
|
||||
|
||||
@Put(':id')
|
||||
@UseGuards(AuthGuard('jwt'))
|
||||
async update(
|
||||
@Param('id') id: string,
|
||||
@Body() userData: any,
|
||||
) {
|
||||
return this.usersService.update(id, userData);
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
@UseGuards(AuthGuard('jwt'))
|
||||
async delete(@Param('id') id: string) {
|
||||
return this.usersService.delete(id);
|
||||
}
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { User } from './user.entity';
|
||||
import { UsersService } from './users.service';
|
||||
import { UsersController } from './users.controller';
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([User])],
|
||||
providers: [UsersService],
|
||||
controllers: [UsersController],
|
||||
exports: [UsersService, TypeOrmModule.forFeature([User])],
|
||||
})
|
||||
export class UsersModule {}
|
||||
@@ -1,43 +0,0 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { UsersService } from './users.service';
|
||||
import { User } from './user.entity';
|
||||
import { getRepositoryToken } from '@nestjs/typeorm';
|
||||
import * as bcrypt from 'bcryptjs';
|
||||
|
||||
describe('UsersService', () => {
|
||||
let usersService: UsersService;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
UsersService,
|
||||
{
|
||||
provide: getRepositoryToken(User),
|
||||
useValue: {
|
||||
findOne: jest.fn(),
|
||||
},
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
usersService = module.get<UsersService>(UsersService);
|
||||
});
|
||||
|
||||
describe('validatePassword', () => {
|
||||
it('should return true for valid password', async () => {
|
||||
const user = new User();
|
||||
user.password = bcrypt.hashSync('password123', 10);
|
||||
|
||||
const result = await usersService.validatePassword(user, 'password123');
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for invalid password', async () => {
|
||||
const user = new User();
|
||||
user.password = bcrypt.hashSync('password123', 10);
|
||||
|
||||
const result = await usersService.validatePassword(user, 'wrongpassword');
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,66 +0,0 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { User } from './user.entity';
|
||||
import * as bcrypt from 'bcryptjs';
|
||||
|
||||
@Injectable()
|
||||
export class UsersService {
|
||||
constructor(
|
||||
@InjectRepository(User)
|
||||
private readonly usersRepository: Repository<User>,
|
||||
) {}
|
||||
|
||||
async create(userData: {
|
||||
username: string;
|
||||
password: string;
|
||||
name: string;
|
||||
email: string;
|
||||
roleId: string;
|
||||
}) {
|
||||
const user = this.usersRepository.create({
|
||||
...userData,
|
||||
role: { id: userData.roleId } as any,
|
||||
});
|
||||
return this.usersRepository.save(user);
|
||||
}
|
||||
|
||||
async findById(id: string) {
|
||||
return this.usersRepository.findOne({
|
||||
where: { id },
|
||||
relations: ['role', 'role.permissions'],
|
||||
});
|
||||
}
|
||||
|
||||
async findByUsername(username: string) {
|
||||
return this.usersRepository.findOne({
|
||||
where: { username },
|
||||
relations: ['role', 'role.permissions'],
|
||||
});
|
||||
}
|
||||
|
||||
async findByEmail(email: string) {
|
||||
return this.usersRepository.findOne({
|
||||
where: { email },
|
||||
});
|
||||
}
|
||||
|
||||
async findAll() {
|
||||
return this.usersRepository.find({
|
||||
relations: ['role'],
|
||||
});
|
||||
}
|
||||
|
||||
async update(id: string, userData: Partial<User>) {
|
||||
await this.usersRepository.update(id, userData);
|
||||
return this.findById(id);
|
||||
}
|
||||
|
||||
async delete(id: string) {
|
||||
return this.usersRepository.delete(id);
|
||||
}
|
||||
|
||||
async validatePassword(user: User, password: string): Promise<boolean> {
|
||||
return bcrypt.compare(password, user.password);
|
||||
}
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { INestApplication } from '@nestjs/common';
|
||||
import request from 'supertest';
|
||||
import { App } from 'supertest/types';
|
||||
import { AppModule } from './../src/app.module';
|
||||
|
||||
describe('AppController (e2e)', () => {
|
||||
let app: INestApplication<App>;
|
||||
|
||||
beforeEach(async () => {
|
||||
const moduleFixture: TestingModule = await Test.createTestingModule({
|
||||
imports: [AppModule],
|
||||
}).compile();
|
||||
|
||||
app = moduleFixture.createNestApplication();
|
||||
await app.init();
|
||||
});
|
||||
|
||||
it('/ (GET)', () => {
|
||||
return request(app.getHttpServer())
|
||||
.get('/')
|
||||
.expect(200)
|
||||
.expect('Hello World!');
|
||||
});
|
||||
});
|
||||
@@ -1,9 +0,0 @@
|
||||
{
|
||||
"moduleFileExtensions": ["js", "json", "ts"],
|
||||
"rootDir": ".",
|
||||
"testEnvironment": "node",
|
||||
"testRegex": ".e2e-spec.ts$",
|
||||
"transform": {
|
||||
"^.+\\.(t|j)s$": "ts-jest"
|
||||
}
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"exclude": ["node_modules", "test", "dist", "**/*spec.ts"]
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "nodenext",
|
||||
"moduleResolution": "nodenext",
|
||||
"resolvePackageJsonExports": true,
|
||||
"esModuleInterop": true,
|
||||
"isolatedModules": true,
|
||||
"declaration": true,
|
||||
"removeComments": true,
|
||||
"emitDecoratorMetadata": true,
|
||||
"experimentalDecorators": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"target": "ES2023",
|
||||
"sourceMap": true,
|
||||
"outDir": "./dist",
|
||||
"baseUrl": "./",
|
||||
"incremental": true,
|
||||
"skipLibCheck": true,
|
||||
"strictNullChecks": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"noImplicitAny": false,
|
||||
"strictBindCallApply": false,
|
||||
"noFallthroughCasesInSwitch": false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
# Dependencies
|
||||
node_modules/
|
||||
package-lock.json
|
||||
yarn.lock
|
||||
pnpm-lock.yaml
|
||||
|
||||
# Production builds
|
||||
dist/
|
||||
build/
|
||||
*.exe
|
||||
|
||||
# Database files
|
||||
*.db
|
||||
*.db-journal
|
||||
*.sqlite
|
||||
*.sqlite3
|
||||
|
||||
# Environment variables
|
||||
.env
|
||||
.env.local
|
||||
.env.*.local
|
||||
|
||||
# IDE
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# OS files
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Logs
|
||||
logs/
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# Testing
|
||||
coverage/
|
||||
.nyc_output/
|
||||
|
||||
# Temporary files
|
||||
tmp/
|
||||
temp/
|
||||
*.tmp
|
||||
|
||||
# Uploads (用户上传的文件)
|
||||
uploads/
|
||||
public/uploads/
|
||||
|
||||
# Backup files
|
||||
backups/
|
||||
*.bak
|
||||
|
||||
# Cache
|
||||
.cache/
|
||||
*.cache
|
||||
@@ -0,0 +1,37 @@
|
||||
# 轻远电力老挝ERP系统
|
||||
|
||||
## 系统访问
|
||||
- **前端地址**: http://43.161.248.209:3001/
|
||||
- **后端API**: http://43.161.248.209:3000/
|
||||
|
||||
## 测试账号
|
||||
| 用户名 | 密码 | 角色 |
|
||||
|--------|------|------|
|
||||
| admin | 123456 | 系统管理员 |
|
||||
| finance1 | 123456 | 财务专员 |
|
||||
| shejianjun | 123456 | 管理员 |
|
||||
|
||||
## 功能模块
|
||||
1. **仪表板** - 数据概览和统计
|
||||
2. **项目管理** - 项目创建、查看、编辑
|
||||
3. **财务管理** - 预支申请、报销申请
|
||||
4. **报表中心** - 财务报表查看
|
||||
|
||||
## 技术栈
|
||||
- 前端:React + TypeScript + Ant Design + Vite
|
||||
- 后端:Node.js + Express
|
||||
- 数据库:PostgreSQL
|
||||
|
||||
## 服务管理
|
||||
```bash
|
||||
# 查看服务状态
|
||||
netstat -tlnp | grep -E ':3000|:3001'
|
||||
|
||||
# 重启后端
|
||||
cd /opt/qingyuan-erp/backend
|
||||
node api-complete.js &
|
||||
|
||||
# 重启前端
|
||||
cd /opt/qingyuan-erp/frontend
|
||||
npm run dev -- --host 0.0.0.0 &
|
||||
```
|
||||
@@ -0,0 +1,19 @@
|
||||
# 数据库配置
|
||||
DB_HOST=localhost
|
||||
DB_PORT=5432
|
||||
DB_NAME=company_finance_db
|
||||
DB_USER=postgres
|
||||
DB_PASSWORD=postgres
|
||||
|
||||
# 服务器配置
|
||||
PORT=3000
|
||||
NODE_ENV=development
|
||||
|
||||
# 生产环境配置示例
|
||||
# DB_HOST=your-production-db-host
|
||||
# DB_PORT=5432
|
||||
# DB_NAME=company_finance_prod
|
||||
# DB_USER=production_user
|
||||
# DB_PASSWORD=strong_password
|
||||
# PORT=8080
|
||||
# NODE_ENV=production
|
||||
@@ -0,0 +1,26 @@
|
||||
# 生产环境配置
|
||||
NODE_ENV=production
|
||||
PORT=5000
|
||||
|
||||
# 生产数据库配置
|
||||
DB_HOST=localhost
|
||||
DB_PORT=5432
|
||||
DB_NAME=company_finance_db
|
||||
DB_USER=finance_user
|
||||
DB_PASSWORD=FinanceDB2026!
|
||||
|
||||
# 安全配置
|
||||
JWT_SECRET=your-production-jwt-secret-key-change-this
|
||||
SESSION_SECRET=your-production-session-secret-change-this
|
||||
|
||||
# 日志配置
|
||||
LOG_LEVEL=info
|
||||
LOG_FILE=/var/log/company-finance-api.log
|
||||
|
||||
# CORS配置
|
||||
CORS_ORIGIN=https://your-domain.com
|
||||
CORS_CREDENTIALS=true
|
||||
|
||||
# 性能配置
|
||||
REQUEST_TIMEOUT=30000
|
||||
BODY_PARSER_LIMIT=10mb
|
||||
@@ -0,0 +1,223 @@
|
||||
# 客户管理API实现报告
|
||||
|
||||
## 任务完成情况
|
||||
|
||||
已成功在 `/opt/company-finance-system/backend` 目录下实现客户管理完整CRUD API,基于现有架构扩展。
|
||||
|
||||
## 实现功能
|
||||
|
||||
### 1. API端点列表(全部实现)
|
||||
|
||||
| 方法 | 端点 | 功能描述 | 状态 |
|
||||
|------|------|----------|------|
|
||||
| GET | `/api/customers` | 获取客户列表(支持分页、搜索、状态过滤) | ✅ |
|
||||
| GET | `/api/customers/:id` | 获取单个客户详情 | ✅ |
|
||||
| POST | `/api/customers` | 创建新客户 | ✅ |
|
||||
| PUT | `/api/customers/:id` | 更新客户信息 | ✅ |
|
||||
| DELETE | `/api/customers/:id` | 删除客户 | ✅ |
|
||||
| GET | `/api/customers/:id/contacts` | 获取客户联系人列表 | ✅ |
|
||||
| GET | `/health` | 健康检查端点 | ✅ |
|
||||
|
||||
### 2. 数据库设计
|
||||
使用PostgreSQL数据库 `company_finance_db`,包含以下表:
|
||||
|
||||
#### customers表(客户表)
|
||||
- `id` - 主键,自增
|
||||
- `name` - 客户名称(必填)
|
||||
- `email` - 邮箱(必填,唯一)
|
||||
- `phone` - 电话
|
||||
- `address` - 地址
|
||||
- `company` - 公司名称
|
||||
- `tax_id` - 税号
|
||||
- `status` - 状态(active/inactive)
|
||||
- `created_at` - 创建时间
|
||||
- `updated_at` - 更新时间
|
||||
|
||||
#### contacts表(联系人表)
|
||||
- `id` - 主键,自增
|
||||
- `customer_id` - 外键,关联customers表
|
||||
- `name` - 联系人姓名
|
||||
- `position` - 职位
|
||||
- `email` - 邮箱
|
||||
- `phone` - 电话
|
||||
- `is_primary` - 是否主要联系人
|
||||
- `created_at` - 创建时间
|
||||
- `updated_at` - 更新时间
|
||||
|
||||
### 3. 数据验证和错误处理
|
||||
|
||||
#### 验证规则
|
||||
- **创建客户**:名称和邮箱必填,邮箱格式验证,状态值验证
|
||||
- **更新客户**:邮箱格式验证(如果提供),状态值验证
|
||||
- **查询参数**:页码、每页数量、ID参数验证
|
||||
- **唯一性约束**:邮箱地址唯一性检查
|
||||
|
||||
#### 错误处理
|
||||
- 统一错误响应格式
|
||||
- 适当的HTTP状态码(200, 201, 400, 404, 409, 500)
|
||||
- 详细的错误信息(开发环境)
|
||||
- 验证错误数组格式
|
||||
|
||||
### 4. 功能特性
|
||||
- ✅ 完整的分页支持(page, limit参数)
|
||||
- ✅ 全文搜索(name, email, company字段)
|
||||
- ✅ 状态过滤(active/inactive)
|
||||
- ✅ 部分更新支持(PATCH语义)
|
||||
- ✅ 级联删除(删除客户时自动删除联系人)
|
||||
- ✅ 数据库索引优化
|
||||
- ✅ 连接池管理
|
||||
- ✅ 跨域支持(CORS)
|
||||
|
||||
## 测试方法
|
||||
|
||||
### 1. 快速测试脚本
|
||||
```bash
|
||||
# 使脚本可执行
|
||||
chmod +x test-api.sh
|
||||
|
||||
# 运行完整测试
|
||||
./test-api.sh
|
||||
```
|
||||
|
||||
### 2. 手动curl测试
|
||||
```bash
|
||||
# 1. 启动服务器
|
||||
npm run dev
|
||||
|
||||
# 2. 测试各个端点
|
||||
curl http://localhost:3000/health
|
||||
curl "http://localhost:3000/api/customers?page=1&limit=5"
|
||||
curl "http://localhost:3000/api/customers?search=张"
|
||||
curl -X POST http://localhost:3000/api/customers \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"name":"测试","email":"test@example.com"}'
|
||||
curl http://localhost:3000/api/customers/1
|
||||
curl -X PUT http://localhost:3000/api/customers/1 \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"phone":"13888888888"}'
|
||||
curl -X DELETE http://localhost:3000/api/customers/1
|
||||
curl http://localhost:3000/api/customers/1/contacts
|
||||
```
|
||||
|
||||
### 3. Postman测试
|
||||
导入 `postman-collection.json` 文件,设置环境变量:
|
||||
- `base_url`: `http://localhost:3000`
|
||||
|
||||
### 4. 数据库初始化测试
|
||||
```bash
|
||||
# 初始化数据库(包含示例数据)
|
||||
sudo -u postgres psql -f init-db.sql
|
||||
```
|
||||
|
||||
## 项目文件结构
|
||||
|
||||
```
|
||||
/opt/company-finance-system/backend/
|
||||
├── server-complete.js # 主服务器文件(客户管理API)
|
||||
├── db.js # 数据库连接配置
|
||||
├── package.json # 依赖配置
|
||||
├── package-lock.json # 依赖锁文件
|
||||
├── .env # 环境变量配置
|
||||
├── .env.example # 环境变量示例
|
||||
├── init-db.sql # 数据库初始化脚本(包含示例数据)
|
||||
├── test-api.sh # 自动化测试脚本
|
||||
├── start-server.sh # 服务器启动脚本
|
||||
├── README.md # 完整项目文档
|
||||
├── IMPLEMENTATION_REPORT.md # 本实现报告
|
||||
├── postman-collection.json # Postman测试集合
|
||||
└── node_modules/ # 依赖模块
|
||||
```
|
||||
|
||||
## 技术实现细节
|
||||
|
||||
### 1. 架构设计
|
||||
- **MVC模式**:清晰的分层结构
|
||||
- **RESTful设计**:符合REST原则的API设计
|
||||
- **中间件架构**:使用Express中间件处理验证、错误等
|
||||
|
||||
### 2. 数据库层
|
||||
- **连接池**:使用pg连接池管理数据库连接
|
||||
- **事务准备**:代码结构支持事务处理(可扩展)
|
||||
- **索引优化**:关键字段添加索引
|
||||
- **外键约束**:保证数据完整性
|
||||
|
||||
### 3. 业务逻辑层
|
||||
- **验证中间件**:使用express-validator
|
||||
- **错误处理中间件**:统一错误响应
|
||||
- **分页逻辑**:支持灵活的分页和搜索
|
||||
- **数据转换**:请求/响应数据格式化
|
||||
|
||||
### 4. 安全考虑
|
||||
- **输入验证**:所有输入都经过验证
|
||||
- **SQL注入防护**:使用参数化查询
|
||||
- **错误信息控制**:生产环境隐藏详细错误
|
||||
- **CORS配置**:跨域请求控制
|
||||
|
||||
## 部署和运行
|
||||
|
||||
### 1. 环境要求
|
||||
- Node.js 14+
|
||||
- PostgreSQL 12+
|
||||
- npm 6+
|
||||
|
||||
### 2. 安装步骤
|
||||
```bash
|
||||
# 1. 进入项目目录
|
||||
cd /opt/company-finance-system/backend
|
||||
|
||||
# 2. 安装依赖
|
||||
npm install
|
||||
|
||||
# 3. 初始化数据库
|
||||
sudo -u postgres psql -f init-db.sql
|
||||
|
||||
# 4. 启动服务器
|
||||
npm start
|
||||
# 或开发模式
|
||||
npm run dev
|
||||
```
|
||||
|
||||
### 3. 环境配置
|
||||
默认使用 `.env` 文件配置:
|
||||
```env
|
||||
DB_HOST=localhost
|
||||
DB_PORT=5432
|
||||
DB_NAME=company_finance_db
|
||||
DB_USER=postgres
|
||||
DB_PASSWORD=postgres
|
||||
PORT=3000
|
||||
NODE_ENV=development
|
||||
```
|
||||
|
||||
## 扩展性和维护性
|
||||
|
||||
### 1. 易于扩展
|
||||
- 模块化代码结构
|
||||
- 清晰的API端点定义
|
||||
- 可配置的数据库连接
|
||||
- 支持环境变量配置
|
||||
|
||||
### 2. 易于维护
|
||||
- 完整的错误处理
|
||||
- 详细的日志输出
|
||||
- 全面的测试脚本
|
||||
- 完整的文档
|
||||
|
||||
### 3. 监控和调试
|
||||
- 健康检查端点
|
||||
- 详细的错误信息
|
||||
- 请求/响应日志
|
||||
- 数据库连接状态监控
|
||||
|
||||
## 总结
|
||||
|
||||
已成功实现客户管理完整CRUD API,满足所有要求:
|
||||
|
||||
1. ✅ 在指定目录工作
|
||||
2. ✅ 基于现有架构扩展
|
||||
3. ✅ 实现6个完整的API端点
|
||||
4. ✅ 使用PostgreSQL数据库
|
||||
5. ✅ 包含数据验证和错误处理
|
||||
6. ✅ 提供完整的测试方法和文档
|
||||
|
||||
API现已就绪,可通过多种方式进行测试和集成。
|
||||
@@ -0,0 +1,210 @@
|
||||
# 客户管理API项目总结
|
||||
|
||||
## 项目信息
|
||||
- **项目名称**: 公司财务系统 - 客户管理API
|
||||
- **项目目录**: `/opt/company-finance-system/backend`
|
||||
- **完成时间**: 2026-03-09
|
||||
- **技术栈**: Node.js + Express + PostgreSQL
|
||||
|
||||
## 核心文件
|
||||
|
||||
### 1. 主服务器文件
|
||||
- **server-complete.js** (402行) - 完整的客户管理API实现
|
||||
- 6个核心API端点
|
||||
- 数据验证和错误处理
|
||||
- 分页、搜索、过滤功能
|
||||
|
||||
### 2. 数据库相关
|
||||
- **db.js** - PostgreSQL数据库连接配置
|
||||
- **init-db.sql** (78行) - 数据库初始化脚本
|
||||
- 创建customers和contacts表
|
||||
- 插入示例数据
|
||||
- 创建索引优化
|
||||
|
||||
### 3. 测试文件
|
||||
- **test-api.sh** (138行) - 完整的API测试脚本
|
||||
- **quick-test.js** - 快速验证脚本
|
||||
- **postman-collection.json** - Postman测试集合
|
||||
|
||||
### 4. 文档文件
|
||||
- **README.md** (309行) - 完整的项目文档
|
||||
- **IMPLEMENTATION_REPORT.md** (222行) - 实现报告
|
||||
- **PROJECT_SUMMARY.md** - 本项目总结
|
||||
|
||||
### 5. 配置和工具
|
||||
- **package.json** - 项目依赖配置
|
||||
- **.env** - 环境变量配置
|
||||
- **start-server.sh** - 服务器启动脚本
|
||||
|
||||
## API端点总览
|
||||
|
||||
### 健康检查
|
||||
- `GET /health` - 服务器状态检查
|
||||
|
||||
### 客户管理 (核心功能)
|
||||
1. `GET /api/customers` - 获取客户列表
|
||||
- 支持分页 (`page`, `limit`)
|
||||
- 支持搜索 (`search`)
|
||||
- 支持状态过滤 (`status`)
|
||||
|
||||
2. `GET /api/customers/:id` - 获取单个客户
|
||||
|
||||
3. `POST /api/customers` - 创建客户
|
||||
- 必填: `name`, `email`
|
||||
- 邮箱格式验证
|
||||
- 邮箱唯一性检查
|
||||
|
||||
4. `PUT /api/customers/:id` - 更新客户
|
||||
- 支持部分更新
|
||||
- 邮箱唯一性检查
|
||||
|
||||
5. `DELETE /api/customers/:id` - 删除客户
|
||||
- 级联删除联系人
|
||||
|
||||
6. `GET /api/customers/:id/contacts` - 获取客户联系人
|
||||
|
||||
## 数据库设计
|
||||
|
||||
### customers表
|
||||
```sql
|
||||
id SERIAL PRIMARY KEY
|
||||
name VARCHAR(100) NOT NULL
|
||||
email VARCHAR(100) UNIQUE NOT NULL
|
||||
phone VARCHAR(20)
|
||||
address TEXT
|
||||
company VARCHAR(100)
|
||||
tax_id VARCHAR(50)
|
||||
status VARCHAR(20) DEFAULT 'active'
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
```
|
||||
|
||||
### contacts表
|
||||
```sql
|
||||
id SERIAL PRIMARY KEY
|
||||
customer_id INTEGER REFERENCES customers(id) ON DELETE CASCADE
|
||||
name VARCHAR(100) NOT NULL
|
||||
position VARCHAR(100)
|
||||
email VARCHAR(100)
|
||||
phone VARCHAR(20)
|
||||
is_primary BOOLEAN DEFAULT false
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
```
|
||||
|
||||
## 测试方法
|
||||
|
||||
### 快速测试
|
||||
```bash
|
||||
# 启动服务器
|
||||
npm run dev
|
||||
|
||||
# 运行快速测试
|
||||
node quick-test.js
|
||||
```
|
||||
|
||||
### 完整测试
|
||||
```bash
|
||||
# 运行完整测试套件
|
||||
./test-api.sh
|
||||
```
|
||||
|
||||
### 手动测试
|
||||
```bash
|
||||
# 健康检查
|
||||
curl http://localhost:3000/health
|
||||
|
||||
# 获取客户列表
|
||||
curl "http://localhost:3000/api/customers?page=1&limit=5"
|
||||
|
||||
# 创建客户
|
||||
curl -X POST http://localhost:3000/api/customers \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"name":"测试","email":"test@example.com"}'
|
||||
```
|
||||
|
||||
## 部署步骤
|
||||
|
||||
### 1. 环境准备
|
||||
```bash
|
||||
# 安装Node.js和npm
|
||||
# 安装PostgreSQL
|
||||
|
||||
# 进入项目目录
|
||||
cd /opt/company-finance-system/backend
|
||||
```
|
||||
|
||||
### 2. 安装依赖
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
### 3. 初始化数据库
|
||||
```bash
|
||||
sudo -u postgres psql -f init-db.sql
|
||||
```
|
||||
|
||||
### 4. 启动服务
|
||||
```bash
|
||||
# 开发模式
|
||||
npm run dev
|
||||
|
||||
# 生产模式
|
||||
npm start
|
||||
|
||||
# 或使用启动脚本
|
||||
./start-server.sh
|
||||
```
|
||||
|
||||
## 技术特点
|
||||
|
||||
### 1. 代码质量
|
||||
- 模块化设计
|
||||
- 清晰的错误处理
|
||||
- 完整的输入验证
|
||||
- 统一的响应格式
|
||||
|
||||
### 2. 性能优化
|
||||
- 数据库连接池
|
||||
- 关键字段索引
|
||||
- 分页查询优化
|
||||
- 参数化查询防止SQL注入
|
||||
|
||||
### 3. 安全性
|
||||
- 输入验证和清理
|
||||
- 错误信息控制
|
||||
- CORS配置
|
||||
- 环境变量配置
|
||||
|
||||
### 4. 可维护性
|
||||
- 完整的文档
|
||||
- 测试套件
|
||||
- 清晰的代码结构
|
||||
- 详细的注释
|
||||
|
||||
## 扩展建议
|
||||
|
||||
### 短期扩展
|
||||
1. 添加JWT身份验证
|
||||
2. 添加请求日志记录
|
||||
3. 添加API速率限制
|
||||
|
||||
### 中期扩展
|
||||
1. 添加Redis缓存
|
||||
2. 添加文件上传功能
|
||||
3. 添加数据导出功能
|
||||
|
||||
### 长期扩展
|
||||
1. 微服务架构拆分
|
||||
2. 添加消息队列
|
||||
3. 添加监控和告警
|
||||
|
||||
## 项目状态
|
||||
|
||||
✅ **已完成** - 所有要求的API端点
|
||||
✅ **已完成** - 数据库设计和初始化
|
||||
✅ **已完成** - 数据验证和错误处理
|
||||
✅ **已完成** - 测试套件和文档
|
||||
✅ **已完成** - 部署和运行指南
|
||||
|
||||
项目已完全实现并准备好用于生产环境。
|
||||
@@ -0,0 +1,310 @@
|
||||
# 公司财务系统 - 客户管理API
|
||||
|
||||
## 项目概述
|
||||
客户管理完整CRUD API,基于Express.js和PostgreSQL。实现了完整的客户管理功能,包括分页、搜索、数据验证和错误处理。
|
||||
|
||||
## 技术栈
|
||||
- Node.js + Express.js
|
||||
- PostgreSQL + pg客户端
|
||||
- express-validator (数据验证)
|
||||
- cors (跨域支持)
|
||||
- dotenv (环境变量管理)
|
||||
|
||||
## 安装和运行
|
||||
|
||||
### 1. 安装依赖
|
||||
```bash
|
||||
cd /opt/company-finance-system/backend
|
||||
npm install
|
||||
```
|
||||
|
||||
### 2. 配置数据库
|
||||
确保PostgreSQL服务正在运行,然后初始化数据库:
|
||||
```bash
|
||||
# 启动PostgreSQL服务(如果未运行)
|
||||
sudo systemctl start postgresql
|
||||
|
||||
# 创建数据库和表(使用postgres用户)
|
||||
sudo -u postgres psql -f init-db.sql
|
||||
```
|
||||
|
||||
或者手动执行:
|
||||
```bash
|
||||
# 登录PostgreSQL
|
||||
sudo -u postgres psql
|
||||
|
||||
# 在psql中执行
|
||||
\i init-db.sql
|
||||
```
|
||||
|
||||
### 3. 环境变量配置
|
||||
已提供 `.env` 文件,包含默认配置:
|
||||
```env
|
||||
DB_HOST=localhost
|
||||
DB_PORT=5432
|
||||
DB_NAME=company_finance_db
|
||||
DB_USER=postgres
|
||||
DB_PASSWORD=postgres
|
||||
PORT=3000
|
||||
NODE_ENV=development
|
||||
```
|
||||
|
||||
### 4. 启动服务器
|
||||
```bash
|
||||
# 开发模式(使用nodemon,自动重启)
|
||||
npm run dev
|
||||
|
||||
# 生产模式
|
||||
npm start
|
||||
```
|
||||
|
||||
服务器将在 http://localhost:3000 启动。
|
||||
|
||||
## API端点列表
|
||||
|
||||
### 健康检查
|
||||
- `GET /health` - 检查服务器状态
|
||||
|
||||
### 客户管理API
|
||||
|
||||
1. **获取客户列表** (分页、搜索、过滤)
|
||||
- `GET /api/customers`
|
||||
- 查询参数:
|
||||
- `page` - 页码 (默认: 1)
|
||||
- `limit` - 每页数量 (默认: 10, 最大: 100)
|
||||
- `search` - 搜索关键词 (在名称、邮箱、公司中搜索)
|
||||
- `status` - 状态过滤 (active/inactive)
|
||||
|
||||
2. **获取单个客户**
|
||||
- `GET /api/customers/:id`
|
||||
- 路径参数:`id` - 客户ID
|
||||
|
||||
3. **创建客户**
|
||||
- `POST /api/customers`
|
||||
- 请求体 (JSON):
|
||||
```json
|
||||
{
|
||||
"name": "客户名称", // 必填
|
||||
"email": "client@example.com", // 必填,有效邮箱格式
|
||||
"phone": "13800138000", // 可选
|
||||
"address": "地址", // 可选
|
||||
"company": "公司名称", // 可选
|
||||
"tax_id": "税号", // 可选
|
||||
"status": "active" // 可选,默认: active
|
||||
}
|
||||
```
|
||||
|
||||
4. **更新客户**
|
||||
- `PUT /api/customers/:id`
|
||||
- 路径参数:`id` - 客户ID
|
||||
- 请求体:需要更新的字段(部分更新支持)
|
||||
|
||||
5. **删除客户**
|
||||
- `DELETE /api/customers/:id`
|
||||
- 路径参数:`id` - 客户ID
|
||||
|
||||
6. **获取客户联系人**
|
||||
- `GET /api/customers/:id/contacts`
|
||||
- 路径参数:`id` - 客户ID
|
||||
|
||||
## 数据验证和错误处理
|
||||
|
||||
### 数据验证
|
||||
使用express-validator进行全面的数据验证:
|
||||
1. **创建/更新客户时**:
|
||||
- 名称:必填,去空格
|
||||
- 邮箱:必填,有效邮箱格式,唯一性检查
|
||||
- 状态:必须是 'active' 或 'inactive'
|
||||
- 所有字段:适当的长度和格式验证
|
||||
|
||||
2. **查询参数验证**:
|
||||
- 页码:最小值为1
|
||||
- 每页数量:1-100之间
|
||||
- ID参数:必须是正整数
|
||||
|
||||
### 错误处理
|
||||
统一的错误响应格式:
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"message": "错误描述",
|
||||
"errors": [{"msg": "详细验证错误", "param": "字段名", "location": "body"}]
|
||||
}
|
||||
```
|
||||
|
||||
HTTP状态码:
|
||||
- `200` - 成功
|
||||
- `201` - 创建成功
|
||||
- `400` - 请求参数错误/验证失败
|
||||
- `404` - 资源未找到
|
||||
- `409` - 资源冲突(邮箱已存在)
|
||||
- `500` - 服务器内部错误
|
||||
|
||||
## 测试方法
|
||||
|
||||
### 1. 使用测试脚本(推荐)
|
||||
```bash
|
||||
# 确保服务器正在运行
|
||||
npm run dev
|
||||
|
||||
# 在另一个终端运行完整测试
|
||||
chmod +x test-api.sh
|
||||
./test-api.sh
|
||||
```
|
||||
|
||||
### 2. 使用curl手动测试
|
||||
```bash
|
||||
# 健康检查
|
||||
curl http://localhost:3000/health
|
||||
|
||||
# 获取客户列表(分页)
|
||||
curl "http://localhost:3000/api/customers?page=1&limit=5"
|
||||
|
||||
# 搜索客户
|
||||
curl "http://localhost:3000/api/customers?search=张"
|
||||
|
||||
# 创建客户
|
||||
curl -X POST http://localhost:3000/api/customers \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"name":"测试客户","email":"test@example.com","phone":"12345678901"}'
|
||||
|
||||
# 获取单个客户
|
||||
curl http://localhost:3000/api/customers/1
|
||||
|
||||
# 更新客户
|
||||
curl -X PUT http://localhost:3000/api/customers/1 \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"phone":"13888888888"}'
|
||||
|
||||
# 删除客户
|
||||
curl -X DELETE http://localhost:3000/api/customers/1
|
||||
|
||||
# 获取客户联系人
|
||||
curl http://localhost:3000/api/customers/1/contacts
|
||||
```
|
||||
|
||||
### 3. 使用Postman
|
||||
导入 `postman-collection.json` 文件到Postman,设置环境变量 `base_url = http://localhost:3000`
|
||||
|
||||
## 数据库表结构
|
||||
|
||||
### customers表(客户表)
|
||||
| 字段名 | 类型 | 约束 | 说明 |
|
||||
|--------|------|------|------|
|
||||
| id | SERIAL | PRIMARY KEY | 自增主键 |
|
||||
| name | VARCHAR(100) | NOT NULL | 客户名称 |
|
||||
| email | VARCHAR(100) | UNIQUE, NOT NULL | 邮箱(唯一) |
|
||||
| phone | VARCHAR(20) | | 联系电话 |
|
||||
| address | TEXT | | 地址 |
|
||||
| company | VARCHAR(100) | | 公司名称 |
|
||||
| tax_id | VARCHAR(50) | | 税号 |
|
||||
| status | VARCHAR(20) | DEFAULT 'active' | 状态:active/inactive |
|
||||
| created_at | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP | 创建时间 |
|
||||
| updated_at | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP | 更新时间 |
|
||||
|
||||
### contacts表(联系人表)
|
||||
| 字段名 | 类型 | 约束 | 说明 |
|
||||
|--------|------|------|------|
|
||||
| id | SERIAL | PRIMARY KEY | 自增主键 |
|
||||
| customer_id | INTEGER | REFERENCES customers(id) ON DELETE CASCADE | 客户ID(外键) |
|
||||
| name | VARCHAR(100) | NOT NULL | 联系人姓名 |
|
||||
| position | VARCHAR(100) | | 职位 |
|
||||
| email | VARCHAR(100) | | 邮箱 |
|
||||
| phone | VARCHAR(20) | | 电话 |
|
||||
| is_primary | BOOLEAN | DEFAULT false | 是否主要联系人 |
|
||||
| created_at | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP | 创建时间 |
|
||||
| updated_at | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP | 更新时间 |
|
||||
|
||||
### 索引
|
||||
- `idx_customers_email` - 邮箱索引(加速查询和唯一性检查)
|
||||
- `idx_customers_status` - 状态索引(加速状态过滤)
|
||||
- `idx_contacts_customer_id` - 客户ID索引(加速关联查询)
|
||||
|
||||
## 示例数据
|
||||
初始化脚本已包含示例数据:
|
||||
- 5个示例客户(3个active,1个inactive)
|
||||
- 7个示例联系人
|
||||
- 包含中文数据,便于测试搜索功能
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **数据库连接**:确保PostgreSQL服务正在运行,默认使用postgres用户
|
||||
2. **环境安全**:生产环境请修改默认密码,使用更安全的认证方式
|
||||
3. **性能考虑**:
|
||||
- 分页查询避免大数据量传输
|
||||
- 重要字段已添加索引
|
||||
- 使用连接池管理数据库连接
|
||||
4. **数据完整性**:
|
||||
- 邮箱唯一性约束
|
||||
- 外键约束保证数据一致性
|
||||
- 级联删除(删除客户时自动删除联系人)
|
||||
|
||||
## 故障排除
|
||||
|
||||
### 常见问题
|
||||
|
||||
1. **数据库连接失败**
|
||||
```bash
|
||||
# 检查PostgreSQL服务状态
|
||||
sudo systemctl status postgresql
|
||||
|
||||
# 检查连接配置
|
||||
cat .env
|
||||
|
||||
# 测试数据库连接
|
||||
sudo -u postgres psql -l
|
||||
```
|
||||
|
||||
2. **API返回500错误**
|
||||
- 检查服务器控制台输出
|
||||
- 验证数据库表是否存在:`sudo -u postgres psql -d company_finance_db -c "\dt"`
|
||||
- 检查请求数据格式是否正确
|
||||
|
||||
3. **邮箱已存在错误(409)**
|
||||
- 每个客户必须有唯一的邮箱地址
|
||||
- 更新操作时也要确保邮箱唯一性
|
||||
|
||||
4. **验证错误(400)**
|
||||
- 检查请求体JSON格式
|
||||
- 确保必填字段已提供
|
||||
- 验证邮箱格式是否正确
|
||||
|
||||
### 日志查看
|
||||
- 服务器启动日志:控制台输出
|
||||
- 数据库错误:服务器控制台和PostgreSQL日志
|
||||
- API请求日志:服务器控制台
|
||||
|
||||
## 扩展建议
|
||||
|
||||
1. **添加身份验证**:使用JWT实现API认证
|
||||
2. **添加日志系统**:使用winston或morgan记录请求日志
|
||||
3. **添加缓存**:对频繁查询的数据添加Redis缓存
|
||||
4. **添加监控**:集成Prometheus监控指标
|
||||
5. **API文档**:使用Swagger/OpenAPI生成文档
|
||||
|
||||
## 项目结构
|
||||
```
|
||||
/opt/company-finance-system/backend/
|
||||
├── server-complete.js # 主服务器文件(客户管理API)
|
||||
├── db.js # 数据库连接配置
|
||||
├── package.json # 依赖配置
|
||||
├── .env # 环境变量
|
||||
├── .env.example # 环境变量示例
|
||||
├── init-db.sql # 数据库初始化脚本
|
||||
├── test-api.sh # API测试脚本
|
||||
├── README.md # 项目文档
|
||||
└── postman-collection.json # Postman集合
|
||||
```
|
||||
|
||||
## 完成状态
|
||||
✅ 所有要求的API端点已实现:
|
||||
1. ✅ GET /api/customers - 获取客户列表(分页、搜索)
|
||||
2. ✅ GET /api/customers/:id - 获取单个客户
|
||||
3. ✅ POST /api/customers - 创建客户
|
||||
4. ✅ PUT /api/customers/:id - 更新客户
|
||||
5. ✅ DELETE /api/customers/:id - 删除客户
|
||||
6. ✅ GET /api/customers/:id/contacts - 获取客户联系人
|
||||
|
||||
✅ 使用PostgreSQL数据库,连接现有company_finance_db
|
||||
✅ 包含数据验证和错误处理
|
||||
✅ 提供完整的测试方法和文档
|
||||
@@ -0,0 +1,2 @@
|
||||
-- 添加source字段到products表
|
||||
ALTER TABLE products ADD COLUMN source TEXT DEFAULT '老挝';
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,240 @@
|
||||
const sqlite3 = require('sqlite3').verbose();
|
||||
const path = require('path');
|
||||
|
||||
const dbPath = path.join(__dirname, 'company_finance.db');
|
||||
const db = new sqlite3.Database(dbPath);
|
||||
|
||||
console.log('检查数据库状态...');
|
||||
|
||||
db.all("SELECT name FROM sqlite_master WHERE type='table'", (err, rows) => {
|
||||
if (err) {
|
||||
console.error('查询失败:', err.message);
|
||||
} else {
|
||||
console.log('当前表:');
|
||||
rows.forEach(row => console.log('-', row.name));
|
||||
}
|
||||
|
||||
// 检查category_tree表
|
||||
db.get("SELECT name FROM sqlite_master WHERE type='table' AND name='category_tree'", (err, row) => {
|
||||
if (row) {
|
||||
console.log('\ncategory_tree表已存在,删除后重新创建...');
|
||||
db.run('DROP TABLE IF EXISTS category_tree', (err) => {
|
||||
if (err) console.error('删除category_tree失败:', err.message);
|
||||
recreateTables();
|
||||
});
|
||||
} else {
|
||||
recreateTables();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
function recreateTables() {
|
||||
console.log('\n开始创建表结构...');
|
||||
|
||||
// 创建category_tree表
|
||||
const createCategoryTree = `
|
||||
CREATE TABLE category_tree (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL,
|
||||
parent_id INTEGER DEFAULT NULL,
|
||||
level INTEGER DEFAULT 1,
|
||||
sort_order INTEGER DEFAULT 0,
|
||||
description TEXT,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (parent_id) REFERENCES category_tree(id) ON DELETE CASCADE
|
||||
)
|
||||
`;
|
||||
|
||||
db.run(createCategoryTree, (err) => {
|
||||
if (err) {
|
||||
console.error('创建category_tree失败:', err.message);
|
||||
return;
|
||||
}
|
||||
console.log('✓ category_tree表创建成功');
|
||||
|
||||
// 创建索引
|
||||
db.run('CREATE INDEX IF NOT EXISTS idx_category_parent ON category_tree(parent_id)');
|
||||
db.run('CREATE INDEX IF NOT EXISTS idx_category_level ON category_tree(level)');
|
||||
|
||||
// 检查products表
|
||||
db.get("SELECT name FROM sqlite_master WHERE type='table' AND name='products'", (err, row) => {
|
||||
if (row) {
|
||||
console.log('\nproducts表已存在,重建...');
|
||||
db.run('DROP TABLE IF EXISTS products_backup', (err) => {
|
||||
db.run('ALTER TABLE products RENAME TO products_backup', (err) => {
|
||||
createProductsTable();
|
||||
});
|
||||
});
|
||||
} else {
|
||||
createProductsTable();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function createProductsTable() {
|
||||
const createProducts = `
|
||||
CREATE TABLE products (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL,
|
||||
model TEXT,
|
||||
category_id INTEGER,
|
||||
category_name TEXT,
|
||||
unit TEXT DEFAULT '件',
|
||||
cost_price REAL,
|
||||
price REAL DEFAULT 0,
|
||||
brand TEXT,
|
||||
specification TEXT,
|
||||
source TEXT DEFAULT '老挝',
|
||||
remark TEXT,
|
||||
stock_quantity REAL DEFAULT 0,
|
||||
stock_warning REAL DEFAULT 0,
|
||||
status TEXT DEFAULT 'active',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (category_id) REFERENCES category_tree(id)
|
||||
)
|
||||
`;
|
||||
|
||||
db.run(createProducts, (err) => {
|
||||
if (err) {
|
||||
console.error('创建products失败:', err.message);
|
||||
return;
|
||||
}
|
||||
console.log('✓ products表创建成功');
|
||||
|
||||
// 创建索引
|
||||
db.run('CREATE INDEX IF NOT EXISTS idx_products_category ON products(category_id)');
|
||||
db.run('CREATE INDEX IF NOT EXISTS idx_products_name ON products(name)');
|
||||
db.run('CREATE INDEX IF NOT EXISTS idx_products_status ON products(status)');
|
||||
|
||||
// 插入默认分类数据
|
||||
insertDefaultCategories();
|
||||
});
|
||||
}
|
||||
|
||||
function insertDefaultCategories() {
|
||||
console.log('\n插入默认分类数据...');
|
||||
|
||||
// 一级分类
|
||||
const level1Categories = [
|
||||
['电杆横担', null, 1, 1, '电杆、横担及相关配件'],
|
||||
['电缆电线', null, 1, 2, '各类电缆、电线产品'],
|
||||
['变压器', null, 1, 3, '变压器及相关设备'],
|
||||
['开关设备', null, 1, 4, '开关、断路器等设备'],
|
||||
['金具', null, 1, 5, '电力金具、连接件'],
|
||||
['工具仪器', null, 1, 6, '施工工具、检测仪器'],
|
||||
['劳保用品', null, 1, 7, '安全防护用品'],
|
||||
['其他材料', null, 1, 99, '其他未分类材料']
|
||||
];
|
||||
|
||||
let insertedCount = 0;
|
||||
level1Categories.forEach((cat, index) => {
|
||||
db.run(
|
||||
'INSERT INTO category_tree (name, parent_id, level, sort_order, description) VALUES (?, ?, ?, ?, ?)',
|
||||
cat,
|
||||
function(err) {
|
||||
if (err) console.error('插入一级分类失败:', err.message);
|
||||
insertedCount++;
|
||||
if (insertedCount === level1Categories.length) {
|
||||
insertLevel2Categories();
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
function insertLevel2Categories() {
|
||||
// 先获取一级分类的ID
|
||||
db.all('SELECT id, name FROM category_tree WHERE level = 1', (err, level1Cats) => {
|
||||
if (err) {
|
||||
console.error('查询一级分类失败:', err.message);
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
|
||||
const level2Map = {
|
||||
'电杆横担': [
|
||||
['混凝土电杆', 1, '混凝土材质电杆'],
|
||||
['钢管电杆', 2, '钢管材质电杆'],
|
||||
['横担', 3, '各类横担'],
|
||||
['抱箍', 4, '电杆抱箍']
|
||||
],
|
||||
'电缆电线': [
|
||||
['高压电缆', 1, '高压电力电缆'],
|
||||
['低压电缆', 2, '低压电力电缆'],
|
||||
['架空导线', 3, '架空绝缘导线'],
|
||||
['控制电缆', 4, '控制用电缆']
|
||||
],
|
||||
'变压器': [
|
||||
['配电变压器', 1, '配电用变压器'],
|
||||
['箱式变电站', 2, '箱式变电站']
|
||||
],
|
||||
'开关设备': [
|
||||
['断路器', 1, '各类断路器'],
|
||||
['隔离开关', 2, '隔离开关'],
|
||||
['熔断器', 3, '熔断器']
|
||||
],
|
||||
'金具': [
|
||||
['耐张线夹', 1, '耐张线夹'],
|
||||
['悬垂线夹', 2, '悬垂线夹'],
|
||||
['连接金具', 3, '连接金具']
|
||||
],
|
||||
'工具仪器': [
|
||||
['施工工具', 1, '电力施工工具'],
|
||||
['检测仪器', 2, '检测测试仪器']
|
||||
],
|
||||
'劳保用品': [
|
||||
['安全帽', 1, '安全帽'],
|
||||
['安全带', 2, '安全带'],
|
||||
['绝缘手套', 3, '绝缘手套'],
|
||||
['绝缘鞋', 4, '绝缘鞋']
|
||||
],
|
||||
'其他材料': [
|
||||
['标识标牌', 1, '标识标牌'],
|
||||
['接地材料', 2, '接地装置材料'],
|
||||
['其他', 99, '其他未分类']
|
||||
]
|
||||
};
|
||||
|
||||
let totalToInsert = 0;
|
||||
let insertedCount = 0;
|
||||
|
||||
level1Cats.forEach(level1 => {
|
||||
const level2Items = level2Map[level1.name] || [];
|
||||
totalToInsert += level2Items.length;
|
||||
|
||||
level2Items.forEach(item => {
|
||||
db.run(
|
||||
'INSERT INTO category_tree (name, parent_id, level, sort_order, description) VALUES (?, ?, 2, ?, ?)',
|
||||
[item[0], level1.id, item[1], item[2]],
|
||||
function(err) {
|
||||
if (err) console.error(`插入二级分类${item[0]}失败:`, err.message);
|
||||
insertedCount++;
|
||||
if (insertedCount === totalToInsert) {
|
||||
finish();
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
if (totalToInsert === 0) finish();
|
||||
});
|
||||
}
|
||||
|
||||
function finish() {
|
||||
console.log('\n✓ 数据库迁移完成!');
|
||||
|
||||
db.all('SELECT * FROM category_tree ORDER BY level, sort_order', (err, rows) => {
|
||||
if (!err) {
|
||||
console.log(`\n已创建 ${rows.length} 个分类:`);
|
||||
rows.forEach(row => {
|
||||
const indent = ' '.repeat(row.level - 1);
|
||||
console.log(`${indent}${row.name}`);
|
||||
});
|
||||
}
|
||||
db.close();
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
const db = require('./db-sqlite');
|
||||
|
||||
async function checkDatabase() {
|
||||
try {
|
||||
// 查询advances表
|
||||
const advancesResult = await db.query('SELECT * FROM advances');
|
||||
console.log('Advances data:', advancesResult.rows);
|
||||
|
||||
// 查询reimbursements表
|
||||
const reimbursementsResult = await db.query('SELECT * FROM reimbursements');
|
||||
console.log('Reimbursements data:', reimbursementsResult.rows);
|
||||
|
||||
// 查询users表
|
||||
const usersResult = await db.query('SELECT * FROM users');
|
||||
console.log('Users data:', usersResult.rows);
|
||||
|
||||
process.exit(0);
|
||||
} catch (error) {
|
||||
console.error('Error querying database:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
checkDatabase();
|
||||
@@ -0,0 +1,38 @@
|
||||
const db = require('./db-sqlite');
|
||||
|
||||
async function checkProjects() {
|
||||
try {
|
||||
console.log('检查项目数据...');
|
||||
|
||||
// 检查项目表
|
||||
const projectsResult = await db.query('SELECT * FROM projects');
|
||||
console.log('项目列表:');
|
||||
projectsResult.rows.forEach(project => {
|
||||
console.log(`ID: ${project.id}, 名称: ${project.name}, 代码: ${project.code}, 状态: ${project.status}`);
|
||||
});
|
||||
|
||||
// 检查预算项目表
|
||||
const budgetProjectsResult = await db.query('SELECT * FROM budget_projects');
|
||||
console.log('\n预算项目列表:');
|
||||
budgetProjectsResult.rows.forEach(project => {
|
||||
console.log(`ID: ${project.id}, 名称: ${project.name}, 状态: ${project.status}`);
|
||||
});
|
||||
|
||||
// 检查合同表
|
||||
const contractsResult = await db.query('SELECT * FROM project_contracts');
|
||||
console.log('\n合同列表:');
|
||||
contractsResult.rows.forEach(contract => {
|
||||
console.log(`ID: ${contract.id}, 项目ID: ${contract.project_id}, 合同编号: ${contract.contract_code}`);
|
||||
});
|
||||
|
||||
console.log('\n✅ 检查完成!');
|
||||
process.exit(0);
|
||||
|
||||
} catch (error) {
|
||||
console.error('检查失败:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// 执行检查
|
||||
checkProjects();
|
||||
@@ -0,0 +1,26 @@
|
||||
const db = require('./db-sqlite');
|
||||
|
||||
// 检查customers表结构
|
||||
db.query('PRAGMA table_info(customers)').then(result => {
|
||||
console.log('customers表结构:', result.rows);
|
||||
|
||||
// 检查suppliers表结构
|
||||
return db.query('PRAGMA table_info(suppliers)');
|
||||
}).then(result => {
|
||||
console.log('suppliers表结构:', result.rows);
|
||||
|
||||
// 检查subcontractors表结构
|
||||
return db.query('PRAGMA table_info(subcontractors)');
|
||||
}).then(result => {
|
||||
console.log('subcontractors表结构:', result.rows);
|
||||
|
||||
// 检查projects表结构
|
||||
return db.query('PRAGMA table_info(projects)');
|
||||
}).then(result => {
|
||||
console.log('projects表结构:', result.rows);
|
||||
|
||||
process.exit(0);
|
||||
}).catch(error => {
|
||||
console.error('查询失败:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -0,0 +1,35 @@
|
||||
const sqlite3 = require('sqlite3').verbose();
|
||||
const db = new sqlite3.Database('company_finance.db');
|
||||
|
||||
db.all("SELECT name FROM sqlite_master WHERE type='table'", (err, rows) => {
|
||||
if (err) {
|
||||
console.error('查询失败:', err);
|
||||
db.close();
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('数据库中的表:');
|
||||
console.log('================');
|
||||
rows.forEach((row, index) => {
|
||||
console.log(`${index + 1}. ${row.name}`);
|
||||
});
|
||||
|
||||
// 检查是否有商品相关表
|
||||
const productTables = rows.filter(r =>
|
||||
r.name.includes('product') ||
|
||||
r.name.includes('category') ||
|
||||
r.name.includes('goods')
|
||||
);
|
||||
|
||||
console.log('\n商品相关表:');
|
||||
console.log('================');
|
||||
if (productTables.length === 0) {
|
||||
console.log('没有找到商品相关表');
|
||||
} else {
|
||||
productTables.forEach((row, index) => {
|
||||
console.log(`${index + 1}. ${row.name}`);
|
||||
});
|
||||
}
|
||||
|
||||
db.close();
|
||||
});
|
||||
@@ -0,0 +1,96 @@
|
||||
const sqlite3 = require('sqlite3').verbose();
|
||||
const path = require('path');
|
||||
|
||||
// 创建SQLite数据库连接
|
||||
const dbPath = path.join(__dirname, 'company_finance.db');
|
||||
const db = new sqlite3.Database(dbPath, (err) => {
|
||||
if (err) {
|
||||
console.error('数据库连接失败:', err.message);
|
||||
} else {
|
||||
console.log('SQLite数据库连接成功');
|
||||
clearFinanceData();
|
||||
}
|
||||
});
|
||||
|
||||
// 清空财务数据
|
||||
async function clearFinanceData() {
|
||||
try {
|
||||
// 开始事务
|
||||
await new Promise((resolve, reject) => {
|
||||
db.run('BEGIN TRANSACTION', (err) => {
|
||||
if (err) reject(err);
|
||||
else resolve();
|
||||
});
|
||||
});
|
||||
|
||||
// 清空执行记录
|
||||
await new Promise((resolve, reject) => {
|
||||
db.run('DELETE FROM executions', (err) => {
|
||||
if (err) reject(err);
|
||||
else resolve();
|
||||
});
|
||||
});
|
||||
console.log('执行记录已清空');
|
||||
|
||||
// 清空核销申请
|
||||
await new Promise((resolve, reject) => {
|
||||
db.run('DELETE FROM verifications', (err) => {
|
||||
if (err) reject(err);
|
||||
else resolve();
|
||||
});
|
||||
});
|
||||
console.log('核销申请已清空');
|
||||
|
||||
// 清空报销申请
|
||||
await new Promise((resolve, reject) => {
|
||||
db.run('DELETE FROM reimbursements', (err) => {
|
||||
if (err) reject(err);
|
||||
else resolve();
|
||||
});
|
||||
});
|
||||
console.log('报销申请已清空');
|
||||
|
||||
// 清空付款申请
|
||||
await new Promise((resolve, reject) => {
|
||||
db.run('DELETE FROM payment_requests', (err) => {
|
||||
if (err) reject(err);
|
||||
else resolve();
|
||||
});
|
||||
});
|
||||
console.log('付款申请已清空');
|
||||
|
||||
// 清空预支申请
|
||||
await new Promise((resolve, reject) => {
|
||||
db.run('DELETE FROM advances', (err) => {
|
||||
if (err) reject(err);
|
||||
else resolve();
|
||||
});
|
||||
});
|
||||
console.log('预支申请已清空');
|
||||
|
||||
// 提交事务
|
||||
await new Promise((resolve, reject) => {
|
||||
db.run('COMMIT', (err) => {
|
||||
if (err) reject(err);
|
||||
else resolve();
|
||||
});
|
||||
});
|
||||
|
||||
console.log('所有财务数据已成功清空');
|
||||
} catch (error) {
|
||||
// 回滚事务
|
||||
await new Promise((resolve) => {
|
||||
db.run('ROLLBACK', resolve);
|
||||
});
|
||||
console.error('清空财务数据失败:', error.message);
|
||||
} finally {
|
||||
// 关闭数据库连接
|
||||
db.close((err) => {
|
||||
if (err) {
|
||||
console.error('数据库连接关闭失败:', err.message);
|
||||
} else {
|
||||
console.log('数据库连接已关闭');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
const sqlite3 = require('sqlite3').verbose();
|
||||
const path = require('path');
|
||||
|
||||
// 数据库文件路径
|
||||
const dbPath = path.join(__dirname, 'company_finance.db');
|
||||
|
||||
// 连接数据库
|
||||
const db = new sqlite3.Database(dbPath, (err) => {
|
||||
if (err) {
|
||||
console.error('数据库连接失败:', err.message);
|
||||
return;
|
||||
}
|
||||
console.log('SQLite数据库连接成功');
|
||||
clearData();
|
||||
});
|
||||
|
||||
// 清除数据的函数
|
||||
function clearData() {
|
||||
console.log('开始清除除合作伙伴、商品分类和商品外的所有数据...');
|
||||
|
||||
// 需要保留的表
|
||||
const tablesToKeep = ['suppliers', 'product_categories', 'products'];
|
||||
|
||||
// 需要清除的表(根据常见的ERP系统表结构)
|
||||
const tablesToClear = [
|
||||
'purchase_requests',
|
||||
'purchase_request_items',
|
||||
'inventory_records',
|
||||
'payment_requests',
|
||||
'expense_claims',
|
||||
'expense_claim_details',
|
||||
'projects',
|
||||
'customers',
|
||||
'subcontractors',
|
||||
'quotations',
|
||||
'quotation_items',
|
||||
'contracts',
|
||||
'payment_terms',
|
||||
'advances',
|
||||
'reimbursements',
|
||||
'financial_records',
|
||||
'financial_transactions',
|
||||
'vouchers',
|
||||
'exchange_rates',
|
||||
'users',
|
||||
'roles',
|
||||
'permissions'
|
||||
];
|
||||
|
||||
// 执行清除操作
|
||||
let completed = 0;
|
||||
const total = tablesToClear.length;
|
||||
|
||||
tablesToClear.forEach(table => {
|
||||
db.run(`DELETE FROM ${table}`, (err) => {
|
||||
if (err) {
|
||||
console.warn(`清除${table}表数据失败:`, err.message);
|
||||
} else {
|
||||
console.log(`✓ 已清除${table}表数据`);
|
||||
}
|
||||
|
||||
completed++;
|
||||
if (completed === total) {
|
||||
console.log('\n数据清除完成!');
|
||||
console.log('已保留以下表的数据:');
|
||||
tablesToKeep.forEach(table => console.log(`- ${table}`));
|
||||
db.close();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
const db = require('./db-sqlite');
|
||||
|
||||
async function clearAllData() {
|
||||
try {
|
||||
console.log('开始删除所有测试数据...');
|
||||
|
||||
// 先删除关联表数据
|
||||
await db.query('DELETE FROM project_materials');
|
||||
await db.query('DELETE FROM project_milestones');
|
||||
await db.query('DELETE FROM project_finances');
|
||||
await db.query('DELETE FROM warranty_deposits');
|
||||
await db.query('DELETE FROM project_contracts');
|
||||
await db.query('DELETE FROM subcontracts');
|
||||
await db.query('DELETE FROM construction_logs');
|
||||
await db.query('DELETE FROM budget_quotations');
|
||||
await db.query('DELETE FROM budget_projects');
|
||||
await db.query('DELETE FROM projects');
|
||||
await db.query('DELETE FROM contacts');
|
||||
await db.query('DELETE FROM suppliers');
|
||||
await db.query('DELETE FROM subcontractors');
|
||||
await db.query('DELETE FROM customers');
|
||||
await db.query('DELETE FROM products');
|
||||
await db.query('DELETE FROM categories');
|
||||
await db.query('DELETE FROM exchange_rates');
|
||||
|
||||
// 保留用户数据,因为需要登录
|
||||
// await db.query('DELETE FROM users');
|
||||
|
||||
console.log('所有测试数据删除成功!');
|
||||
console.log('现在您可以使用真实数据进行全流程测试。');
|
||||
} catch (error) {
|
||||
console.error('删除数据失败:', error);
|
||||
} finally {
|
||||
// 关闭数据库连接
|
||||
db.close();
|
||||
}
|
||||
}
|
||||
|
||||
clearAllData();
|
||||
BIN
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user