Update 2026-06-24 10:47:45

This commit is contained in:
yi
2026-06-24 10:47:45 +08:00
parent ffc7eef984
commit 94625d4f3f
29 changed files with 1417 additions and 158 deletions
+530 -62
View File
@@ -1,87 +1,555 @@
# 云海财务系统 # 云海财务系统
## 1. 项目简介 **研发运营**:新觅
**版本**1.0.0 | **更新**2026-06-23
- **项目概述**:云海财务系统是一款高效、智能、专业的企业财务与项目管理平台,能够全方位提升企业的项目监控与资金流转效率 面向工程建设与跨国分公司的企业财务与项目管理平台,实现项目成本、采购供应链与资金流转的一体化管理,支持预支、报销、付款、核销等完整财务审批链路
- **项目定位**:主要面向中大型企业、工程建设及跨国分公司,提供财务报销、项目预算与进度管理服务。
- **核心价值**:实现项目与财务数据的无缝对接,简化审批流,实时监控预算使用情况,确保企业资金健康与合规。
## 2. 整体架构与技术栈 | 层级 | 技术选型 |
|------|----------|
| 后端 | Node.js 18 + Express |
| 前端 | React 18 + TypeScript + Ant Design 5 + Vite |
| 数据库 | PostgreSQL 15 |
| 部署 | Docker Compose(本地构建镜像) |
本系统采用前后端分离的现代化架构,通过 Docker 容器化部署以确保高可用性。 ---
### 技术栈展示: ## 目录
- **后端技术**Node.js + Express
- **前端技术**React 18 + TypeScript + Ant Design 5 + Vite
- **数据库**PostgreSQL 15
- **部署技术**Docker / Docker Compose
### 系统架构图 - [快速开始](#快速开始)
*(暂无架构图,系统分为前端应用容器、后端 API 容器及独立的 PostgreSQL 数据库容器)* - [操作手册](#操作手册)
- [1. 系统概述](#1-系统概述)
- [2. 环境要求](#2-环境要求)
- [3. 安装与启动](#3-安装与启动)
- [4. 登录与账号](#4-登录与账号)
- [5. 界面与模块导航](#5-界面与模块导航)
- [6. 业务模块操作指南](#6-业务模块操作指南)
- [7. 典型业务流程](#7-典型业务流程)
- [8. 系统管理](#8-系统管理)
- [9. 日常运维](#9-日常运维)
- [10. 常见问题与排查](#10-常见问题与排查)
- [11. 附录](#11-附录)
## 3. 项目目录概览 ---
```text ## 快速开始
├── backend/ # 后端服务源码
│ ├── routes/ # API路由 ```bash
│ ├── services/ # 业务逻辑 # 方式一:Docker 一键部署(推荐)
│ ├── migrations/ # 数据库迁移脚本 docker compose -f docker-compose.full.yml up -d --build
│ └── app.js # 后端主程序
├── frontend/ # 前端服务源码 # 检查服务状态
│ ├── src/ # React源码及组件 docker compose -f docker-compose.full.yml ps
│ └── vite.config.ts # Vite配置
├── scripts/ # 初始化脚本 (如 init_sample_data.sql) # 健康检查
└── docker-compose.full.yml # Docker本地部署配置 curl http://localhost:10051/api/health
``` ```
## 4. 核心业务功能 | 服务 | 地址 | 说明 |
|------|------|------|
| 前端 Web | http://localhost:10050 | Nginx 托管 SPAAPI 反向代理至后端 |
| 后端 API | http://localhost:10051 | REST API,健康检查 `/api/health` |
| PostgreSQL | localhost:10052 | 容器内端口 5432,仅调试时直连 |
- **项目管理**:支持项目的创建、进度跟踪、合同与里程碑管理。 | 角色 | 用户名 | 默认密码 |
- **财务管理**:包括财务报销、预支款申请、付款申请与核销流程。 |------|--------|----------|
- **采购与供应链**:商品管理、采购申请、订单管理与入库记录。 | 系统管理员 | `admin` | `X123c321@` |
- **伙伴管理**:集中管理供应商、分包商与客户信息。 | 财务专员 | `finance` | `X123c321@` |
- **数据分析与仪表盘**:直观展示核心财务数据及项目状态。 | 项目经理 | `manager` | `X123c321@` |
| 普通员工 | `employee` | `X123c321@` |
## 5. 实际应用场景示例 > 以上账号由 `scripts/init_sample_data.sql` 初始化。生产环境请立即修改默认密码与 `JWT_SECRET`。
- **跨国分公司财务管控**:海外分公司提交预支与报销申请,总部可在线审核,统一汇率转换及预算扣减。 **项目结构**
- **工程类项目核算**:将施工日志与项目付款节点绑定,确保按进度结算分包商款项。
- **企业日常运营**:员工出差报销、办公采购,全流程数字化审批与记录。
## 6. 帮助解决的核心问题 ```text
company-finance-system/
├── backend/ # Express API 服务
│ ├── routes/ # 业务路由
│ ├── migrations/ # 数据库迁移脚本
│ └── app.js # 入口
├── frontend/ # React 前端
│ └── src/pages/ # 业务页面
├── scripts/
│ ├── init_schema.sql # 完整表结构(Docker 首次初始化)
│ └── init_sample_data.sql # 示例业务数据
└── docker-compose.full.yml # 全栈部署配置
```
- **财务数据孤岛**:项目信息与财务开销割裂,难以追踪真实成本。 **新觅源码库**https://www.xinmi.cloud/
- **审批效率低下**:传统纸质或邮件审批慢、易遗漏。
- **预算超支风险**:缺乏实时的数据监控及超支预警机制。
## 7. 快速开始 ---
### 环境要求 ## 操作手册
- Docker 及 Docker Compose
- Node.js (如需本地独立运行)
### 安装与运行步骤 (基于 Docker) ### 1. 系统概述
本系统内置了 `docker-compose.full.yml` 文件及示例数据初始化脚本,可一键启动全套服务。 #### 1.1 产品简介
1. **一键构建与启动** 云海财务系统是一款高效、智能、专业的企业财务与项目管理平台,主要面向中大型企业、工程建设及跨国分公司,提供
```bash
docker-compose -f docker-compose.full.yml up -d --build
```
2. **检查服务状态**
```bash
docker-compose -f docker-compose.full.yml ps
```
### 访问地址与默认账号 - 项目全生命周期管理与成本核算
- **前端访问地址**`http://localhost:10050` - 预支、报销、付款、核销等财务单据流转
- **后端 API 地址**`http://localhost:10051` - 采购申请、订单、库存与付款计划管理
- **默认管理员账号**`admin` / `X123c321@` (具体密码请参考登录页提示) - 供应商、分包商、客户及物流公司集中维护
- 多语言界面(简体中文、English、ไทย、ລາວ)
### 常见问题 #### 1.2 核心能力
- **数据库未初始化**:如果首次启动未完成数据装载,可执行 `docker-compose down -v` 删除未初始化的数据卷后重新启动。
- **端口冲突**:如果 `10050` 或 `10051` 端口已被占用,请修改 `docker-compose.full.yml` 中的端口映射。
## 8. 运行示例截图 | 模块分组 | 主要功能 |
|----------|----------|
| 项目管理 | 项目建档、合同金额、进度状态、付款节点 |
| 预算报价 | 预算项目创建、报价明细 |
| 施工管理 | 施工总览、施工日志、里程碑跟踪 |
| 审批管理 | 待审批单据、待执行付款 |
| 财务申请 | 预支、报销、付款、核销申请 |
| 财务管理 | 财务概览、汇率、项目成本、预支核销状态 |
| 采购管理 | 商品、采购申请/订单、付款计划、库存 |
| 合作伙伴 | 供应商、分包商、客户、物流公司 |
| 后台管理 | 用户/角色、流程模板、分类、导入、日志、备份 |
*(截图预留位置)* #### 1.3 技术架构
```text
┌─────────────┐ /api/* ┌─────────────┐ SQL ┌──────────────┐
│ 浏览器 │ ──────────────► │ Nginx:80 │ ───────────► │ Express:3000 │
│ :10050 │ │ (frontend) │ proxy │ (backend) │
└─────────────┘ └─────────────┘ └──────┬───────┘
┌──────────────┐
│ PostgreSQL │
│ :5432→10052 │
└──────────────┘
```
![系统架构示意](docs/images/image-20260623-architecture.png)
---
### 2. 环境要求
| 项 | 要求 |
|----|------|
| Docker | 20.10+ |
| Docker Compose | v2+ |
| 磁盘 | 建议 ≥ 2 GB(含数据库与上传文件卷) |
| 浏览器 | Chrome / Edge / Firefox 最新两个主版本 |
本地开发(可选):
| 项 | 要求 |
|----|------|
| Node.js | 18+ |
| npm | 9+ |
| PostgreSQL | 15(或使用 `docker-compose.yml` 仅启动数据库) |
---
### 3. 安装与启动
#### 3.1 Docker 全栈部署(推荐)
`docker-compose.full.yml` 包含三个服务:`postgres``backend``frontend`
```bash
# 构建并后台启动
docker compose -f docker-compose.full.yml up -d --build
# 查看日志
docker compose -f docker-compose.full.yml logs -f backend
# 停止服务
docker compose -f docker-compose.full.yml down
# 重置数据库(会删除数据卷,重新执行 init_sample_data.sql
docker compose -f docker-compose.full.yml down -v
docker compose -f docker-compose.full.yml up -d --build
```
首次启动时,PostgreSQL 容器会自动挂载 `scripts/init_sample_data.sql` 完成表结构与示例数据初始化。
#### 3.2 环境变量(Docker
可在项目根目录创建 `.env` 文件,或在启动前导出变量:
| 变量 | 默认值 | 说明 |
|------|--------|------|
| `DB_USER` | `postgres` | 数据库用户名 |
| `DB_PASSWORD` | `changeme` | 数据库密码 |
| `DB_NAME` | `company_finance` | 数据库名 |
| `JWT_SECRET` | `please-change-this-secret` | JWT 签名密钥,**生产必改** |
| `CORS_ORIGIN` | (空) | 逗号分隔的允许来源;为空时允许所有来源 |
#### 3.3 本地开发
```bash
# 方式一:仅启动数据库
docker compose -f docker-compose.yml up -d
# 后端
cd backend
npm install
# 配置环境变量(参考下表)
npm run dev
# 前端(新终端)
cd frontend
npm install
npm run dev
```
后端本地环境变量(`backend/.env`):
| 变量 | 示例值 | 说明 |
|------|--------|------|
| `DB_HOST` | `localhost` | 数据库主机 |
| `DB_PORT` | `5432` | 数据库端口(Docker 映射为 `10052` |
| `DB_NAME` | `company_finance` | 数据库名 |
| `DB_USER` | `postgres` | 数据库用户 |
| `DB_PASSWORD` | `changeme` | 数据库密码 |
| `PORT` | `3000` | API 监听端口 |
| `JWT_SECRET` | 随机长字符串 | JWT 密钥 |
| `NODE_ENV` | `development` | 运行环境 |
> 注意:`backend/.env.example` 中仍保留 SQLite 示例,当前版本实际使用 PostgreSQL,请以 `db.js` 与 `docker-compose.full.yml` 为准。
---
### 4. 登录与账号
1. 浏览器访问 http://localhost:10050
2. 输入用户名与密码,或点击登录页「测试账户」快捷登录
3. 登录成功后跳转至工作台(`/dashboard`
4. JWT 令牌有效期为 **24 小时**,过期后需重新登录
| 角色 | 用户名 | 权限概述 |
|------|--------|----------|
| 系统管理员 | `admin` | 全部业务功能 + 后台管理(用户、角色、流程、备份等) |
| 财务专员 | `finance` | 财务审批、付款执行、汇率与报表 |
| 项目经理 | `manager` | 项目、施工、预算及关联业务 |
| 普通员工 | `employee` | 发起预支/报销/采购等申请,查看授权范围数据 |
![image-20260624104319931](images/image-20260624104319931.png)
---
### 5. 界面与模块导航
登录后左侧为主业务菜单,右上角可切换语言与个人中心;管理员可通过用户菜单进入「后台管理」(`/admin`)。
| 菜单 | 路由 | 说明 |
|------|------|------|
| 工作台 | `/dashboard` | 核心指标与待办概览 |
| 项目管理 | `/projects` | 项目列表与详情 |
| 预算报价 | `/budget-projects` | 预算项目维护 |
| 施工管理 | `/construction` | 施工总览、日志、里程碑 |
| 待审批 | `/approval` | 待审批单据处理 |
| 待执行 | `/execution` | 待执行付款操作 |
| 预支申请 | `/advances` | 预支款申请 |
| 报销申请 | `/reimbursements` | 费用报销 |
| 付款申请 | `/payment-requests` | 对外付款申请 |
| 核销申请 | `/verification` | 预支/付款核销 |
| 财务概览 | `/finance` | 财务数据总览 |
| 汇率管理 | `/exchange-rates` | 多币种汇率维护 |
| 项目成本 | `/project-cost` | 按项目归集成本 |
| 预支核销状态 | `/advances/verification-status` | 预支款核销进度 |
| 报表分析 | `/reports` | 财务报表与分析 |
| 商品管理 | `/products` | 物料/商品主数据 |
| 采购申请 | `/purchase-requests` | 采购需求发起 |
| 采购订单 | `/purchase-orders` | 采购订单管理 |
| 付款计划 | `/payment-plans` | 采购付款计划 |
| 库存管理 | `/inventory` | 入库与库存台账 |
| 供应商管理 | `/suppliers` | 供应商档案 |
| 分包商管理 | `/subcontractors` | 分包商档案 |
| 客户管理 | `/customers` | 客户档案 |
| 物流管理 | `/logistics-companies` | 物流公司维护 |
![image-20260624104306842](images/image-20260624104306842.png)
---
### 6. 业务模块操作指南
#### 6.1 项目管理
**路径**`/projects`
**功能概述**:维护工程项目基本信息、合同金额、负责人及状态,关联客户与后续财务、施工数据。
| 元素 | 说明 |
|------|------|
| 项目编码 | 唯一标识,如 `P2026-001` |
| 合同金额 | 项目总收入基准 |
| 状态 | 规划中 / 进行中 / 已完成等 |
| 付款节点 | 与 `/api/payment-nodes` 联动,按进度结算 |
![image-20260624104333660](images/image-20260624104333660.png)
#### 6.2 预算报价
**路径**`/budget-projects`
**功能概述**:创建预算项目、维护报价明细,为后续采购与成本对比提供基准。
| 元素 | 说明 |
|------|------|
| 预算项目 | 独立于执行项目的报价载体 |
| 报价明细 | 支持分项录入与汇总 |
![image-20260624104344479](images/image-20260624104344479.png)
#### 6.3 施工管理
**路径**`/construction``/construction/:id/logs``/construction/:id/milestones`
**功能概述**:跟踪工程施工进度,记录施工日志与里程碑,支撑按进度付款。
| 元素 | 说明 |
|------|------|
| 施工总览 | 各项目施工状态一览 |
| 施工日志 | 按日记录现场情况 |
| 里程碑 | 关键节点完成确认 |
#### ![image-20260624104509907](images/image-20260624104509907.png)
#### ![image-20260624104525295](images/image-20260624104525295.png)6.4 财务申请(预支 / 报销 / 付款 / 核销)
**路径**`/advances``/reimbursements``/payment-requests``/verification`
**功能概述**:员工发起各类财务单据,经审批后进入执行环节;预支款需后续报销或核销冲抵。
| 单据类型 | 典型场景 |
|----------|----------|
| 预支申请 | 出差、现场备用金 |
| 报销申请 | 费用实报实销 |
| 付款申请 | 对供应商/分包商付款 |
| 核销申请 | 预支款与发票/实付对齐 |
![image-20260624104539379](images/image-20260624104539379.png)
![image-20260624104548854](images/image-20260624104548854.png)
![image-20260624104557054](images/image-20260624104557054.png)
#### 6.5 审批与执行
**路径**`/approval``/execution`
**功能概述**:审批人处理待办单据;财务人员在「待执行」中完成实际付款操作。
| 环节 | 说明 |
|------|------|
| 待审批 | 按角色权限审批通过或驳回 |
| 待执行 | 审批通过后登记付款执行记录 |
![image-20260624104635658](images/image-20260624104635658.png)
#### 6.6 采购与库存
**路径**`/products``/purchase-requests``/purchase-orders``/payment-plans``/inventory`
**功能概述**:从商品主数据到采购申请、订单、付款计划及入库的全链路管理。
| 环节 | 说明 |
|------|------|
| 采购申请 | 业务部门提出采购需求 |
| 采购订单 | 审批后生成正式订单 |
| 付款计划 | 按合同约定拆分付款期次 |
| 库存管理 | 收货入库与库存查询 |
![image-20260624104653703](images/image-20260624104653703.png)
#### 6.7 合作伙伴
**路径**`/suppliers``/subcontractors``/customers``/logistics-companies`
**功能概述**:集中维护供应商、分包商、客户及物流公司联系人与业务信息。
![image-20260624104704292](images/image-20260624104704292.png)
#### 6.8 财务管理与报表
**路径**`/finance``/exchange-rates``/project-cost``/reports`
**功能概述**:汇总财务收支、维护多币种汇率、按项目分析成本利润,输出报表。
| 功能 | 说明 |
|------|------|
| 汇率管理 | 支持跨国分公司币种换算 |
| 项目成本 | 将采购、人工、分包等费用归集到项目 |
| 报表分析 | 项目成本利润等分析视图 |
![image-20260624104623194](images/image-20260624104623194.png)
---
### 7. 典型业务流程
#### 7.1 员工出差预支与报销
```text
员工提交预支申请 → 项目经理/财务审批 → 财务执行付款
出差结束提交报销 → 审批通过 → 与预支款核销冲抵
```
#### 7.2 工程采购全流程
```text
维护商品主数据 → 提交采购申请 → 审批 → 生成采购订单
制定付款计划 → 收货入库(库存) → 按期付款申请 → 执行付款
```
#### 7.3 工程项目按进度付款
```text
创建项目与客户合同 → 设置付款节点/里程碑 → 施工日志确认进度
分包商付款申请 → 审批 → 付款执行 → 项目成本归集
```
#### 7.4 跨国分公司财务管控
```text
维护汇率 → 海外分公司以本地币种申请 → 总部统一审批
折算为本位币记账 → 项目成本与报表按统一口径输出
```
---
### 8. 系统管理
管理员登录后,点击右上角用户菜单 → **后台管理**,进入 `/admin` 区域。
| 菜单 | 路由 | 功能 |
|------|------|------|
| 用户管理 | `/admin/users` | 增删改用户、重置信息 |
| 角色权限 | `/admin/roles` | 角色与权限配置 |
| 流程管理 | `/admin/process` | 审批流程定义 |
| 工程模板管理 | `/admin/process-templates` | 施工/业务流程模板 |
| 财务分类管理 | `/admin/expense-categories` | 费用科目分类 |
| Excel 批量导入 | `/admin/excel-import` | 批量导入主数据 |
| 系统日志 | `/admin/logs` | 操作与系统日志查询 |
| 数据备份 | `/admin/backup` | 备份记录与恢复入口 |
| 关于系统 | `/admin/about` | 版本与技术信息 |
---
### 9. 日常运维
#### 9.1 健康检查
```bash
curl http://localhost:10051/api/health
```
正常返回 JSON,包含 `success: true` 及 API 端点列表。
#### 9.2 查看日志
```bash
# 后端日志
docker compose -f docker-compose.full.yml logs -f backend
# 前端 Nginx 日志
docker compose -f docker-compose.full.yml logs -f frontend
# 数据库日志
docker compose -f docker-compose.full.yml logs -f postgres
```
#### 9.3 重启服务
```bash
docker compose -f docker-compose.full.yml restart backend frontend
```
#### 9.4 数据备份
- 数据库数据持久化在 Docker 卷 `yunhaifinance-postgres-data`
- 上传文件持久化在 `yunhaifinance-upload-data`(挂载至后端 `/app/uploads`
- 可通过 `pg_dump` 手动备份:
```bash
docker exec yunhaifinance-postgres pg_dump -U postgres company_finance > backup.sql
```
#### 9.5 更新部署
```bash
git pull
docker compose -f docker-compose.full.yml up -d --build
```
---
### 10. 常见问题与排查
| 现象 | 可能原因 | 处理办法 |
|------|----------|----------|
| 前端能开但接口 502 | 后端未就绪或崩溃 | `docker compose logs backend` 查错;确认数据库可连 |
| 登录提示用户名或密码错误 | 初始化脚本未执行 | `docker compose down -v` 后重新 `up --build` |
| 端口被占用 | 10050/10051/10052 冲突 | 修改 `docker-compose.full.yml` 左侧端口映射 |
| 登录后很快掉线 | JWT 过期(24h) | 重新登录;生产可调整后端 `expiresIn` |
| 上传失败 | 请求体超限 | 后端限制 50MBNginx 代理限制 10MB |
| 跨域问题 | `CORS_ORIGIN` 配置 | 生产环境在 `.env` 中设置允许的域名 |
| 数据库连接失败 | 密码或主机名错误 | 核对 `DB_*` 环境变量与 postgres 容器状态 |
---
### 11. 附录
#### 11.1 环境变量速查
| 变量 | 服务 | 默认值 | 说明 |
|------|------|--------|------|
| `DB_HOST` | backend | `postgres`Docker | 数据库主机 |
| `DB_PORT` | backend | `5432` | 数据库端口 |
| `DB_NAME` | backend / postgres | `company_finance` | 数据库名 |
| `DB_USER` | backend / postgres | `postgres` | 数据库用户 |
| `DB_PASSWORD` | backend / postgres | `changeme` | 数据库密码 |
| `JWT_SECRET` | backend | `please-change-this-secret` | JWT 密钥 |
| `CORS_ORIGIN` | backend | (空) | CORS 白名单 |
| `PORT` | backend | `3000` | API 端口 |
| `NODE_ENV` | backend | `production`Docker | 运行环境 |
#### 11.2 主要 API 前缀
| 前缀 | 说明 |
|------|------|
| `/api/auth` | 登录、令牌校验 |
| `/api/projects` | 项目管理 |
| `/api/advances` | 预支款 |
| `/api/reimbursements` | 报销 |
| `/api/payment-requests` | 付款申请 |
| `/api/verifications` | 核销 |
| `/api/purchase-requests` | 采购申请 |
| `/api/purchase-orders` | 采购订单 |
| `/api/inventory` | 库存 |
| `/api/finance-stats` | 财务统计 |
| `/api/health` | 健康检查 |
#### 11.3 默认账号
| 用户名 | 姓名 | 角色 | 邮箱 |
|--------|------|------|------|
| `admin` | 系统管理员 | admin | admin@xinmi.cloud |
| `finance` | 财务专员 | finance | finance@xinmi.cloud |
| `manager` | 项目经理 | manager | manager@xinmi.cloud |
| `employee` | 普通员工 | employee | employee@xinmi.cloud |
默认密码均为 `X123c321@`(与登录页测试账户一致)。
#### 11.4 相关链接
| 名称 | 地址 |
|------|------|
| 新觅源码库 | https://www.xinmi.cloud/ |
---
**新觅**
文档版本:1.0.0 | 2026-06-23
+3 -3
View File
@@ -372,7 +372,7 @@ router.post('/execute', async (req, res) => {
break; break;
} }
case 'advance': { case 'advance': {
const advRow = await db.query('SELECT project_id, applicant_id, applicant, advance_code, purpose FROM advances WHERE id = $1', [source_id]); const advRow = await db.query('SELECT project_id, applicant_id, applicant, advance_code, reason AS purpose FROM advances WHERE id = $1', [source_id]);
if (advRow.rows.length > 0) { if (advRow.rows.length > 0) {
finProjectId = advRow.rows[0].project_id; finProjectId = advRow.rows[0].project_id;
finUserId = advRow.rows[0].applicant_id; finUserId = advRow.rows[0].applicant_id;
@@ -385,10 +385,10 @@ router.post('/execute', async (req, res) => {
break; break;
} }
case 'reimbursement': { case 'reimbursement': {
const reimbRow = await db.query('SELECT project_id, applicant_id, applicant, reimbursement_code, expense_type, description as reimb_desc FROM reimbursements WHERE id = $1', [source_id]); const reimbRow = await db.query('SELECT project_id, user_id, applicant, reimbursement_code, expense_type, reason AS reimb_desc FROM reimbursements WHERE id = $1', [source_id]);
if (reimbRow.rows.length > 0) { if (reimbRow.rows.length > 0) {
finProjectId = reimbRow.rows[0].project_id; finProjectId = reimbRow.rows[0].project_id;
finUserId = reimbRow.rows[0].applicant_id; finUserId = reimbRow.rows[0].user_id;
finUserName = reimbRow.rows[0].applicant; finUserName = reimbRow.rows[0].applicant;
finSourceCode = reimbRow.rows[0].reimbursement_code; finSourceCode = reimbRow.rows[0].reimbursement_code;
finDescription = finDescription || reimbRow.rows[0].reimb_desc || '报销'; finDescription = finDescription || reimbRow.rows[0].reimb_desc || '报销';
+25 -23
View File
@@ -23,7 +23,7 @@ router.get('/', async (req, res) => {
const { purchase_order_id, status } = req.query; const { purchase_order_id, status } = req.query;
let query = ` let query = `
SELECT pp.*, SELECT pp.*,
po.order_code, po.code AS order_code,
po.supplier_id, po.supplier_id,
s.name as supplier_name, s.name as supplier_name,
pr.status as request_status pr.status as request_status
@@ -36,12 +36,12 @@ router.get('/', async (req, res) => {
const conditions = []; const conditions = [];
if (purchase_order_id) { if (purchase_order_id) {
conditions.push('pp.purchase_order_id = $1');
params.push(purchase_order_id); params.push(purchase_order_id);
conditions.push(`pp.purchase_order_id = $${params.length}`);
} }
if (status) { if (status) {
conditions.push('pp.status = $1');
params.push(status); params.push(status);
conditions.push(`pp.status = $${params.length}`);
} }
if (conditions.length > 0) { if (conditions.length > 0) {
@@ -76,7 +76,7 @@ router.get('/:id', async (req, res) => {
const result = await db.query(` const result = await db.query(`
SELECT pp.*, SELECT pp.*,
po.order_code, po.code AS order_code,
po.supplier_id, po.supplier_id,
s.name as supplier_name, s.name as supplier_name,
pr.status as request_status, pr.status as request_status,
@@ -85,7 +85,7 @@ router.get('/:id', async (req, res) => {
LEFT JOIN purchase_orders po ON pp.purchase_order_id = po.id LEFT JOIN purchase_orders po ON pp.purchase_order_id = po.id
LEFT JOIN suppliers s ON po.supplier_id = s.id LEFT JOIN suppliers s ON po.supplier_id = s.id
LEFT JOIN payment_requests pr ON pp.payment_request_id = pr.id LEFT JOIN payment_requests pr ON pp.payment_request_id = pr.id
WHERE pp.id = ? WHERE pp.id = $1
`, [id]); `, [id]);
if (result.rows.length === 0) { if (result.rows.length === 0) {
@@ -155,8 +155,8 @@ router.put('/:id', async (req, res) => {
const result = await db.query(` const result = await db.query(`
UPDATE payment_plans UPDATE payment_plans
SET stage = ?, planned_date = ?, planned_amount = ?, planned_percentage = ?, remark = ?, updated_at = CURRENT_TIMESTAMP SET stage = $1, planned_date = $2, planned_amount = $3, planned_percentage = $4, remark = $5, updated_at = CURRENT_TIMESTAMP
WHERE id = ? WHERE id = $6
`, [stage, planned_date, planned_amount, planned_percentage, remark, id]); `, [stage, planned_date, planned_amount, planned_percentage, remark, id]);
res.json({ res.json({
@@ -216,7 +216,7 @@ router.post('/:id/create-request', async (req, res) => {
SELECT pp.*, po.supplier_id, po.currency, po.project_id SELECT pp.*, po.supplier_id, po.currency, po.project_id
FROM payment_plans pp FROM payment_plans pp
LEFT JOIN purchase_orders po ON pp.purchase_order_id = po.id LEFT JOIN purchase_orders po ON pp.purchase_order_id = po.id
WHERE pp.id = ? WHERE pp.id = $1
`, [id]); `, [id]);
if (planResult.rows.length === 0) { if (planResult.rows.length === 0) {
@@ -229,7 +229,7 @@ router.post('/:id/create-request', async (req, res) => {
return res.status(400).json({ success: false, message: '只能对待付款状态的计划创建付款申请' }); return res.status(400).json({ success: false, message: '只能对待付款状态的计划创建付款申请' });
} }
await db.query('BEGIN TRANSACTION'); await db.query('BEGIN');
try { try {
const requestCode = 'PAY' + new Date().toISOString().slice(0, 10).replace(/-/g, '') + const requestCode = 'PAY' + new Date().toISOString().slice(0, 10).replace(/-/g, '') +
@@ -237,23 +237,25 @@ router.post('/:id/create-request', async (req, res) => {
const requestResult = await db.query(` const requestResult = await db.query(`
INSERT INTO payment_requests INSERT INTO payment_requests
(code, payment_type, purchase_order_id, amount, currency, applicant, request_date, status, created_at) (request_code, payment_type, purchase_order_id, amount, currency, applicant, payment_date, status, created_at)
VALUES ($1, 'material', $2, $3, $4, '系统管理员', CURRENT_DATE, 'pending', CURRENT_TIMESTAMP) VALUES ($1, 'material', $2, $3, $4, '系统管理员', CURRENT_DATE, 'pending', CURRENT_TIMESTAMP)
RETURNING id`, RETURNING id`,
[requestCode, plan.purchase_order_id, plan.planned_amount, plan.currency || 'CNY']); [requestCode, plan.purchase_order_id, plan.planned_amount, plan.currency || 'CNY']);
const requestId = requestResult.rows[0].id;
await db.query(` await db.query(`
UPDATE payment_plans UPDATE payment_plans
SET status = 'requested', payment_request_id = ?, updated_at = CURRENT_TIMESTAMP SET status = 'requested', payment_request_id = $1, updated_at = CURRENT_TIMESTAMP
WHERE id = ? WHERE id = $2
`, [requestResult.lastID, id]); `, [requestId, id]);
await db.query('COMMIT'); await db.query('COMMIT');
res.json({ res.json({
success: true, success: true,
message: '付款申请创建成功', message: '付款申请创建成功',
data: { request_id: requestResult.lastID, request_code: requestCode } data: { request_id: requestId, request_code: requestCode }
}); });
} catch (innerError) { } catch (innerError) {
await db.query('ROLLBACK'); await db.query('ROLLBACK');
@@ -290,20 +292,20 @@ router.post('/:id/mark-paid', async (req, res) => {
return res.status(400).json({ success: false, message: '只能对已申请或已批准的计划标记为已支付' }); return res.status(400).json({ success: false, message: '只能对已申请或已批准的计划标记为已支付' });
} }
await db.query('BEGIN TRANSACTION'); await db.query('BEGIN');
try { try {
await db.query(` await db.query(`
UPDATE payment_plans UPDATE payment_plans
SET status = 'paid', actual_amount = ?, actual_date = ?, updated_at = CURRENT_TIMESTAMP SET status = 'paid', actual_amount = $1, actual_date = $2, updated_at = CURRENT_TIMESTAMP
WHERE id = ? WHERE id = $3
`, [actual_amount || plan.planned_amount, actual_date || new Date().toISOString().slice(0, 10), id]); `, [actual_amount || plan.planned_amount, actual_date || new Date().toISOString().slice(0, 10), id]);
if (plan.payment_request_id) { if (plan.payment_request_id) {
await db.query(` await db.query(`
UPDATE payment_requests UPDATE payment_requests
SET status = 'paid', updated_at = CURRENT_TIMESTAMP SET status = 'paid', updated_at = CURRENT_TIMESTAMP
WHERE id = ? WHERE id = $1
`, [plan.payment_request_id]); `, [plan.payment_request_id]);
} }
@@ -311,7 +313,7 @@ router.post('/:id/mark-paid', async (req, res) => {
SELECT SUM(CASE WHEN status = 'paid' THEN actual_amount ELSE 0 END) as paid_amount, SELECT SUM(CASE WHEN status = 'paid' THEN actual_amount ELSE 0 END) as paid_amount,
SUM(planned_amount) as total_amount SUM(planned_amount) as total_amount
FROM payment_plans FROM payment_plans
WHERE purchase_order_id = ? WHERE purchase_order_id = $1
`, [plan.purchase_order_id]); `, [plan.purchase_order_id]);
const { paid_amount, total_amount } = orderResult.rows[0]; const { paid_amount, total_amount } = orderResult.rows[0];
@@ -325,8 +327,8 @@ router.post('/:id/mark-paid', async (req, res) => {
await db.query(` await db.query(`
UPDATE purchase_orders UPDATE purchase_orders
SET paid_amount = ?, status = ?, updated_at = CURRENT_TIMESTAMP SET paid_amount = $1, status = $2, updated_at = CURRENT_TIMESTAMP
WHERE id = ? WHERE id = $3
`, [paid_amount, newOrderStatus, plan.purchase_order_id]); `, [paid_amount, newOrderStatus, plan.purchase_order_id]);
await db.query('COMMIT'); await db.query('COMMIT');
@@ -360,13 +362,13 @@ router.get('/reminders/upcoming', async (req, res) => {
const result = await db.query(` const result = await db.query(`
SELECT pp.*, SELECT pp.*,
po.order_code, po.code AS order_code,
s.name as supplier_name s.name as supplier_name
FROM payment_plans pp FROM payment_plans pp
LEFT JOIN purchase_orders po ON pp.purchase_order_id = po.id LEFT JOIN purchase_orders po ON pp.purchase_order_id = po.id
LEFT JOIN suppliers s ON po.supplier_id = s.id LEFT JOIN suppliers s ON po.supplier_id = s.id
WHERE pp.status = 'pending' WHERE pp.status = 'pending'
AND pp.planned_date <= date('now', '+' || ? || ' days') AND pp.planned_date <= CURRENT_DATE + ($1 || ' days')::interval
AND pp.planned_date >= CURRENT_DATE AND pp.planned_date >= CURRENT_DATE
ORDER BY pp.planned_date ASC ORDER BY pp.planned_date ASC
`, [days]); `, [days]);
+1 -1
View File
@@ -22,7 +22,7 @@ router.get('/', async (req, res) => {
const result = await db.query(` const result = await db.query(`
SELECT r.*, u.name as user_name, p.name as project_name SELECT r.*, u.name as user_name, p.name as project_name
FROM reimbursements r FROM reimbursements r
LEFT JOIN users u ON r.applicant_id = u.id LEFT JOIN users u ON r.user_id = u.id
LEFT JOIN projects p ON r.project_id = p.id LEFT JOIN projects p ON r.project_id = p.id
ORDER BY r.created_at DESC ORDER BY r.created_at DESC
`); `);
+3
View File
@@ -8,12 +8,15 @@ services:
- "10052:5432" - "10052:5432"
volumes: volumes:
- yunhaifinance-postgres-data:/var/lib/postgresql/data - yunhaifinance-postgres-data:/var/lib/postgresql/data
- ./scripts/init_schema.sql:/docker-entrypoint-initdb.d/001_init_schema.sql:ro
- ./scripts/init_sample_data.sql:/docker-entrypoint-initdb.d/999_init_sample_data.sql:ro - ./scripts/init_sample_data.sql:/docker-entrypoint-initdb.d/999_init_sample_data.sql:ro
environment: environment:
POSTGRES_USER: ${DB_USER:-postgres} POSTGRES_USER: ${DB_USER:-postgres}
POSTGRES_PASSWORD: ${DB_PASSWORD:-changeme} POSTGRES_PASSWORD: ${DB_PASSWORD:-changeme}
POSTGRES_DB: ${DB_NAME:-company_finance} POSTGRES_DB: ${DB_NAME:-company_finance}
POSTGRES_INITDB_ARGS: "--encoding=UTF8 --locale=zh_CN.UTF-8" POSTGRES_INITDB_ARGS: "--encoding=UTF8 --locale=zh_CN.UTF-8"
networks:
- yunhaifinance-network
backend: backend:
build: build:
@@ -43,6 +43,7 @@ import {
} from '@ant-design/icons' } from '@ant-design/icons'
import { useAuthStore } from '../../store/authStore' import { useAuthStore } from '../../store/authStore'
import { useLanguageStore } from '../../store/languageStore' import { useLanguageStore } from '../../store/languageStore'
import { PROJECT_LINKS } from '../../config/project'
import CompanyLogo from '../common/CompanyLogo' import CompanyLogo from '../common/CompanyLogo'
import LanguageSelector from '../common/LanguageSelector' import LanguageSelector from '../common/LanguageSelector'
@@ -228,7 +229,7 @@ const MainLayout: React.FC = () => {
{ {
key: 'xinmi-code', key: 'xinmi-code',
icon: <FileSearchOutlined />, icon: <FileSearchOutlined />,
label: <a href="http://code.xinmi.cloud/" target="_blank" rel="noopener noreferrer" aria-label="新觅源码库"></a> label: <a href={PROJECT_LINKS.xinmi} target="_blank" rel="noopener noreferrer" aria-label="新觅源码库"></a>
} }
], [t, currentLanguage]) ], [t, currentLanguage])
@@ -470,7 +471,7 @@ const MainLayout: React.FC = () => {
<Space direction="vertical" size={2}> <Space direction="vertical" size={2}>
<Text type="secondary" style={{ fontFamily: 'monospace', fontSize: '13px' }}></Text> <Text type="secondary" style={{ fontFamily: 'monospace', fontSize: '13px' }}></Text>
<Text type="secondary"> <Text type="secondary">
- <a href="http://code.xinmi.cloud/" target="_blank" rel="noopener noreferrer" aria-label="访问 新觅源码库 平台">访 </a> <a href={PROJECT_LINKS.xinmi} target="_blank" rel="noopener noreferrer"></a>
</Text> </Text>
</Space> </Space>
</Layout.Footer> </Layout.Footer>
+3
View File
@@ -0,0 +1,3 @@
export const PROJECT_LINKS = {
xinmi: 'https://www.xinmi.cloud/',
} as const
+4 -4
View File
@@ -87,7 +87,7 @@ export default {
}, },
login: { login: {
title: 'Qingyuan Power Lao ERP', title: '云海财务系统',
subtitle: 'Integrated Project Management & Finance Reimbursement Platform', subtitle: 'Integrated Project Management & Finance Reimbursement Platform',
username: 'Username', username: 'Username',
password: 'Password', password: 'Password',
@@ -2208,11 +2208,11 @@ export default {
description: 'System information and version', description: 'System information and version',
systemInfo: 'System Information', systemInfo: 'System Information',
systemName: 'System Name', systemName: 'System Name',
systemNameValue: 'Qingyuan Power Lao ERP', systemNameValue: '云海财务系统',
version: 'System Version', version: 'System Version',
versionValue: 'V1.0.0', versionValue: 'V1.0.0',
devTeam: 'Development Team', devTeam: 'Development Team',
devTeamValue: 'Qingyuan Power IT Department', devTeamValue: 'Xinmi',
onlineDate: 'Launch Date', onlineDate: 'Launch Date',
onlineDateValue: 'March 2026', onlineDateValue: 'March 2026',
techArchitecture: 'Technical Architecture', techArchitecture: 'Technical Architecture',
@@ -2237,7 +2237,7 @@ export default {
connectionStatus: 'Connection Status', connectionStatus: 'Connection Status',
normal: 'Normal', normal: 'Normal',
lastBackup: 'Last Backup', lastBackup: 'Last Backup',
footer: '© 2026 Qingyuan Power Lao ERP System - Version V1.0.0', footer: '© 2026 Yunhai Finance System V1.0.0 | Xinmi',
}, },
backup: { backup: {
+4 -4
View File
@@ -88,7 +88,7 @@ export default {
}, },
login: { login: {
title: 'ຊິງຢວນ ໄຟຟ້າລາວ ERP', title: '云海财务系统',
subtitle: 'ແພລດຟອມຄຸ້ມຄອງໂຄງການ ແລະ ການເງິນ', subtitle: 'ແພລດຟອມຄຸ້ມຄອງໂຄງການ ແລະ ການເງິນ',
username: 'ຊື່ຜູ້ໃຊ້', username: 'ຊື່ຜູ້ໃຊ້',
password: 'ລະຫັດຜ່ານ', password: 'ລະຫັດຜ່ານ',
@@ -2972,10 +2972,10 @@ export default {
technicalSupport: 'ສະ', technicalSupport: 'ສະ',
feedback: 'ຕຳ', feedback: 'ຕຳ',
leaveMessage: 'ຂໍ້', leaveMessage: 'ຂໍ້',
systemNameValue: 'Qingyuan Power Lao ERP', systemNameValue: '云海财务系统',
versionValue: 'V1.0.0', versionValue: 'V1.0.0',
devTeam: 'Development Team', devTeam: 'Development Team',
devTeamValue: 'Qingyuan Power IT Department', devTeamValue: '新觅',
onlineDate: 'Launch Date', onlineDate: 'Launch Date',
onlineDateValue: 'March 2026', onlineDateValue: 'March 2026',
techArchitecture: 'Technical Architecture', techArchitecture: 'Technical Architecture',
@@ -2998,7 +2998,7 @@ export default {
connectionStatus: 'Connection Status', connectionStatus: 'Connection Status',
normal: 'Normal', normal: 'Normal',
lastBackup: 'Last Backup', lastBackup: 'Last Backup',
footer: '© 2026 Qingyuan Power Lao ERP System - Version V1.0.0', footer: '© 2026 云海财务系统 V1.0.0 | 新觅',
}, },
backup: { backup: {
+4 -4
View File
@@ -87,7 +87,7 @@ export default {
}, },
login: { login: {
title: 'ชิงหยวนพาวเวอร์ สปป.ลาว ERP', title: '云海财务系统',
subtitle: 'แพลตฟอร์มการจัดการโครงการและการเบิกค่าใช้จ่ายทางการเงิน', subtitle: 'แพลตฟอร์มการจัดการโครงการและการเบิกค่าใช้จ่ายทางการเงิน',
username: 'ชื่อผู้ใช้', username: 'ชื่อผู้ใช้',
password: 'รหัสผ่าน', password: 'รหัสผ่าน',
@@ -2208,11 +2208,11 @@ export default {
description: 'ข้อมูลระบบและรุ่น', description: 'ข้อมูลระบบและรุ่น',
systemInfo: 'ข้อมูลระบบ', systemInfo: 'ข้อมูลระบบ',
systemName: 'ชื่อระบบ', systemName: 'ชื่อระบบ',
systemNameValue: 'ชิงหยวนพาวเวอร์ สปป.ลาว ERP', systemNameValue: '云海财务系统',
version: 'รุ่นระบบ', version: 'รุ่นระบบ',
versionValue: 'V1.0.0', versionValue: 'V1.0.0',
devTeam: 'ทีมพัฒนา', devTeam: 'ทีมพัฒนา',
devTeamValue: 'ฝ่ายเทคโนโลยีสารสนเทศ ชิงหยวนพาวเวอร์', devTeamValue: '新觅',
onlineDate: 'วันที่เปิดใช้งาน', onlineDate: 'วันที่เปิดใช้งาน',
onlineDateValue: 'มีนาคม 2026', onlineDateValue: 'มีนาคม 2026',
techArchitecture: 'สถาปัตยกรรมทางเทคนิค', techArchitecture: 'สถาปัตยกรรมทางเทคนิค',
@@ -2237,7 +2237,7 @@ export default {
connectionStatus: 'สถานะการเชื่อมต่อ', connectionStatus: 'สถานะการเชื่อมต่อ',
normal: 'ปกติ', normal: 'ปกติ',
lastBackup: 'สำรองข้อมูลล่าสุด', lastBackup: 'สำรองข้อมูลล่าสุด',
footer: '© 2026 ระบบ ERP ชิงหยวนพาวเวอร์ สปป.ลาว - รุ่น V1.0.0', footer: '© 2026 云海财务系统 V1.0.0 | 新觅',
}, },
backup: { backup: {
+2 -2
View File
@@ -2212,7 +2212,7 @@ export default {
version: '系统版本', version: '系统版本',
versionValue: 'V1.0.0', versionValue: 'V1.0.0',
devTeam: '开发团队', devTeam: '开发团队',
devTeamValue: '云海技术部', devTeamValue: '新觅',
onlineDate: '上线日期', onlineDate: '上线日期',
onlineDateValue: '2026年3月', onlineDateValue: '2026年3月',
techArchitecture: '技术架构', techArchitecture: '技术架构',
@@ -2237,7 +2237,7 @@ export default {
connectionStatus: '连接状态', connectionStatus: '连接状态',
normal: '正常', normal: '正常',
lastBackup: '最近备份', lastBackup: '最近备份',
footer: '© 2026 云海财务系统系统 - 版本 V1.0.0', footer: '© 2026 云海财务系统 - 版本 V1.0.0 | 新觅',
}, },
backup: { backup: {
+4 -3
View File
@@ -9,6 +9,7 @@ import {
RocketOutlined RocketOutlined
} from '@ant-design/icons'; } from '@ant-design/icons';
import { useLanguageStore } from '../../store/languageStore'; import { useLanguageStore } from '../../store/languageStore';
import { PROJECT_LINKS } from '../../config/project';
const { Title, Paragraph, Text } = Typography; const { Title, Paragraph, Text } = Typography;
@@ -37,7 +38,7 @@ const AboutPage: React.FC = () => {
size="large" size="large"
shape="round" shape="round"
icon={<RocketOutlined />} icon={<RocketOutlined />}
href="http://code.xinmi.cloud/" href={PROJECT_LINKS.xinmi}
target="_blank" target="_blank"
style={{ style={{
backgroundColor: 'white', backgroundColor: 'white',
@@ -49,7 +50,7 @@ const AboutPage: React.FC = () => {
boxShadow: '0 8px 16px rgba(0,0,0,0.1)' boxShadow: '0 8px 16px rgba(0,0,0,0.1)'
}} }}
> >
</Button> </Button>
</div> </div>
</Card> </Card>
@@ -148,7 +149,7 @@ const AboutPage: React.FC = () => {
</div> </div>
<Divider /> <Divider />
<Descriptions column={1} size="small"> <Descriptions column={1} size="small">
<Descriptions.Item label={t('about.dbName')}>company_finance_db</Descriptions.Item> <Descriptions.Item label={t('about.dbName')}>company_finance</Descriptions.Item>
<Descriptions.Item label={t('about.connectionStatus')}>{t('about.normal')}</Descriptions.Item> <Descriptions.Item label={t('about.connectionStatus')}>{t('about.normal')}</Descriptions.Item>
<Descriptions.Item label={t('about.lastBackup')}>2026-03-19 00:00</Descriptions.Item> <Descriptions.Item label={t('about.lastBackup')}>2026-03-19 00:00</Descriptions.Item>
</Descriptions> </Descriptions>
Binary file not shown.

After

Width:  |  Height:  |  Size: 875 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 875 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 110 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 170 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 136 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 143 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 134 KiB

+191 -49
View File
@@ -1,58 +1,200 @@
SET client_encoding = 'UTF8'; SET client_encoding = 'UTF8';
CREATE TABLE IF NOT EXISTS users ( -- ============================================================
id SERIAL PRIMARY KEY, -- 云海财务系统 - 示例数据
username VARCHAR(255) UNIQUE, -- ============================================================
name VARCHAR(255),
email VARCHAR(255),
phone VARCHAR(255),
role VARCHAR(50),
password_hash VARCHAR(255),
created_at TIMESTAMP,
updated_at TIMESTAMP
);
CREATE TABLE IF NOT EXISTS customers ( -- 1. 用户
id SERIAL PRIMARY KEY,
name VARCHAR(255),
address VARCHAR(255),
remark TEXT,
contact_person VARCHAR(255),
phone VARCHAR(255),
email VARCHAR(255),
created_at TIMESTAMP,
updated_at TIMESTAMP
);
CREATE TABLE IF NOT EXISTS projects (
id SERIAL PRIMARY KEY,
name VARCHAR(255),
project_code VARCHAR(255),
customer_id INTEGER,
project_manager_id INTEGER,
contract_amount NUMERIC,
status VARCHAR(50),
location VARCHAR(255),
created_at TIMESTAMP,
updated_at TIMESTAMP
);
-- 1. 插入用户数据
INSERT INTO users (username, name, email, phone, role, password_hash, created_at, updated_at) VALUES INSERT INTO users (username, name, email, phone, role, password_hash, created_at, updated_at) VALUES
('admin', '系统管理员', 'admin@yunhai.com', '13800000001', 'admin', '$2a$10$xyz', NOW(), NOW()), ('admin', '系统管理员', 'admin@xinmi.cloud', '13800000001', 'admin', '$2b$10$XYs6Rc6LVqeFsmY4cMZdmOy.bhSc4Ib3yFfwj5kt.G.oHNiuZSkZm', NOW(), NOW()),
('finance', '财务专员', 'finance@yunhai.com', '13800000002', 'finance', '$2a$10$xyz', NOW(), NOW()), ('finance', '财务专员', 'finance@xinmi.cloud', '13800000002', 'finance', '$2b$10$XYs6Rc6LVqeFsmY4cMZdmOy.bhSc4Ib3yFfwj5kt.G.oHNiuZSkZm', NOW(), NOW()),
('manager', '项目经理', 'manager@yunhai.com', '13800000003', 'manager', '$2a$10$xyz', NOW(), NOW()), ('manager', '项目经理', 'manager@xinmi.cloud', '13800000003', 'manager', '$2b$10$XYs6Rc6LVqeFsmY4cMZdmOy.bhSc4Ib3yFfwj5kt.G.oHNiuZSkZm', NOW(), NOW()),
('employee', '普通员工', 'employee@yunhai.com', '13800000004', 'employee', '$2a$10$xyz', NOW(), NOW()) ('employee', '普通员工', 'employee@xinmi.cloud', '13800000004', 'employee', '$2b$10$XYs6Rc6LVqeFsmY4cMZdmOy.bhSc4Ib3yFfwj5kt.G.oHNiuZSkZm', NOW(), NOW())
ON CONFLICT (username) DO NOTHING; ON CONFLICT (username) DO NOTHING;
-- 2. 插入客户数据 -- 2. 客户
INSERT INTO customers (name, address, remark, contact_person, phone, email, created_at, updated_at) VALUES INSERT INTO customers (name, address, remark, contact_person, phone, email, created_at, updated_at)
SELECT * FROM (VALUES
('老挝国家电力公司', '万象市', '大客户', 'Somphong', '020-11111111', 'edl@laos.com', NOW(), NOW()), ('老挝国家电力公司', '万象市', '大客户', 'Somphong', '020-11111111', 'edl@laos.com', NOW(), NOW()),
('万象基建集团', '万象市', '长期合作', 'Champa', '020-22222222', 'infra@laos.com', NOW(), NOW()), ('万象基建集团', '万象市', '长期合作', 'Champa', '020-22222222', 'infra@laos.com', NOW(), NOW()),
('琅勃拉邦电力局', '琅勃拉邦', '', 'Keo', '020-33333333', 'lpq@laos.com', NOW(), NOW()); ('琅勃拉邦电力局', '琅勃拉邦', '', 'Keo', '020-33333333', 'lpq@laos.com', NOW(), NOW())
) AS v(name, address, remark, contact_person, phone, email, created_at, updated_at)
WHERE NOT EXISTS (SELECT 1 FROM customers WHERE name = v.name);
-- 3. 插入项目数据 (假设customer_id与上表对应) -- 3. 项目
INSERT INTO projects (name, project_code, customer_id, project_manager_id, contract_amount, status, location, created_at, updated_at) VALUES INSERT INTO projects (name, project_code, customer_id, project_manager_id, contract_amount, status, location, start_date, description, created_at, updated_at)
('万象22kV线路工程', 'P2026-001', 1, 3, 1500000, 'in_progress', '万象市赛塔尼县', NOW(), NOW()), SELECT v.name, v.project_code, c.id, u.id, v.contract_amount, v.status, v.location, v.start_date::date, v.description, NOW(), NOW()
('琅勃拉邦变电站改造', 'P2026-002', 3, 3, 800000, 'planning', '琅勃拉邦', NOW(), NOW()), FROM (VALUES
('南塔电力抢修', 'P2026-003', 1, 3, 300000, 'completed', '南塔省', NOW(), NOW()); ('万象22kV线路工程', 'P2026-001', '老挝国家电力公司', 'manager', 1500000, 'in_progress', '万象市赛塔尼县', '2026-01-15', '22kV架空线路新建工程'),
('琅勃拉邦变电站改造', 'P2026-002', '琅勃拉邦电力局', 'manager', 800000, 'planning', '琅勃拉邦', '2026-03-01', '35kV变电站设备更新改造'),
('南塔电力抢修', 'P2026-003', '老挝国家电力公司', 'manager', 300000, 'completed', '南塔省', '2025-11-01', '台风后应急抢修工程')
) AS v(name, project_code, customer_name, manager_username, contract_amount, status, location, start_date, description)
JOIN customers c ON c.name = v.customer_name
JOIN users u ON u.username = v.manager_username
WHERE NOT EXISTS (SELECT 1 FROM projects WHERE project_code = v.project_code);
-- 4. 供应商
INSERT INTO suppliers (name, address, contact_person, phone, email, product_category, supplier_source, country, remark, created_at, updated_at) VALUES
('万象电力器材有限公司', '万象市工业区', 'Bounmy', '020-55551111', 'sales@pole.la', '电杆横担', '老挝', '老挝', '本地电杆供应商', NOW(), NOW()),
('昆明电缆集团', '云南省昆明市', 'Zhang Wei', '13888880001', 'export@km-cable.cn', '电缆电线', '中国', '中国', '高压电缆主力供应商', NOW(), NOW()),
('深圳开关设备厂', '广东省深圳市', 'Li Ming', '13900001111', 'sales@sz-switch.cn', '开关设备', '中国', '中国', '断路器与隔离开关', NOW(), NOW());
INSERT INTO contacts (entity_id, entity_type, name, position, phone, is_primary, created_at, updated_at) VALUES
(1, 'supplier', 'Bounmy', '销售经理', '020-55551111', 1, NOW(), NOW()),
(2, 'supplier', 'Zhang Wei', '出口经理', '13888880001', 1, NOW(), NOW());
INSERT INTO supplier_payment_infos (supplier_id, account_name, bank_account, bank_name, is_primary, created_at, updated_at) VALUES
(1, '万象电力器材有限公司', 'LA0012345678', 'BCEL Bank', 1, NOW(), NOW()),
(2, '昆明电缆集团', '6222021234567890', '中国工商银行昆明分行', 1, NOW(), NOW());
-- 5. 分包商
INSERT INTO subcontractors (name, address, contact_person, phone, email, specialty, country, score, remark, created_at, updated_at) VALUES
('老挝电力施工一队', '万象市', 'Kham', '020-66662222', 'team1@laos.com', '架空线路施工', '老挝', 95, '经验丰富', NOW(), NOW()),
('万象电气安装公司', '万象市', 'Phone', '020-77773333', 'install@laos.com', '变电站安装', '老挝', 100, '新合作方', NOW(), NOW());
-- 6. 商品分类与商品
INSERT INTO product_categories (name, parent_id) VALUES
('电杆横担', NULL),
('电缆电线', NULL),
('开关设备', NULL);
INSERT INTO product_categories (name, parent_id) VALUES
('混凝土电杆', 1),
('高压电缆', 2),
('断路器', 3);
INSERT INTO products (name, model, category_id, category_name, unit, cost_price, price, brand, specification, source, stock_quantity, status, created_at, updated_at) VALUES
('12米混凝土电杆', 'DP-12M', 4, '混凝土电杆', '', 850, 1200, '万象建材', 'φ190×12000mm', '老挝', 200, 'active', NOW(), NOW()),
('JKLYJ-1-10 架空绝缘导线', 'JKLYJ-10', 5, '高压电缆', '', 8.5, 12, '昆明电缆', '1×10mm²', '中国', 5000, 'active', NOW(), NOW()),
('ZW32-12 真空断路器', 'ZW32-12', 6, '断路器', '', 8500, 12000, '深圳开关', '12kV户外型', '中国', 15, 'active', NOW(), NOW());
-- 7. 汇率
INSERT INTO exchange_rates (currency_code, to_currency_code, rate, effective_date, created_at) VALUES
('CNY', 'LAK', 2900, CURRENT_DATE, NOW()),
('CNY', 'USD', 0.143, CURRENT_DATE, NOW()),
('CNY', 'THB', 4.8, CURRENT_DATE, NOW()),
('USD', 'LAK', 20300, CURRENT_DATE, NOW());
-- 8. 财务分类
INSERT INTO expense_categories (category_level1, category_level2, label, description, sort_order) VALUES
('income', 'contract_payment', '项目合同收款', '客户按合同打款到账', 1),
('project', 'material', '材料采购', '电杆、导线、电缆等', 1),
('project', 'subcontract', '施工分包', '外包施工队费用', 3),
('company', 'salary', '工资薪酬', '办公室管理人员工资', 1)
ON CONFLICT (category_level1, category_level2) DO NOTHING;
-- 9. 预支 / 报销 / 付款
INSERT INTO advances (applicant_id, project_id, amount, currency, amount_cny, reason, advance_date, advance_code, status, applicant, created_at, updated_at)
SELECT u.id, p.id, v.amount, v.currency, v.amount_cny, v.reason, v.advance_date::date, v.advance_code, v.status, v.applicant, NOW(), NOW()
FROM (VALUES
(4, 1, 5000, 'CNY', 5000, '万象项目现场出差备用金', '2026-02-10', 'ADV-20260210-001', 'approved', '普通员工'),
(4, 1, 2000000, 'LAK', 690, '现场零星采购备用金', '2026-03-05', 'ADV-20260305-001', 'pending', '普通员工')
) AS v(applicant_id, project_id, amount, currency, amount_cny, reason, advance_date, advance_code, status, applicant)
JOIN users u ON u.id = v.applicant_id
JOIN projects p ON p.id = v.project_id
ON CONFLICT (advance_code) DO NOTHING;
INSERT INTO reimbursements (user_id, project_id, amount, currency, amount_cny, reason, reimbursement_date, reimbursement_code, status, applicant, expense_type, created_at, updated_at)
SELECT u.id, p.id, v.amount, v.currency, v.amount_cny, v.reason, v.reimbursement_date::date, v.reimbursement_code, v.status, v.applicant, v.expense_type, NOW(), NOW()
FROM (VALUES
(4, 1, 3200, 'CNY', 3200, '万象项目出差住宿费及交通费', '2026-02-20', 'REIM-20260220-001', 'pending', '普通员工', 'travel')
) AS v(user_id, project_id, amount, currency, amount_cny, reason, reimbursement_date, reimbursement_code, status, applicant, expense_type)
JOIN users u ON u.id = v.user_id
JOIN projects p ON p.id = v.project_id
ON CONFLICT (reimbursement_code) DO NOTHING;
INSERT INTO payment_requests (request_code, applicant, payment_date, payee, bank_account, bank_name, amount, currency, reason, status, expense_type, expense_category, project_id, created_at, updated_at)
SELECT v.request_code, v.applicant, v.payment_date::date, v.payee, v.bank_account, v.bank_name, v.amount, v.currency, v.reason, v.status, v.expense_type, v.expense_category, p.id, NOW(), NOW()
FROM (VALUES
('PAY-20260301-001', '财务专员', '2026-03-01', '万象电力器材有限公司', 'LA0012345678', 'BCEL Bank', 120000, 'CNY', '万象项目电杆首付款', 'approved', 'project', 'material', 'P2026-001'),
('PAY-20260315-001', '财务专员', '2026-03-15', '办公室房东', '6222009988776655', '中国建设银行', 15000, 'CNY', '2026年3月办公室租金', 'pending', 'company', 'rent', NULL)
) AS v(request_code, applicant, payment_date, payee, bank_account, bank_name, amount, currency, reason, status, expense_type, expense_category, project_code)
LEFT JOIN projects p ON p.project_code = v.project_code
ON CONFLICT (request_code) DO NOTHING;
-- 10. 采购申请与订单
INSERT INTO purchase_requests (request_code, project_id, applicant, request_date, expense_category, purchase_type, total_amount, currency, status, brief_description, remark, created_at, updated_at)
SELECT v.request_code, p.id, v.applicant, v.request_date::date, v.expense_category, v.purchase_type, v.total_amount, v.currency, v.status, v.brief_description, v.remark, NOW(), NOW()
FROM (VALUES
('PUR-20260310-001', 'P2026-001', '项目经理', '2026-03-10', 'material', 'inventory', 144000, 'CNY', 'approved', '万象项目电杆及金具采购', '预计120根12米电杆'),
('PUR-20260318-001', 'P2026-002', '项目经理', '2026-03-18', 'equipment', 'inventory', 36000, 'CNY', 'pending', '琅勃拉邦变电站断路器采购', '待询价确认型号')
) AS v(request_code, project_code, applicant, request_date, expense_category, purchase_type, total_amount, currency, status, brief_description, remark)
JOIN projects p ON p.project_code = v.project_code
ON CONFLICT (request_code) DO NOTHING;
INSERT INTO purchase_orders (code, purchase_request_id, project_id, supplier_id, currency, status, estimated_amount, remark, created_by, created_at, updated_at)
SELECT 'PO202603120001', pr.id, p.id, s.id, 'CNY', 'confirmed', 144000, '电杆采购正式订单', 'manager', NOW(), NOW()
FROM purchase_requests pr
JOIN projects p ON p.project_code = 'P2026-001'
JOIN suppliers s ON s.name = '万象电力器材有限公司'
WHERE pr.request_code = 'PUR-20260310-001'
ON CONFLICT (code) DO NOTHING;
INSERT INTO purchase_order_items (order_id, product_id, product_name, unit, quantity, unit_price, total_price, created_at) VALUES
(1, 1, '12米混凝土电杆', '', 120, 1200, 144000, NOW());
INSERT INTO payment_plans (purchase_order_id, project_id, plan_name, stage, planned_date, planned_amount, planned_percentage, status, created_at, updated_at) VALUES
(1, 1, '电杆采购首付款', '首付30%', '2026-03-15', 43200, 30, 'pending', NOW(), NOW()),
(1, 1, '电杆采购尾款', '尾款70%', '2026-04-15', 100800, 70, 'pending', NOW(), NOW());
-- 11. 付款节点与记录
INSERT INTO payment_nodes (project_id, name, amount, due_date, status, created_at, updated_at) VALUES
(1, '合同签订预付款', 450000, '2026-02-01', 'completed', NOW(), NOW()),
(1, '材料到场进度款', 450000, '2026-04-01', 'pending', NOW(), NOW()),
(1, '竣工验收尾款', 600000, '2026-08-01', 'pending', NOW(), NOW());
INSERT INTO payment_records (node_id, amount, payment_date, method, status, created_at) VALUES
(1, 450000, '2026-02-05', '银行转账', 'completed', NOW());
-- 12. 物流公司
INSERT INTO logistics_companies (code, name, address, phone, status, remark, created_at, updated_at) VALUES
('LC-001', '中老跨境物流', '万象市物流园', '020-88889999', 'active', '中国至老挝跨境运输', NOW(), NOW()),
('LC-002', '万象本地运输', '万象市', '020-88887777', 'active', '老挝境内二次转运', NOW(), NOW());
-- 13. 施工日志
INSERT INTO construction_logs (project_id, log_date, weather, recorded_by, work_content, next_plan, issues, created_at) VALUES
(1, '2026-03-15', '', '项目经理', '完成A段电杆基坑开挖20基', '继续B段基坑开挖', '部分场地积水需排水', NOW()),
(1, '2026-03-16', '多云', '项目经理', '电杆到货验收120根,合格118根', '组织电杆组立', '2根电杆表面裂纹退回', NOW());
-- 14. 工程模板
INSERT INTO project_type_templates (name, description, phases, is_system, created_at, updated_at) VALUES
('架空线路工程', '架空线路项目标准阶段', '[
{"order":1,"name":"勘察设计","type":"serial","depends":[],"sub_items":["线路勘察","图纸设计"]},
{"order":2,"name":"物资采购","type":"parallel","depends":[1],"sub_items":["电杆采购","导线采购"]},
{"order":3,"name":"基础施工","type":"serial","depends":[1],"sub_items":["杆坑开挖","基础浇筑"]},
{"order":4,"name":"架线施工","type":"serial","depends":[2,3],"sub_items":["电杆组立","放线紧线"]},
{"order":5,"name":"竣工验收","type":"serial","depends":[4],"sub_items":[]}
]'::jsonb, true, NOW(), NOW());
INSERT INTO project_phases (project_id, phase_name, phase_order, phase_type, status, sub_items, created_at) VALUES
(1, '勘察设计', 1, 'serial', 'completed', '[{"name":"现场勘察","done":true},{"name":"图纸设计","done":true}]'::jsonb, NOW()),
(1, '物资采购', 2, 'parallel', 'in_progress', '[{"name":"电杆采购","done":true},{"name":"导线采购","done":false}]'::jsonb, NOW()),
(1, '基础施工', 3, 'serial', 'pending', '[{"name":"杆坑开挖","done":false}]'::jsonb, NOW());
-- 15. 预算项目
INSERT INTO budget_projects (budget_code, name, customer_id, business_manager_id, status, total_amount, currency, survey_notes, remark, created_by, created_at, updated_at)
SELECT 'BJ0001', '万象22kV线路工程预算', c.id, u.id, 'approved', 1450000, 'CNY', '初版预算报价', '含材料、施工、运输', u.id, NOW(), NOW()
FROM customers c, users u
WHERE c.name = '老挝国家电力公司' AND u.username = 'manager'
AND NOT EXISTS (SELECT 1 FROM budget_projects WHERE budget_code = 'BJ0001');
INSERT INTO budget_quotations (budget_project_id, version, quotation_date, amount, currency, notes, status, created_at)
SELECT bp.id, 1, '2026-01-10', 1450000, 'CNY', '含材料、施工、运输', 'approved', NOW()
FROM budget_projects bp
WHERE bp.budget_code = 'BJ0001'
AND NOT EXISTS (SELECT 1 FROM budget_quotations WHERE budget_project_id = bp.id);
-- 16. 财务账本记录
INSERT INTO financial_records (record_code, txn_type, category_level1, category_level2, project_id, user_name, amount_original, currency, exchange_rate, amount_cny, record_date, counterparty_name, description, status, created_at, updated_at)
SELECT v.record_code, v.txn_type, v.category_level1, v.category_level2, p.id, v.user_name, v.amount_original, v.currency, v.exchange_rate, v.amount_cny, v.record_date::date, v.counterparty_name, v.description, v.status, NOW(), NOW()
FROM (VALUES
('FR-INC-20260205-001', 'income', 'income', 'contract_payment', 'P2026-001', '财务专员', 450000, 'CNY', 1, 450000, '2026-02-05', '老挝国家电力公司', '万象项目合同预付款到账', 'confirmed'),
('FR-EXP-20260312-001', 'expense', 'project', 'material', 'P2026-001', '财务专员', 43200, 'CNY', 1, 43200, '2026-03-12', '万象电力器材有限公司', '电杆采购首付款', 'confirmed'),
('FR-EXP-20260315-001', 'expense', 'company', 'rent', NULL, '财务专员', 15000, 'CNY', 1, 15000, '2026-03-15', '办公室房东', '3月办公室租金', 'confirmed')
) AS v(record_code, txn_type, category_level1, category_level2, project_code, user_name, amount_original, currency, exchange_rate, amount_cny, record_date, counterparty_name, description, status)
LEFT JOIN projects p ON p.project_code = v.project_code
ON CONFLICT (record_code) DO NOTHING;
-- 17. 库存记录
INSERT INTO inventory_records (record_type, project_id, purchase_request_id, product_id, quantity, unit_price, total_amount, record_date, operator, remark, created_at) VALUES
('in', 1, 1, 1, 118, 1200, 141600, '2026-03-16', '项目经理', '电杆验收入库(2根退回)', NOW());
+639
View File
@@ -0,0 +1,639 @@
SET client_encoding = 'UTF8';
-- ============================================================
-- 云海财务系统 - PostgreSQL 完整表结构
-- ============================================================
CREATE TABLE IF NOT EXISTS users (
id SERIAL PRIMARY KEY,
username VARCHAR(255) UNIQUE,
name VARCHAR(255),
email VARCHAR(255),
phone VARCHAR(255),
role VARCHAR(50),
avatar TEXT,
passport TEXT,
driver_license TEXT,
password_hash VARCHAR(255),
is_active BOOLEAN DEFAULT true,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS customers (
id SERIAL PRIMARY KEY,
name VARCHAR(255),
address VARCHAR(255),
remark TEXT,
contact_person VARCHAR(255),
phone VARCHAR(255),
email VARCHAR(255),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS projects (
id SERIAL PRIMARY KEY,
name VARCHAR(255),
project_code VARCHAR(255),
customer_id INTEGER REFERENCES customers(id),
project_manager_id INTEGER REFERENCES users(id),
contract_amount NUMERIC,
start_date DATE,
end_date DATE,
description TEXT,
status VARCHAR(50) DEFAULT 'planning',
location VARCHAR(255),
type_template_id INTEGER,
current_phase VARCHAR(50),
phase_progress NUMERIC(5,2) DEFAULT 0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS contacts (
id SERIAL PRIMARY KEY,
entity_id INTEGER NOT NULL,
entity_type VARCHAR(50) NOT NULL,
name VARCHAR(255),
position VARCHAR(255),
phone VARCHAR(255),
is_primary INTEGER DEFAULT 0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS suppliers (
id SERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL,
address TEXT,
contact_person VARCHAR(255),
phone VARCHAR(255),
email VARCHAR(255),
product_category VARCHAR(255),
supplier_source VARCHAR(100),
supply_category TEXT,
country TEXT,
remark TEXT,
status TEXT DEFAULT 'active',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS supplier_payment_infos (
id SERIAL PRIMARY KEY,
supplier_id INTEGER REFERENCES suppliers(id) ON DELETE CASCADE,
account_name TEXT,
bank_account TEXT,
bank_name TEXT,
qr_code TEXT,
is_primary INTEGER DEFAULT 0,
is_default BOOLEAN DEFAULT false,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS subcontractors (
id SERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL,
address TEXT,
contact_person VARCHAR(255),
phone VARCHAR(255),
email VARCHAR(255),
specialty TEXT,
category TEXT,
country TEXT,
remark TEXT,
score INTEGER DEFAULT 100,
initial_score INTEGER DEFAULT 100,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS subcontractor_payment_infos (
id SERIAL PRIMARY KEY,
subcontractor_id INTEGER REFERENCES subcontractors(id) ON DELETE CASCADE,
account_name TEXT,
bank_name TEXT,
bank_account TEXT,
qr_code TEXT,
is_primary INTEGER DEFAULT 0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS customer_payment_infos (
id SERIAL PRIMARY KEY,
customer_id INTEGER REFERENCES customers(id) ON DELETE CASCADE,
account_name TEXT,
bank_name TEXT,
bank_account TEXT,
qr_code TEXT,
is_primary INTEGER DEFAULT 0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS product_categories (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL,
parent_id INTEGER REFERENCES product_categories(id)
);
CREATE TABLE IF NOT EXISTS products (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL,
model TEXT,
category_id INTEGER REFERENCES product_categories(id),
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
);
CREATE TABLE IF NOT EXISTS exchange_rates (
id SERIAL PRIMARY KEY,
currency_code TEXT NOT NULL,
to_currency_code TEXT NOT NULL,
rate REAL NOT NULL,
effective_date DATE NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS advances (
id SERIAL PRIMARY KEY,
applicant_id INTEGER REFERENCES users(id),
project_id INTEGER REFERENCES projects(id),
amount REAL NOT NULL DEFAULT 0,
currency TEXT DEFAULT 'CNY',
amount_cny REAL DEFAULT 0,
reason TEXT,
advance_date DATE,
advance_code TEXT UNIQUE,
status TEXT DEFAULT 'pending',
applicant TEXT,
attachments TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS reimbursements (
id SERIAL PRIMARY KEY,
user_id INTEGER REFERENCES users(id),
project_id INTEGER REFERENCES projects(id),
amount REAL NOT NULL DEFAULT 0,
currency TEXT DEFAULT 'CNY',
amount_cny REAL DEFAULT 0,
reason TEXT,
reimbursement_date DATE,
reimbursement_code TEXT UNIQUE,
status TEXT DEFAULT 'pending',
applicant TEXT,
expense_type TEXT,
detail_items TEXT,
attachments TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS payment_requests (
id SERIAL PRIMARY KEY,
request_code TEXT UNIQUE,
applicant TEXT,
payment_date DATE,
payee TEXT,
bank_account TEXT,
bank_name TEXT,
amount REAL DEFAULT 0,
currency TEXT DEFAULT 'CNY',
reason TEXT,
status TEXT DEFAULT 'pending',
detail_items TEXT,
attachments TEXT,
payee_type TEXT,
payee_id INTEGER,
expense_type TEXT DEFAULT 'company',
expense_category TEXT,
project_id INTEGER REFERENCES projects(id),
purchase_request_id INTEGER,
purchase_order_id INTEGER,
payment_type TEXT DEFAULT 'company',
logistics_company_id INTEGER,
logistics_document_url TEXT,
driver_phone TEXT,
cargo_weight REAL,
transport_distance REAL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS verifications (
id SERIAL PRIMARY KEY,
advance_id INTEGER REFERENCES advances(id),
amount REAL DEFAULT 0,
currency TEXT DEFAULT 'CNY',
reason TEXT,
verification_date DATE,
verification_code TEXT UNIQUE,
status TEXT DEFAULT 'pending',
applicant TEXT,
advance_code TEXT,
advance_amount REAL,
detail_items TEXT,
attachments TEXT,
expense_type TEXT,
project_id INTEGER REFERENCES projects(id),
settlement TEXT,
settlement_amount REAL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS executions (
id SERIAL PRIMARY KEY,
apply_id INTEGER,
apply_type TEXT,
action TEXT,
execute_method TEXT,
voucher_no TEXT,
remark TEXT,
reject_reason TEXT,
voucher_files TEXT,
operator TEXT,
operator_role TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS purchase_requests (
id SERIAL PRIMARY KEY,
request_code TEXT UNIQUE NOT NULL,
project_id INTEGER NOT NULL REFERENCES projects(id),
applicant TEXT,
request_date DATE,
supplier_id INTEGER REFERENCES suppliers(id),
supplier_name TEXT,
expense_category TEXT,
purchase_type TEXT DEFAULT 'inventory',
total_amount REAL NOT NULL DEFAULT 0,
currency TEXT DEFAULT 'CNY',
status TEXT DEFAULT 'pending',
remark TEXT,
remarks TEXT,
brief_description TEXT,
attachments TEXT,
expected_date DATE,
execute_date DATE,
execute_method TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS purchase_request_items (
id SERIAL PRIMARY KEY,
purchase_request_id INTEGER NOT NULL REFERENCES purchase_requests(id) ON DELETE CASCADE,
product_id INTEGER REFERENCES products(id),
product_name TEXT NOT NULL,
specification TEXT,
unit TEXT,
quantity REAL NOT NULL DEFAULT 0,
unit_price REAL NOT NULL DEFAULT 0,
total_price REAL NOT NULL DEFAULT 0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS purchase_orders (
id SERIAL PRIMARY KEY,
code TEXT UNIQUE NOT NULL,
purchase_request_id INTEGER REFERENCES purchase_requests(id),
project_id INTEGER REFERENCES projects(id),
supplier_id INTEGER REFERENCES suppliers(id),
supplier_country TEXT DEFAULT 'Laos',
currency TEXT DEFAULT 'CNY',
status TEXT DEFAULT 'draft',
estimated_amount REAL DEFAULT 0,
paid_amount REAL DEFAULT 0,
contract_url TEXT,
quotation_url TEXT,
actual_delivery_date DATE,
expected_delivery_date DATE,
receiving_status VARCHAR(20) DEFAULT 'pending',
remark TEXT,
created_by TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS purchase_order_items (
id SERIAL PRIMARY KEY,
order_id INTEGER NOT NULL REFERENCES purchase_orders(id) ON DELETE CASCADE,
product_id INTEGER REFERENCES products(id),
product_name TEXT,
specification TEXT,
unit TEXT,
quantity REAL DEFAULT 0,
unit_price REAL DEFAULT 0,
total_price REAL DEFAULT 0,
received_quantity REAL DEFAULT 0,
verified_quantity REAL DEFAULT 0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS payment_plans (
id SERIAL PRIMARY KEY,
purchase_order_id INTEGER REFERENCES purchase_orders(id),
project_id INTEGER REFERENCES projects(id),
plan_name TEXT,
stage TEXT,
planned_date DATE,
planned_amount REAL,
planned_percentage REAL,
actual_amount REAL DEFAULT 0,
actual_date DATE,
payment_request_id INTEGER,
reminder_days INTEGER DEFAULT 3,
amount REAL DEFAULT 0,
due_date DATE,
status TEXT DEFAULT 'pending',
remark TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS payment_nodes (
id SERIAL PRIMARY KEY,
project_id INTEGER REFERENCES projects(id),
name TEXT,
amount REAL DEFAULT 0,
due_date DATE,
status TEXT DEFAULT 'pending',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS payment_records (
id SERIAL PRIMARY KEY,
node_id INTEGER REFERENCES payment_nodes(id),
amount REAL DEFAULT 0,
payment_date DATE,
method TEXT,
status TEXT DEFAULT 'completed',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS inventory_records (
id SERIAL PRIMARY KEY,
record_type TEXT NOT NULL,
project_id INTEGER REFERENCES projects(id),
purchase_request_id INTEGER REFERENCES purchase_requests(id),
product_id INTEGER NOT NULL REFERENCES products(id),
quantity REAL NOT NULL DEFAULT 0,
unit_price REAL,
total_amount REAL,
record_date DATE NOT NULL,
operator TEXT,
remark TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS logistics_companies (
id SERIAL PRIMARY KEY,
code TEXT UNIQUE NOT NULL,
name TEXT NOT NULL,
address TEXT,
phone TEXT,
email TEXT,
quotation_description TEXT,
status TEXT DEFAULT 'active',
remark TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS logistics_company_contacts (
id SERIAL PRIMARY KEY,
logistics_company_id INTEGER REFERENCES logistics_companies(id) ON DELETE CASCADE,
name TEXT,
phone TEXT,
position TEXT,
is_primary INTEGER DEFAULT 0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS logistics_company_bank_accounts (
id SERIAL PRIMARY KEY,
logistics_company_id INTEGER REFERENCES logistics_companies(id) ON DELETE CASCADE,
account_name TEXT,
bank_name TEXT,
account_number TEXT,
is_default INTEGER DEFAULT 0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS construction_logs (
id SERIAL PRIMARY KEY,
project_id INTEGER REFERENCES projects(id) ON DELETE CASCADE,
log_date DATE NOT NULL,
weather TEXT,
recorded_by TEXT,
work_content TEXT,
next_plan TEXT,
issues TEXT,
photos TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS project_type_templates (
id SERIAL PRIMARY KEY,
name VARCHAR(100) NOT NULL,
description TEXT,
phases JSONB NOT NULL DEFAULT '[]',
is_system BOOLEAN DEFAULT false,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS project_phases (
id SERIAL PRIMARY KEY,
project_id INTEGER REFERENCES projects(id) ON DELETE CASCADE,
template_key VARCHAR(50),
phase_name VARCHAR(100) NOT NULL,
phase_order INTEGER NOT NULL,
phase_type VARCHAR(20) DEFAULT 'serial',
depends_on INTEGER[],
status VARCHAR(20) DEFAULT 'pending',
started_at TIMESTAMP,
completed_at TIMESTAMP,
completed_by INTEGER REFERENCES users(id),
remark TEXT,
photos TEXT[],
sub_items JSONB DEFAULT '[]',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS budget_projects (
id SERIAL PRIMARY KEY,
budget_code TEXT,
name TEXT NOT NULL,
customer_id INTEGER REFERENCES customers(id),
business_manager_id INTEGER REFERENCES users(id),
location TEXT,
survey_date DATE,
intermediary_name TEXT,
intermediary_fee_type TEXT DEFAULT 'fixed',
intermediary_fee REAL DEFAULT 0,
customer_requirements TEXT,
survey_notes TEXT,
status TEXT DEFAULT 'draft',
total_amount REAL DEFAULT 0,
currency TEXT DEFAULT 'CNY',
remark TEXT,
created_by INTEGER REFERENCES users(id),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS budget_quotations (
id SERIAL PRIMARY KEY,
budget_project_id INTEGER REFERENCES budget_projects(id) ON DELETE CASCADE,
version INTEGER DEFAULT 1,
quotation_date DATE,
amount REAL DEFAULT 0,
currency TEXT DEFAULT 'CNY',
file_url TEXT,
notes TEXT,
status TEXT DEFAULT 'draft',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS budget_attachments (
id SERIAL PRIMARY KEY,
budget_project_id INTEGER REFERENCES budget_projects(id) ON DELETE CASCADE,
file_url TEXT,
file_type TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS project_contracts (
id SERIAL PRIMARY KEY,
project_id INTEGER REFERENCES projects(id) ON DELETE CASCADE,
contract_no TEXT,
contract_amount REAL,
sign_date DATE,
file_url TEXT,
remark TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS project_milestones (
id SERIAL PRIMARY KEY,
project_id INTEGER REFERENCES projects(id) ON DELETE CASCADE,
milestone_name TEXT,
amount REAL DEFAULT 0,
due_date DATE,
status TEXT DEFAULT 'pending',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS project_receipts (
id SERIAL PRIMARY KEY,
project_id INTEGER REFERENCES projects(id),
milestone_id INTEGER REFERENCES project_milestones(id),
amount REAL DEFAULT 0,
receipt_date DATE,
financial_record_id INTEGER,
remark TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS subcontractor_events (
id SERIAL PRIMARY KEY,
subcontractor_id INTEGER REFERENCES subcontractors(id),
project_id INTEGER REFERENCES projects(id),
event_type VARCHAR(10),
score_change INTEGER,
event_date DATE,
title VARCHAR(200),
description TEXT,
created_by INTEGER REFERENCES users(id),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- 财务分类与账本(与 migrations 006/007 一致)
CREATE TABLE IF NOT EXISTS expense_categories (
id SERIAL PRIMARY KEY,
category_level1 TEXT NOT NULL,
category_level2 TEXT NOT NULL,
label TEXT NOT NULL,
description TEXT,
sort_order INTEGER DEFAULT 0,
is_active BOOLEAN DEFAULT true,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE(category_level1, category_level2)
);
CREATE TABLE IF NOT EXISTS financial_records (
id SERIAL PRIMARY KEY,
record_code TEXT UNIQUE NOT NULL,
txn_type TEXT NOT NULL CHECK (txn_type IN ('income', 'expense')),
category_level1 TEXT NOT NULL,
category_level2 TEXT NOT NULL,
project_id INTEGER REFERENCES projects(id),
user_id INTEGER REFERENCES users(id),
user_name TEXT,
amount_original REAL NOT NULL DEFAULT 0,
currency TEXT NOT NULL DEFAULT 'CNY',
exchange_rate REAL NOT NULL DEFAULT 1,
amount_cny REAL NOT NULL DEFAULT 0,
record_date DATE NOT NULL,
counterparty_name TEXT,
counterparty_type TEXT,
counterparty_id INTEGER,
source TEXT NOT NULL DEFAULT 'manual',
source_id INTEGER,
source_code TEXT,
description TEXT,
voucher_no TEXT,
attachments TEXT,
status TEXT NOT NULL DEFAULT 'confirmed' CHECK (status IN ('confirmed', 'pending', 'voided')),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_products_category ON products(category_id);
CREATE INDEX IF NOT EXISTS idx_purchase_requests_project ON purchase_requests(project_id);
CREATE INDEX IF NOT EXISTS idx_financial_records_project ON financial_records(project_id);
CREATE INDEX IF NOT EXISTS idx_expense_categories_level1 ON expense_categories(category_level1);
-- 兼容已存在的旧 projects 表,补充缺失字段
ALTER TABLE projects ADD COLUMN IF NOT EXISTS start_date DATE;
ALTER TABLE projects ADD COLUMN IF NOT EXISTS end_date DATE;
ALTER TABLE projects ADD COLUMN IF NOT EXISTS description TEXT;
ALTER TABLE projects ADD COLUMN IF NOT EXISTS type_template_id INTEGER;
ALTER TABLE projects ADD COLUMN IF NOT EXISTS current_phase VARCHAR(50);
ALTER TABLE projects ADD COLUMN IF NOT EXISTS phase_progress NUMERIC(5,2) DEFAULT 0;
-- 兼容已存在库:补充路由依赖字段
ALTER TABLE users ADD COLUMN IF NOT EXISTS is_active BOOLEAN DEFAULT true;
UPDATE users SET is_active = true WHERE is_active IS NULL;
ALTER TABLE budget_projects ADD COLUMN IF NOT EXISTS budget_code TEXT;
ALTER TABLE budget_projects ADD COLUMN IF NOT EXISTS business_manager_id INTEGER REFERENCES users(id);
ALTER TABLE budget_projects ADD COLUMN IF NOT EXISTS location TEXT;
ALTER TABLE budget_projects ADD COLUMN IF NOT EXISTS survey_date DATE;
ALTER TABLE budget_projects ADD COLUMN IF NOT EXISTS intermediary_name TEXT;
ALTER TABLE budget_projects ADD COLUMN IF NOT EXISTS intermediary_fee_type TEXT DEFAULT 'fixed';
ALTER TABLE budget_projects ADD COLUMN IF NOT EXISTS intermediary_fee REAL DEFAULT 0;
ALTER TABLE budget_projects ADD COLUMN IF NOT EXISTS customer_requirements TEXT;
ALTER TABLE budget_projects ADD COLUMN IF NOT EXISTS survey_notes TEXT;
ALTER TABLE reimbursements ADD COLUMN IF NOT EXISTS approval_remark TEXT;