diff --git a/README.md b/README.md index 24fafbc..6d7e313 100644 --- a/README.md +++ b/README.md @@ -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路由 -│ ├── services/ # 业务逻辑 -│ ├── migrations/ # 数据库迁移脚本 -│ └── app.js # 后端主程序 -├── frontend/ # 前端服务源码 -│ ├── src/ # React源码及组件 -│ └── vite.config.ts # Vite配置 -├── scripts/ # 初始化脚本 (如 init_sample_data.sql) -└── docker-compose.full.yml # Docker本地部署配置 +## 快速开始 + +```bash +# 方式一:Docker 一键部署(推荐) +docker compose -f docker-compose.full.yml up -d --build + +# 检查服务状态 +docker compose -f docker-compose.full.yml ps + +# 健康检查 +curl http://localhost:10051/api/health ``` -## 4. 核心业务功能 +| 服务 | 地址 | 说明 | +|------|------|------| +| 前端 Web | http://localhost:10050 | Nginx 托管 SPA,API 反向代理至后端 | +| 后端 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、ไทย、ລາວ) -### 常见问题 -- **数据库未初始化**:如果首次启动未完成数据装载,可执行 `docker-compose down -v` 删除未初始化的数据卷后重新启动。 -- **端口冲突**:如果 `10050` 或 `10051` 端口已被占用,请修改 `docker-compose.full.yml` 中的端口映射。 +#### 1.2 核心能力 -## 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` | +| 上传失败 | 请求体超限 | 后端限制 50MB;Nginx 代理限制 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 diff --git a/backend/routes/payment-execution.js b/backend/routes/payment-execution.js index 4c4b71d..9a9c850 100644 --- a/backend/routes/payment-execution.js +++ b/backend/routes/payment-execution.js @@ -372,7 +372,7 @@ router.post('/execute', async (req, res) => { break; } 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) { finProjectId = advRow.rows[0].project_id; finUserId = advRow.rows[0].applicant_id; @@ -385,10 +385,10 @@ router.post('/execute', async (req, res) => { break; } 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) { finProjectId = reimbRow.rows[0].project_id; - finUserId = reimbRow.rows[0].applicant_id; + finUserId = reimbRow.rows[0].user_id; finUserName = reimbRow.rows[0].applicant; finSourceCode = reimbRow.rows[0].reimbursement_code; finDescription = finDescription || reimbRow.rows[0].reimb_desc || '报销'; diff --git a/backend/routes/payment-plans.js b/backend/routes/payment-plans.js index 1982d03..1424f7c 100644 --- a/backend/routes/payment-plans.js +++ b/backend/routes/payment-plans.js @@ -23,7 +23,7 @@ router.get('/', async (req, res) => { const { purchase_order_id, status } = req.query; let query = ` SELECT pp.*, - po.order_code, + po.code AS order_code, po.supplier_id, s.name as supplier_name, pr.status as request_status @@ -36,12 +36,12 @@ router.get('/', async (req, res) => { const conditions = []; if (purchase_order_id) { - conditions.push('pp.purchase_order_id = $1'); params.push(purchase_order_id); + conditions.push(`pp.purchase_order_id = $${params.length}`); } if (status) { - conditions.push('pp.status = $1'); params.push(status); + conditions.push(`pp.status = $${params.length}`); } if (conditions.length > 0) { @@ -76,7 +76,7 @@ router.get('/:id', async (req, res) => { const result = await db.query(` SELECT pp.*, - po.order_code, + po.code AS order_code, po.supplier_id, s.name as supplier_name, 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 suppliers s ON po.supplier_id = s.id LEFT JOIN payment_requests pr ON pp.payment_request_id = pr.id - WHERE pp.id = ? + WHERE pp.id = $1 `, [id]); if (result.rows.length === 0) { @@ -155,8 +155,8 @@ router.put('/:id', async (req, res) => { const result = await db.query(` UPDATE payment_plans - SET stage = ?, planned_date = ?, planned_amount = ?, planned_percentage = ?, remark = ?, updated_at = CURRENT_TIMESTAMP - WHERE id = ? + SET stage = $1, planned_date = $2, planned_amount = $3, planned_percentage = $4, remark = $5, updated_at = CURRENT_TIMESTAMP + WHERE id = $6 `, [stage, planned_date, planned_amount, planned_percentage, remark, id]); res.json({ @@ -216,7 +216,7 @@ router.post('/:id/create-request', async (req, res) => { SELECT pp.*, po.supplier_id, po.currency, po.project_id FROM payment_plans pp LEFT JOIN purchase_orders po ON pp.purchase_order_id = po.id - WHERE pp.id = ? + WHERE pp.id = $1 `, [id]); 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: '只能对待付款状态的计划创建付款申请' }); } - await db.query('BEGIN TRANSACTION'); + await db.query('BEGIN'); try { 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(` 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) RETURNING id`, [requestCode, plan.purchase_order_id, plan.planned_amount, plan.currency || 'CNY']); + const requestId = requestResult.rows[0].id; + await db.query(` UPDATE payment_plans - SET status = 'requested', payment_request_id = ?, updated_at = CURRENT_TIMESTAMP - WHERE id = ? - `, [requestResult.lastID, id]); + SET status = 'requested', payment_request_id = $1, updated_at = CURRENT_TIMESTAMP + WHERE id = $2 + `, [requestId, id]); await db.query('COMMIT'); res.json({ success: true, message: '付款申请创建成功', - data: { request_id: requestResult.lastID, request_code: requestCode } + data: { request_id: requestId, request_code: requestCode } }); } catch (innerError) { await db.query('ROLLBACK'); @@ -290,20 +292,20 @@ router.post('/:id/mark-paid', async (req, res) => { return res.status(400).json({ success: false, message: '只能对已申请或已批准的计划标记为已支付' }); } - await db.query('BEGIN TRANSACTION'); + await db.query('BEGIN'); try { await db.query(` UPDATE payment_plans - SET status = 'paid', actual_amount = ?, actual_date = ?, updated_at = CURRENT_TIMESTAMP - WHERE id = ? + SET status = 'paid', actual_amount = $1, actual_date = $2, updated_at = CURRENT_TIMESTAMP + WHERE id = $3 `, [actual_amount || plan.planned_amount, actual_date || new Date().toISOString().slice(0, 10), id]); if (plan.payment_request_id) { await db.query(` UPDATE payment_requests SET status = 'paid', updated_at = CURRENT_TIMESTAMP - WHERE id = ? + WHERE id = $1 `, [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, SUM(planned_amount) as total_amount FROM payment_plans - WHERE purchase_order_id = ? + WHERE purchase_order_id = $1 `, [plan.purchase_order_id]); const { paid_amount, total_amount } = orderResult.rows[0]; @@ -325,8 +327,8 @@ router.post('/:id/mark-paid', async (req, res) => { await db.query(` UPDATE purchase_orders - SET paid_amount = ?, status = ?, updated_at = CURRENT_TIMESTAMP - WHERE id = ? + SET paid_amount = $1, status = $2, updated_at = CURRENT_TIMESTAMP + WHERE id = $3 `, [paid_amount, newOrderStatus, plan.purchase_order_id]); await db.query('COMMIT'); @@ -360,13 +362,13 @@ router.get('/reminders/upcoming', async (req, res) => { const result = await db.query(` SELECT pp.*, - po.order_code, + po.code AS order_code, s.name as supplier_name FROM payment_plans pp LEFT JOIN purchase_orders po ON pp.purchase_order_id = po.id LEFT JOIN suppliers s ON po.supplier_id = s.id 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 ORDER BY pp.planned_date ASC `, [days]); diff --git a/backend/routes/reimbursements.js b/backend/routes/reimbursements.js index e3adadf..4eefa0c 100644 --- a/backend/routes/reimbursements.js +++ b/backend/routes/reimbursements.js @@ -22,7 +22,7 @@ router.get('/', async (req, res) => { const result = await db.query(` SELECT r.*, u.name as user_name, p.name as project_name 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 ORDER BY r.created_at DESC `); diff --git a/docker-compose.full.yml b/docker-compose.full.yml index 96f47fe..71f3b2c 100644 --- a/docker-compose.full.yml +++ b/docker-compose.full.yml @@ -8,12 +8,15 @@ services: - "10052:5432" volumes: - 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 environment: POSTGRES_USER: ${DB_USER:-postgres} POSTGRES_PASSWORD: ${DB_PASSWORD:-changeme} POSTGRES_DB: ${DB_NAME:-company_finance} POSTGRES_INITDB_ARGS: "--encoding=UTF8 --locale=zh_CN.UTF-8" + networks: + - yunhaifinance-network backend: build: diff --git a/frontend/src/components/layout/MainLayout.tsx b/frontend/src/components/layout/MainLayout.tsx index 7c61904..865d377 100644 --- a/frontend/src/components/layout/MainLayout.tsx +++ b/frontend/src/components/layout/MainLayout.tsx @@ -43,6 +43,7 @@ import { } from '@ant-design/icons' import { useAuthStore } from '../../store/authStore' import { useLanguageStore } from '../../store/languageStore' +import { PROJECT_LINKS } from '../../config/project' import CompanyLogo from '../common/CompanyLogo' import LanguageSelector from '../common/LanguageSelector' @@ -228,7 +229,7 @@ const MainLayout: React.FC = () => { { key: 'xinmi-code', icon: , - label: 新觅源码库 + label: 新觅源码库 } ], [t, currentLanguage]) @@ -470,7 +471,7 @@ const MainLayout: React.FC = () => { 系统信息与资源 - 新觅源码库 - 访问 新觅源码库 平台 + 新觅源码库 diff --git a/frontend/src/config/project.ts b/frontend/src/config/project.ts new file mode 100644 index 0000000..04a319c --- /dev/null +++ b/frontend/src/config/project.ts @@ -0,0 +1,3 @@ +export const PROJECT_LINKS = { + xinmi: 'https://www.xinmi.cloud/', +} as const diff --git a/frontend/src/locales/en-US.ts b/frontend/src/locales/en-US.ts index 2dcf685..70bad95 100644 --- a/frontend/src/locales/en-US.ts +++ b/frontend/src/locales/en-US.ts @@ -87,7 +87,7 @@ export default { }, login: { - title: 'Qingyuan Power Lao ERP', + title: '云海财务系统', subtitle: 'Integrated Project Management & Finance Reimbursement Platform', username: 'Username', password: 'Password', @@ -2208,11 +2208,11 @@ export default { description: 'System information and version', systemInfo: 'System Information', systemName: 'System Name', - systemNameValue: 'Qingyuan Power Lao ERP', + systemNameValue: '云海财务系统', version: 'System Version', versionValue: 'V1.0.0', devTeam: 'Development Team', - devTeamValue: 'Qingyuan Power IT Department', + devTeamValue: 'Xinmi', onlineDate: 'Launch Date', onlineDateValue: 'March 2026', techArchitecture: 'Technical Architecture', @@ -2237,7 +2237,7 @@ export default { connectionStatus: 'Connection Status', normal: 'Normal', lastBackup: 'Last Backup', - footer: '© 2026 Qingyuan Power Lao ERP System - Version V1.0.0', + footer: '© 2026 Yunhai Finance System V1.0.0 | Xinmi', }, backup: { diff --git a/frontend/src/locales/lo-LA.ts b/frontend/src/locales/lo-LA.ts index af9008d..cbaa81d 100644 --- a/frontend/src/locales/lo-LA.ts +++ b/frontend/src/locales/lo-LA.ts @@ -88,7 +88,7 @@ export default { }, login: { - title: 'ຊິງຢວນ ໄຟຟ້າລາວ ERP', + title: '云海财务系统', subtitle: 'ແພລດຟອມຄຸ້ມຄອງໂຄງການ ແລະ ການເງິນ', username: 'ຊື່ຜູ້ໃຊ້', password: 'ລະຫັດຜ່ານ', @@ -2972,10 +2972,10 @@ export default { technicalSupport: 'ສະ', feedback: 'ຕຳ', leaveMessage: 'ຂໍ້', - systemNameValue: 'Qingyuan Power Lao ERP', + systemNameValue: '云海财务系统', versionValue: 'V1.0.0', devTeam: 'Development Team', - devTeamValue: 'Qingyuan Power IT Department', + devTeamValue: '新觅', onlineDate: 'Launch Date', onlineDateValue: 'March 2026', techArchitecture: 'Technical Architecture', @@ -2998,7 +2998,7 @@ export default { connectionStatus: 'Connection Status', normal: 'Normal', lastBackup: 'Last Backup', - footer: '© 2026 Qingyuan Power Lao ERP System - Version V1.0.0', + footer: '© 2026 云海财务系统 V1.0.0 | 新觅', }, backup: { diff --git a/frontend/src/locales/th-TH.ts b/frontend/src/locales/th-TH.ts index 5891f91..06d4f88 100644 --- a/frontend/src/locales/th-TH.ts +++ b/frontend/src/locales/th-TH.ts @@ -87,7 +87,7 @@ export default { }, login: { - title: 'ชิงหยวนพาวเวอร์ สปป.ลาว ERP', + title: '云海财务系统', subtitle: 'แพลตฟอร์มการจัดการโครงการและการเบิกค่าใช้จ่ายทางการเงิน', username: 'ชื่อผู้ใช้', password: 'รหัสผ่าน', @@ -2208,11 +2208,11 @@ export default { description: 'ข้อมูลระบบและรุ่น', systemInfo: 'ข้อมูลระบบ', systemName: 'ชื่อระบบ', - systemNameValue: 'ชิงหยวนพาวเวอร์ สปป.ลาว ERP', + systemNameValue: '云海财务系统', version: 'รุ่นระบบ', versionValue: 'V1.0.0', devTeam: 'ทีมพัฒนา', - devTeamValue: 'ฝ่ายเทคโนโลยีสารสนเทศ ชิงหยวนพาวเวอร์', + devTeamValue: '新觅', onlineDate: 'วันที่เปิดใช้งาน', onlineDateValue: 'มีนาคม 2026', techArchitecture: 'สถาปัตยกรรมทางเทคนิค', @@ -2237,7 +2237,7 @@ export default { connectionStatus: 'สถานะการเชื่อมต่อ', normal: 'ปกติ', lastBackup: 'สำรองข้อมูลล่าสุด', - footer: '© 2026 ระบบ ERP ชิงหยวนพาวเวอร์ สปป.ลาว - รุ่น V1.0.0', + footer: '© 2026 云海财务系统 V1.0.0 | 新觅', }, backup: { diff --git a/frontend/src/locales/zh-CN.ts b/frontend/src/locales/zh-CN.ts index 24fa5f8..3bfcb38 100644 --- a/frontend/src/locales/zh-CN.ts +++ b/frontend/src/locales/zh-CN.ts @@ -2212,7 +2212,7 @@ export default { version: '系统版本', versionValue: 'V1.0.0', devTeam: '开发团队', - devTeamValue: '云海技术部', + devTeamValue: '新觅', onlineDate: '上线日期', onlineDateValue: '2026年3月', techArchitecture: '技术架构', @@ -2237,7 +2237,7 @@ export default { connectionStatus: '连接状态', normal: '正常', lastBackup: '最近备份', - footer: '© 2026 云海财务系统系统 - 版本 V1.0.0', + footer: '© 2026 云海财务系统 - 版本 V1.0.0 | 新觅', }, backup: { diff --git a/frontend/src/pages/admin/AboutPage.tsx b/frontend/src/pages/admin/AboutPage.tsx index bebaf74..12862f0 100644 --- a/frontend/src/pages/admin/AboutPage.tsx +++ b/frontend/src/pages/admin/AboutPage.tsx @@ -9,6 +9,7 @@ import { RocketOutlined } from '@ant-design/icons'; import { useLanguageStore } from '../../store/languageStore'; +import { PROJECT_LINKS } from '../../config/project'; const { Title, Paragraph, Text } = Typography; @@ -37,7 +38,7 @@ const AboutPage: React.FC = () => { size="large" shape="round" icon={} - href="http://code.xinmi.cloud/" + href={PROJECT_LINKS.xinmi} target="_blank" style={{ backgroundColor: 'white', @@ -49,7 +50,7 @@ const AboutPage: React.FC = () => { boxShadow: '0 8px 16px rgba(0,0,0,0.1)' }} > - 探索 新觅源码库 + 新觅源码库 @@ -148,7 +149,7 @@ const AboutPage: React.FC = () => { - company_finance_db + company_finance {t('about.normal')} 2026-03-19 00:00 diff --git a/images/image-20260624104054977.png b/images/image-20260624104054977.png new file mode 100644 index 0000000..793e03e Binary files /dev/null and b/images/image-20260624104054977.png differ diff --git a/images/image-20260624104303041.png b/images/image-20260624104303041.png new file mode 100644 index 0000000..8377b23 Binary files /dev/null and b/images/image-20260624104303041.png differ diff --git a/images/image-20260624104306842.png b/images/image-20260624104306842.png new file mode 100644 index 0000000..8377b23 Binary files /dev/null and b/images/image-20260624104306842.png differ diff --git a/images/image-20260624104319931.png b/images/image-20260624104319931.png new file mode 100644 index 0000000..793e03e Binary files /dev/null and b/images/image-20260624104319931.png differ diff --git a/images/image-20260624104333660.png b/images/image-20260624104333660.png new file mode 100644 index 0000000..6807a81 Binary files /dev/null and b/images/image-20260624104333660.png differ diff --git a/images/image-20260624104344479.png b/images/image-20260624104344479.png new file mode 100644 index 0000000..903c679 Binary files /dev/null and b/images/image-20260624104344479.png differ diff --git a/images/image-20260624104509907.png b/images/image-20260624104509907.png new file mode 100644 index 0000000..55445ae Binary files /dev/null and b/images/image-20260624104509907.png differ diff --git a/images/image-20260624104525295.png b/images/image-20260624104525295.png new file mode 100644 index 0000000..859e2e1 Binary files /dev/null and b/images/image-20260624104525295.png differ diff --git a/images/image-20260624104539379.png b/images/image-20260624104539379.png new file mode 100644 index 0000000..c89a20d Binary files /dev/null and b/images/image-20260624104539379.png differ diff --git a/images/image-20260624104548854.png b/images/image-20260624104548854.png new file mode 100644 index 0000000..4c442b6 Binary files /dev/null and b/images/image-20260624104548854.png differ diff --git a/images/image-20260624104557054.png b/images/image-20260624104557054.png new file mode 100644 index 0000000..0f3e2a5 Binary files /dev/null and b/images/image-20260624104557054.png differ diff --git a/images/image-20260624104623194.png b/images/image-20260624104623194.png new file mode 100644 index 0000000..bce5219 Binary files /dev/null and b/images/image-20260624104623194.png differ diff --git a/images/image-20260624104635658.png b/images/image-20260624104635658.png new file mode 100644 index 0000000..91c5739 Binary files /dev/null and b/images/image-20260624104635658.png differ diff --git a/images/image-20260624104653703.png b/images/image-20260624104653703.png new file mode 100644 index 0000000..def3b2f Binary files /dev/null and b/images/image-20260624104653703.png differ diff --git a/images/image-20260624104704292.png b/images/image-20260624104704292.png new file mode 100644 index 0000000..e45abaf Binary files /dev/null and b/images/image-20260624104704292.png differ diff --git a/scripts/init_sample_data.sql b/scripts/init_sample_data.sql index 5c5edff..6351a30 100644 --- a/scripts/init_sample_data.sql +++ b/scripts/init_sample_data.sql @@ -1,58 +1,200 @@ 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 ( - 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 -('admin', '系统管理员', 'admin@yunhai.com', '13800000001', 'admin', '$2a$10$xyz', NOW(), NOW()), -('finance', '财务专员', 'finance@yunhai.com', '13800000002', 'finance', '$2a$10$xyz', NOW(), NOW()), -('manager', '项目经理', 'manager@yunhai.com', '13800000003', 'manager', '$2a$10$xyz', NOW(), NOW()), -('employee', '普通员工', 'employee@yunhai.com', '13800000004', 'employee', '$2a$10$xyz', NOW(), NOW()) +-- 1. 用户 +INSERT INTO users (username, name, email, phone, role, password_hash, created_at, updated_at) VALUES +('admin', '系统管理员', 'admin@xinmi.cloud', '13800000001', 'admin', '$2b$10$XYs6Rc6LVqeFsmY4cMZdmOy.bhSc4Ib3yFfwj5kt.G.oHNiuZSkZm', NOW(), NOW()), +('finance', '财务专员', 'finance@xinmi.cloud', '13800000002', 'finance', '$2b$10$XYs6Rc6LVqeFsmY4cMZdmOy.bhSc4Ib3yFfwj5kt.G.oHNiuZSkZm', NOW(), NOW()), +('manager', '项目经理', 'manager@xinmi.cloud', '13800000003', 'manager', '$2b$10$XYs6Rc6LVqeFsmY4cMZdmOy.bhSc4Ib3yFfwj5kt.G.oHNiuZSkZm', NOW(), NOW()), +('employee', '普通员工', 'employee@xinmi.cloud', '13800000004', 'employee', '$2b$10$XYs6Rc6LVqeFsmY4cMZdmOy.bhSc4Ib3yFfwj5kt.G.oHNiuZSkZm', NOW(), NOW()) ON CONFLICT (username) DO NOTHING; --- 2. 插入客户数据 -INSERT INTO customers (name, address, remark, contact_person, phone, email, created_at, updated_at) VALUES +-- 2. 客户 +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()), ('万象基建集团', '万象市', '长期合作', '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与上表对应) -INSERT INTO projects (name, project_code, customer_id, project_manager_id, contract_amount, status, location, created_at, updated_at) VALUES -('万象22kV线路工程', 'P2026-001', 1, 3, 1500000, 'in_progress', '万象市赛塔尼县', NOW(), NOW()), -('琅勃拉邦变电站改造', 'P2026-002', 3, 3, 800000, 'planning', '琅勃拉邦', NOW(), NOW()), -('南塔电力抢修', 'P2026-003', 1, 3, 300000, 'completed', '南塔省', NOW(), NOW()); +-- 3. 项目 +INSERT INTO projects (name, project_code, customer_id, project_manager_id, contract_amount, status, location, start_date, description, created_at, updated_at) +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() +FROM (VALUES +('万象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()); diff --git a/scripts/init_schema.sql b/scripts/init_schema.sql new file mode 100644 index 0000000..454b3ee --- /dev/null +++ b/scripts/init_schema.sql @@ -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;