first commit
This commit is contained in:
145
CLAUDE.md
Executable file
145
CLAUDE.md
Executable file
@@ -0,0 +1,145 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Project Overview
|
||||
|
||||
企业供应链合同管理系统 (中尚鹏 / ZSP). Monorepo with a Vue 3 SPA frontend and Go REST API backend, managing contracts, deliveries, inventory, finance, purchasing, and reporting for a supply chain business.
|
||||
|
||||
### 目录结构
|
||||
|
||||
```
|
||||
├── frontend/ # Vue 3 SPA 前端
|
||||
├── backend/ # Go + Gin REST API 后端
|
||||
├── docs/ # 项目文档
|
||||
│ ├── api/ # API 文档
|
||||
│ ├── deploy/ # 部署文档
|
||||
│ ├── dev/ # 开发文档与需求分析
|
||||
│ └── Git提交规范.md # Git 提交规范
|
||||
├── scripts/ # 部署脚本
|
||||
└── CLAUDE.md # Claude Code 配置文件
|
||||
```
|
||||
|
||||
### 5 大业务模块(21 个子菜单)
|
||||
|
||||
1. **合同管理**(3 个子菜单):业务合同管理、中尚鹏合同管理、公司信息管理
|
||||
2. **提货管理**(5 个子菜单):我要提货、提货明细、库存信息、货权转移、其他出库
|
||||
3. **财务管理**(7 个子菜单):货代费用、杂费管理、服务费管理、发票管理、结算管理、对账单、利润明细
|
||||
4. **采购管理**(4 个子菜单):采购报单、采购运踪明细、国外供应商、采购预算
|
||||
5. **查询&报表中心**(5 个子菜单):合同执行情况、营业明细表、其他明细表、商品基础资料、其他报表
|
||||
|
||||
## Commands
|
||||
|
||||
### Frontend (frontend/)
|
||||
|
||||
```bash
|
||||
pnpm dev # Start Vite dev server (port 5173, proxies /api to 127.0.0.1:8080)
|
||||
pnpm build # Production build to frontend/dist/
|
||||
pnpm typecheck # tsc --noEmit + vue-tsc --noEmit
|
||||
pnpm lint:eslint # ESLint check
|
||||
pnpm lint:prettier # Prettier check
|
||||
pnpm lint # Run all linters
|
||||
```
|
||||
|
||||
### Backend (backend/)
|
||||
|
||||
```bash
|
||||
go run cmd/api/main.go # Start server (port 8080)
|
||||
air -c .air.toml # Start with hot-reload
|
||||
go test ./... # Run all tests
|
||||
go test ./test/ # Run all handler tests (user + contract + delivery)
|
||||
go build -o ./tmp/main ./cmd/api/main.go # Build binary
|
||||
```
|
||||
|
||||
### Docker
|
||||
|
||||
```bash
|
||||
docker-compose -f docker-compose.dev.yml up --build # Dev: backend + MySQL + Air
|
||||
docker-compose --env-file .env.prod up -d # Production
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
### Frontend (Vue 3 + TypeScript + Vite + Pinia + Element Plus)
|
||||
|
||||
Based on [vue-pure-admin](https://github.com/pure-admin/vue-pure-admin) Lite Edition (non-i18n). Key directories:
|
||||
|
||||
- `src/api/` — Axios-based API client modules, one file per domain (contract.ts, delivery.ts, finance.ts, etc.)
|
||||
- `src/assets/` — Static assets (icons, images)
|
||||
- `src/components/` — Reusable components (ReAuth, ReDialog, ReIcon, etc.)
|
||||
- `src/config/` — App configuration
|
||||
- `src/directives/` — Custom Vue directives (auth, copy, ripple, etc.)
|
||||
- `src/layout/` — App shell with 3 layout modes (vertical/horizontal/mix)
|
||||
- `src/router/modules/` — Route modules, one per feature domain, loaded dynamically via async routes
|
||||
- `src/store/modules/` — Pinia stores (user, app, permission, settings, epTheme, multiTags)
|
||||
- `src/style/` — Global styles (reset, index, sidebar, tailwind)
|
||||
- `src/utils/` — Utility functions (HTTP client, auth, tree operations, etc.)
|
||||
- `src/views/` — Page components by feature: login/, system/contract/, delivery/, finance/, purchase/, query-report/
|
||||
|
||||
### Backend (Go + Gin + GORM + MySQL 8.0)
|
||||
|
||||
Clean layered architecture:
|
||||
|
||||
- `internal/handler/` — HTTP request handling, parameter parsing, response writing
|
||||
- `internal/service/` — Business logic orchestration
|
||||
- `internal/repository/` — Data access via GORM queries
|
||||
- `internal/model/` — GORM model structs with table tags (27 tables via AutoMigrate)
|
||||
- `internal/dto/` — Request/response DTOs
|
||||
- `internal/middleware/` — JWT auth middleware (enabled for all routes except /auth/* and static files)
|
||||
- `internal/server/route.go` — All API route definitions (single source of truth)
|
||||
|
||||
API routes are under `/api/` prefix with per-domain route groups. JWT middleware is applied to all routes except `/auth/register`, `/auth/login`, and static file routes.
|
||||
|
||||
### API Route Groups
|
||||
|
||||
| Group | Prefix | Handler | Description |
|
||||
|-------|--------|---------|-------------|
|
||||
| Auth | `/api/auth/register`, `/api/auth/login` | UserHandler | Public: user registration/login |
|
||||
| Users | `/api/users/:id` | UserHandler | Protected: user info |
|
||||
| Contract | `/api/contract` | ContractHandler | Business contracts CRUD + folders + batches |
|
||||
| ZSP Contract | `/api/zsp-contract` | ZSPContractHandler | CRUD + details + Excel import/export |
|
||||
| Company | `/api/company` | CompanyHandler | Company info management |
|
||||
| Delivery | `/api/apply-delivery` | DeliveryHandler | Delivery applications |
|
||||
| Outbound | `/api/delivery-details` | DeliveryHandler | Outbound orders |
|
||||
| Inventory | `/api/inventory` | DeliveryHandler | Warehouses/inbound/summary |
|
||||
| Ownership | `/api/ownership-transfer` | DeliveryHandler | Ownership transfer CRUD |
|
||||
| Finance | `/api/zsp-finances` | ZSPFinancesHandler | Freight/misc/service fees, costs, profits |
|
||||
|
||||
### Key patterns
|
||||
|
||||
- **Backend handler → service → repository** — Each layer calls the next. Handlers are thin; business logic lives in services. Tests use mocked service interfaces.
|
||||
- **DTOs for API contracts** — Request/response types in `internal/dto/`, not raw models.
|
||||
- **GORM AutoMigrate** — 27 models auto-migrated on startup; schema changes are additive.
|
||||
- **Frontend API modules** — Each `src/api/*.ts` file exports typed functions that return Axios promises with the base response wrapper (`Result<T>`).
|
||||
- **HTTP interceptors** — Auto-attach JWT Authorization header, handle token refresh, NProgress loading bar.
|
||||
- **Async routes** — Routes are fetched dynamically post-login; each router module exports an array of route configs.
|
||||
- **3 layout modes** — Adaptive vertical/horizontal/mix layouts with multi-tab + keep-alive caching.
|
||||
- **No i18n** — All UI text is hardcoded in Chinese.
|
||||
|
||||
## Testing
|
||||
|
||||
- Backend tests live in `backend/test/` and use mocked service interfaces with `httptest.NewRecorder()`.
|
||||
- Currently 24 tests covering user, contract, and delivery handlers.
|
||||
- Frontend testing infrastructure is not yet configured.
|
||||
- Run tests: `cd backend && go test ./test/ -v`
|
||||
|
||||
## Known Issues / Technical Debt
|
||||
|
||||
1. **Dual contract models** — Legacy `Contract` model coexists with new `ZSPContractFile`/`ZSPContractDetail` models.
|
||||
2. **API URL inconsistency** — Some routes use `/list`, `/create`, `/update/:id` suffixes while others use direct REST patterns.
|
||||
3. **Test coverage** — User, contract, and delivery handler tests exist (24 tests); finance and ZSP contract handlers still lack tests.
|
||||
4. **Delivery handler** — Refactored to use service/repository layer (was directly using `*gorm.DB`). The `InventorySummary` method still accesses DB directly through `svc.DB()` for complex aggregate queries.
|
||||
|
||||
## Git Conventions
|
||||
|
||||
- **Commit format**: Conventional Commits with Chinese descriptions. Types: `feat`, `fix`, `docs`, `style`, `refactor`, `perf`, `test`, `chore`. Scopes: `contract`, `finance`, `delivery`, `inventory`, `invoice`, `api`, `frontend`, `backend`.
|
||||
- **Branch strategy**: `main` → production, `dev` → development, `feature/xxx` → features, `fix/xxx` → fixes.
|
||||
- **Husky hooks**: commit-msg (commitlint) + pre-commit (lint-staged).
|
||||
|
||||
## Docs
|
||||
|
||||
Project documentation is in `docs/`:
|
||||
- `docs/api/` — API documentation and frontend route docs
|
||||
- `docs/deploy/` — Nginx configuration, Docker deployment, environment requirements
|
||||
- `docs/dev/` — Requirements analysis, known issues, finance module pending questions
|
||||
- `docs/Git提交规范.md` — Git commit standards (Conventional Commits)
|
||||
36
backend/.air.toml
Executable file
36
backend/.air.toml
Executable file
@@ -0,0 +1,36 @@
|
||||
# .air.toml
|
||||
root = "."
|
||||
tmp_dir = "tmp"
|
||||
|
||||
[build]
|
||||
cmd = "go build -o ./tmp/main ./cmd/api/main.go"
|
||||
bin = "tmp/main"
|
||||
full_bin = "./tmp/main"
|
||||
delay = 1000
|
||||
exclude_dir = ["assets", "tmp", "vendor", "testdata"]
|
||||
exclude_file = []
|
||||
exclude_regex = ["_test.go"]
|
||||
exclude_unchanged = false
|
||||
follow_symlink = false
|
||||
log = "build-errors.log"
|
||||
send_interrupt = true
|
||||
stop_on_error = true
|
||||
|
||||
[color]
|
||||
main = "magenta"
|
||||
watcher = "cyan"
|
||||
build = "yellow"
|
||||
runner = "green"
|
||||
|
||||
[log]
|
||||
time = false
|
||||
|
||||
[misc]
|
||||
clean_on_exit = true
|
||||
[watcher]
|
||||
# 忽略的文件变化
|
||||
ignore_list = ["vendor/*", "*.log", ".git/*", ".tmp/*", "node_modules/*"]
|
||||
|
||||
# 设置轮询间隔(适用于 Docker for Mac/Windows)
|
||||
poll = true
|
||||
poll_interval = 500 # 毫秒
|
||||
104
backend/.dockerignore
Executable file
104
backend/.dockerignore
Executable file
@@ -0,0 +1,104 @@
|
||||
# Include any files or directories that you don't want to be copied to your
|
||||
# container here (e.g., local build artifacts, temporary files, etc.).
|
||||
#
|
||||
# For more help, visit the .dockerignore file reference guide at
|
||||
# https://docs.docker.com/engine/reference/builder/#dockerignore-file
|
||||
|
||||
**/.DS_Store
|
||||
**/__pycache__
|
||||
**/.venv
|
||||
**/.classpath
|
||||
**/.dockerignore
|
||||
**/.env
|
||||
**/.git
|
||||
**/.gitignore
|
||||
**/.project
|
||||
**/.settings
|
||||
**/.toolstarget
|
||||
**/.vs
|
||||
**/.vscode
|
||||
**/*.*proj.user
|
||||
**/*.dbmdl
|
||||
**/*.jfm
|
||||
**/bin
|
||||
**/charts
|
||||
**/docker-compose*
|
||||
**/compose*
|
||||
**/Dockerfile*
|
||||
**/node_modules
|
||||
**/npm-debug.log
|
||||
**/obj
|
||||
**/secrets.dev.yaml
|
||||
**/values.dev.yaml
|
||||
LICENSE
|
||||
README.md
|
||||
# Include any files or directories that you don't want to be copied to your
|
||||
# container here (e.g., local build artifacts, temporary files, etc.).
|
||||
#
|
||||
# For more help, visit the .dockerignore file reference guide at
|
||||
# https://docs.docker.com/engine/reference/builder/#dockerignore-file
|
||||
|
||||
**/.DS_Store
|
||||
**/__pycache__
|
||||
**/.venv
|
||||
**/.classpath
|
||||
**/.dockerignore
|
||||
**/.env
|
||||
**/.git
|
||||
**/.gitignore
|
||||
**/.project
|
||||
**/.settings
|
||||
**/.toolstarget
|
||||
**/.vs
|
||||
**/.vscode
|
||||
**/*.*proj.user
|
||||
**/*.dbmdl
|
||||
**/*.jfm
|
||||
**/bin
|
||||
**/charts
|
||||
**/docker-compose*
|
||||
**/compose*
|
||||
**/Dockerfile*
|
||||
**/node_modules
|
||||
**/npm-debug.log
|
||||
**/obj
|
||||
**/secrets.dev.yaml
|
||||
**/values.dev.yaml
|
||||
LICENSE
|
||||
README.md
|
||||
# Include any files or directories that you don't want to be copied to your
|
||||
# container here (e.g., local build artifacts, temporary files, etc.).
|
||||
#
|
||||
# For more help, visit the .dockerignore file reference guide at
|
||||
# https://docs.docker.com/engine/reference/builder/#dockerignore-file
|
||||
|
||||
**/.DS_Store
|
||||
**/__pycache__
|
||||
**/.venv
|
||||
**/.classpath
|
||||
**/.dockerignore
|
||||
**/.env
|
||||
**/.git
|
||||
**/.gitignore
|
||||
**/.project
|
||||
**/.settings
|
||||
**/.toolstarget
|
||||
**/.vs
|
||||
**/.vscode
|
||||
**/*.*proj.user
|
||||
**/*.dbmdl
|
||||
**/*.jfm
|
||||
**/bin
|
||||
**/charts
|
||||
**/docker-compose*
|
||||
**/compose*
|
||||
**/Dockerfile*
|
||||
**/node_modules
|
||||
**/npm-debug.log
|
||||
**/obj
|
||||
**/secrets.dev.yaml
|
||||
**/values.dev.yaml
|
||||
LICENSE
|
||||
README.md
|
||||
mysql_data/
|
||||
.env.dev
|
||||
10
backend/.env.dev
Executable file
10
backend/.env.dev
Executable file
@@ -0,0 +1,10 @@
|
||||
# 开发环境配置
|
||||
ENV=development
|
||||
APP_NAME=myapp-dev
|
||||
DB_HOST=mysql
|
||||
DB_PORT=3306
|
||||
DB_USER=root
|
||||
DB_PASSWORD=password
|
||||
DB_NAME=myapp
|
||||
# REDIS_HOST=redis
|
||||
# REDIS_PORT=6379
|
||||
13
backend/.env.prod
Executable file
13
backend/.env.prod
Executable file
@@ -0,0 +1,13 @@
|
||||
# 生产环境配置(部署前请修改敏感信息)
|
||||
ENV=production
|
||||
APP_NAME=myapp
|
||||
DB_HOST=mysql
|
||||
DB_PORT=3306
|
||||
DB_USER=root
|
||||
DB_PASSWORD=password
|
||||
DB_NAME=myapp
|
||||
|
||||
# Docker Compose 变量(用于 MySQL 等服务的密码替换)
|
||||
MYSQL_ROOT_PASSWORD=password
|
||||
MYSQL_DATABASE=myapp
|
||||
MYSQL_PASSWORD=password
|
||||
5
backend/.gitignore
vendored
Executable file
5
backend/.gitignore
vendored
Executable file
@@ -0,0 +1,5 @@
|
||||
*.exe
|
||||
/bin/
|
||||
/.github/
|
||||
/.continue/
|
||||
/data/
|
||||
27
backend/Dockerfile
Executable file
27
backend/Dockerfile
Executable file
@@ -0,0 +1,27 @@
|
||||
# 构建阶段
|
||||
FROM golang:1.25.4-alpine3.22 AS builder
|
||||
WORKDIR /app
|
||||
|
||||
COPY go.mod go.sum ./
|
||||
RUN go env -w GO111MODULE=on
|
||||
RUN go env -w GOPROXY=https://mirrors.aliyun.com/goproxy/,direct
|
||||
RUN go mod download
|
||||
|
||||
COPY . .
|
||||
RUN CGO_ENABLED=0 GOOS=linux go build -o main ./cmd/api
|
||||
|
||||
# 运行阶段
|
||||
FROM alpine:latest
|
||||
WORKDIR /app
|
||||
|
||||
RUN apk add --no-cache ca-certificates tzdata
|
||||
ENV TZ=Asia/Shanghai
|
||||
|
||||
COPY --from=builder /app/main .
|
||||
COPY --from=builder /app/configs ./configs
|
||||
|
||||
# 数据目录(上传文件等),运行时挂载 volume
|
||||
RUN mkdir -p data/contract/uploads data/contract/batch_archive data/ownership data/zsp-contract/files
|
||||
|
||||
EXPOSE 8080
|
||||
ENTRYPOINT ["/app/main"]
|
||||
27
backend/Dockerfile.dev
Executable file
27
backend/Dockerfile.dev
Executable file
@@ -0,0 +1,27 @@
|
||||
# Dockerfile.dev
|
||||
FROM golang:1.25.4-alpine3.22
|
||||
|
||||
# 安装必要的工具
|
||||
RUN apk add --no-cache git curl bash
|
||||
|
||||
# 安装 air(Go 1.21+ 版本)
|
||||
RUN go install github.com/air-verse/air@latest
|
||||
|
||||
# 设置工作目录 - 注意这里用 /workspace,不是 /app
|
||||
WORKDIR /workspace
|
||||
|
||||
# 复制依赖文件
|
||||
COPY go.mod go.sum ./
|
||||
|
||||
# 下载依赖到 GOPATH
|
||||
RUN go mod download
|
||||
|
||||
# 暴露端口
|
||||
EXPOSE 8080
|
||||
|
||||
# 设置环境变量让 go 命令能找到依赖
|
||||
ENV GOPATH=/go
|
||||
ENV PATH=$PATH:/go/bin
|
||||
|
||||
# 使用 air 启动应用
|
||||
CMD ["air", "-c", ".air.toml"]
|
||||
325
backend/README_CN.md
Executable file
325
backend/README_CN.md
Executable file
@@ -0,0 +1,325 @@
|
||||
# ZSP 后端管理系统
|
||||
|
||||
基于 Go + Gin + GORM + MySQL 构建的现代化合同管理与用户认证系统后端API。
|
||||
|
||||
## 📋 项目简介
|
||||
|
||||
ZSP 后端是一个用于供应链合同管理的企业级应用系统,提供用户认证、合同管理、文件存储等核心功能。系统采用微服务架构思想,具备高可扩展性和良好的开发体验。
|
||||
|
||||
### 主要特性
|
||||
- 🔐 **JWT 用户认证与授权**
|
||||
- 📄 **合同全生命周期管理**
|
||||
- 🐳 **Docker 容器化开发环境**
|
||||
- 🔄 **热重载开发体验** (Air)
|
||||
- 📊 **MySQL 数据库支持**
|
||||
- 🧪 **自动化数据库迁移**
|
||||
- 🔧 **配置驱动架构**
|
||||
|
||||
## 🏗️ 技术栈
|
||||
|
||||
### 后端框架
|
||||
- **Go 1.25.4** - 高性能编程语言
|
||||
- **Gin** - 轻量级 Web 框架
|
||||
- **GORM** - ORM 数据库工具
|
||||
- **JWT** - 用户认证令牌
|
||||
|
||||
### 开发工具
|
||||
- **Docker & Docker Compose** - 容器化开发环境
|
||||
- **Air** - Go 应用热重载工具
|
||||
- **MySQL 8.0** - 关系型数据库
|
||||
|
||||
### 配置管理
|
||||
- **Viper** - 配置管理库
|
||||
- **YAML 配置文件** - 结构化配置
|
||||
|
||||
## 📁 项目结构
|
||||
|
||||
```
|
||||
zsp-backend/
|
||||
├── cmd/api/ # 应用入口
|
||||
│ └── main.go # 主程序入口
|
||||
├── configs/ # 配置文件
|
||||
│ └── config.dev.yaml # 开发环境配置
|
||||
├── internal/ # 内部包
|
||||
│ ├── handler/ # HTTP 处理器
|
||||
│ │ ├── user_handler.go # 用户相关接口
|
||||
│ │ └── contract_handler.go # 合同相关接口
|
||||
│ ├── middleware/ # 中间件
|
||||
│ │ └── auth.go # 认证中间件
|
||||
│ ├── model/ # 数据模型
|
||||
│ │ ├── user.go # 用户模型
|
||||
│ │ ├── contract.go # 合同模型
|
||||
│ │ └── error.go # 错误响应模型
|
||||
│ ├── repository/ # 数据访问层
|
||||
│ │ ├── user_repository.go
|
||||
│ │ └── contract_repository.go
|
||||
│ ├── service/ # 业务逻辑层
|
||||
│ │ ├── user_service.go
|
||||
│ │ └── contract_service.go
|
||||
│ └── server/ # 服务器配置
|
||||
│ └── router.go # 路由配置
|
||||
├── pkg/ # 公共包
|
||||
│ ├── config/ # 配置加载
|
||||
│ └── database/ # 数据库连接
|
||||
├── scripts/ # 脚本文件
|
||||
│ └── init_database.sql # 数据库初始化脚本
|
||||
├── migrations/ # 数据库迁移文件
|
||||
├── test/ # 测试文件
|
||||
├── web/ # 前端文件(可选)
|
||||
├── deployments/ # 部署配置
|
||||
├── docs/ # 文档
|
||||
├── tmp/ # 临时文件
|
||||
└── bin/ # 构建输出
|
||||
```
|
||||
|
||||
## 🚀 快速开始
|
||||
|
||||
### 前提条件
|
||||
- Docker & Docker Compose
|
||||
- Go 1.25.4+ (可选,用于本地开发)
|
||||
|
||||
### 使用 Docker 开发环境(推荐)
|
||||
|
||||
1. **克隆项目**
|
||||
```bash
|
||||
git clone <仓库地址>
|
||||
cd zsp-backend
|
||||
```
|
||||
|
||||
2. **启动开发环境**
|
||||
```bash
|
||||
docker-compose -f docker-compose.dev.yml up --build
|
||||
```
|
||||
|
||||
3. **访问应用**
|
||||
- API 服务: http://localhost:8080
|
||||
- MySQL 数据库: localhost:3306
|
||||
|
||||
### 本地开发(不使用 Docker)
|
||||
|
||||
1. **安装依赖**
|
||||
```bash
|
||||
go mod download
|
||||
```
|
||||
|
||||
2. **启动 MySQL 数据库**
|
||||
```bash
|
||||
# 使用 Docker 启动 MySQL
|
||||
docker run --name mysql-dev -e MYSQL_ROOT_PASSWORD=password \
|
||||
-e MYSQL_DATABASE=myapp -p 3306:3306 -d mysql:8.0
|
||||
```
|
||||
|
||||
3. **运行应用**
|
||||
```bash
|
||||
# 使用 Air 热重载
|
||||
air -c .air.toml
|
||||
|
||||
# 或直接运行
|
||||
go run cmd/api/main.go
|
||||
```
|
||||
|
||||
## 🔧 配置说明
|
||||
|
||||
### 配置文件 (`configs/config.dev.yaml`)
|
||||
```yaml
|
||||
server:
|
||||
port: 8080
|
||||
mode: "debug"
|
||||
read_timeout: 30
|
||||
write_timeout: 30
|
||||
|
||||
database:
|
||||
host: "mysql"
|
||||
port: 3306
|
||||
user: "root"
|
||||
password: "password"
|
||||
dbname: "myapp"
|
||||
max_open_conns: 100
|
||||
max_idle_conns: 10
|
||||
reset_database: true
|
||||
|
||||
jwt:
|
||||
secret: "your-jwt-secret-key"
|
||||
expire_hours: 24
|
||||
```
|
||||
|
||||
## 📖 API 接口文档
|
||||
|
||||
### 用户认证
|
||||
|
||||
#### 用户注册
|
||||
```http
|
||||
POST /api/v1/users/register
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"username": "testuser",
|
||||
"email": "test@example.com",
|
||||
"password": "password123"
|
||||
}
|
||||
```
|
||||
|
||||
#### 用户登录
|
||||
```http
|
||||
POST /api/v1/users/login
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"email": "test@example.com",
|
||||
"password": "password123"
|
||||
}
|
||||
```
|
||||
|
||||
响应:
|
||||
```json
|
||||
{
|
||||
"message": "Login successful",
|
||||
"user": {
|
||||
"id": 1,
|
||||
"username": "testuser",
|
||||
"email": "test@example.com"
|
||||
},
|
||||
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
|
||||
}
|
||||
```
|
||||
|
||||
### 合同管理
|
||||
|
||||
#### 创建合同
|
||||
```http
|
||||
POST /api/v1/contracts
|
||||
Authorization: Bearer <jwt-token>
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"name": "采购合同",
|
||||
"contract_type": "采购",
|
||||
"contract_code": "CG2024001",
|
||||
"buy_party": "买方公司",
|
||||
"sell_party": "卖方公司",
|
||||
"count_party": "计数方",
|
||||
"unit_price": "1000.00",
|
||||
"bl_number": "BL123456",
|
||||
"delivery_time": "2024-12-31",
|
||||
"pickup_time": "2024-12-30",
|
||||
"packaging": "标准包装",
|
||||
"remarks": "备注信息",
|
||||
"file_path": "/uploads/contract.pdf"
|
||||
}
|
||||
```
|
||||
|
||||
#### 获取合同列表
|
||||
```http
|
||||
GET /api/v1/contracts
|
||||
Authorization: Bearer <jwt-token>
|
||||
```
|
||||
|
||||
## 🗄️ 数据模型
|
||||
|
||||
### 用户表 (users)
|
||||
| 字段 | 类型 | 描述 |
|
||||
|------|------|------|
|
||||
| id | uint | 主键 |
|
||||
| username | string(50) | 用户名,唯一 |
|
||||
| email | string(100) | 邮箱,唯一 |
|
||||
| password | string(255) | 密码哈希 |
|
||||
| created_at | datetime | 创建时间 |
|
||||
| updated_at | datetime | 更新时间 |
|
||||
|
||||
### 合同表 (contracts)
|
||||
| 字段 | 类型 | 描述 |
|
||||
|------|------|------|
|
||||
| id | uint | 主键 |
|
||||
| name | string(100) | 合同名称 |
|
||||
| contract_type | string(50) | 合同类型 |
|
||||
| contract_code | string(50) | 合同编码 |
|
||||
| buy_party | string(100) | 买方 |
|
||||
| sell_party | string(100) | 卖方 |
|
||||
| count_party | string(100) | 计数方 |
|
||||
| unit_price | string(50) | 单价 |
|
||||
| bl_number | string(50) | 提单号 |
|
||||
| delivery_time | string(50) | 交付时间 |
|
||||
| pickup_time | string(50) | 提货时间 |
|
||||
| packaging | string(50) | 包装方式 |
|
||||
| remarks | string(255) | 备注 |
|
||||
| file_path | string(255) | 文件路径 |
|
||||
|
||||
## 🐳 Docker 开发环境
|
||||
|
||||
### 开发环境配置 (`docker-compose.dev.yml`)
|
||||
- **应用服务**: Go + Air 热重载
|
||||
- **数据库服务**: MySQL 8.0
|
||||
- **网络配置**: 自定义网络 `app-network`
|
||||
- **卷挂载**: 代码实时同步,Go 模块缓存
|
||||
|
||||
## 🧪 测试
|
||||
|
||||
### 运行测试
|
||||
```bash
|
||||
# 运行所有测试
|
||||
go test ./...
|
||||
|
||||
# 运行特定包测试
|
||||
go test ./internal/handler
|
||||
|
||||
# 运行测试并显示覆盖率
|
||||
go test -cover ./...
|
||||
```
|
||||
|
||||
## 🔄 数据库迁移
|
||||
|
||||
### 自动迁移
|
||||
在开发环境中,设置 `reset_database: true` 会自动执行数据库迁移。
|
||||
|
||||
### 手动迁移
|
||||
1. 创建迁移文件在 `migrations/` 目录
|
||||
2. 使用 GORM 的 `AutoMigrate` 或手动执行 SQL
|
||||
|
||||
## 📊 部署
|
||||
|
||||
### 生产环境构建
|
||||
```bash
|
||||
# 构建生产镜像
|
||||
docker build -t zsp-backend-prod .
|
||||
|
||||
# 使用生产配置
|
||||
docker run -p 8080:8080 \
|
||||
-v /path/to/config.yaml:/app/config.yaml \
|
||||
zsp-backend-prod
|
||||
```
|
||||
|
||||
## 🛠️ 开发工具
|
||||
|
||||
### Air 热重载配置 (`.air.toml`)
|
||||
- 自动检测文件变化并重新编译
|
||||
- 排除测试文件和构建目录
|
||||
- 自定义构建命令和输出目录
|
||||
|
||||
### 代码规范
|
||||
- 使用 `go fmt` 格式化代码
|
||||
- 遵循 Go 官方代码规范
|
||||
- 使用有意义的包和函数命名
|
||||
|
||||
## 🤝 贡献指南
|
||||
|
||||
1. Fork 项目
|
||||
2. 创建功能分支 (`git checkout -b feature/新功能`)
|
||||
3. 提交更改 (`git commit -m '添加新功能'`)
|
||||
4. 推送到分支 (`git push origin feature/新功能`)
|
||||
5. 创建 Pull Request
|
||||
|
||||
## 📄 许可证
|
||||
|
||||
本项目采用 MIT 许可证 - 查看 [LICENSE](LICENSE) 文件了解详情。
|
||||
|
||||
## 📞 支持
|
||||
|
||||
如有问题或建议,请:
|
||||
1. 查看 Issues
|
||||
2. 提交新的 Issue
|
||||
3. 联系项目维护者
|
||||
|
||||
---
|
||||
|
||||
**最后更新**: 2024年1月
|
||||
**版本**: 1.0.0-dev
|
||||
95
backend/cmd/api/main.go
Executable file
95
backend/cmd/api/main.go
Executable file
@@ -0,0 +1,95 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strconv"
|
||||
"syscall"
|
||||
|
||||
"github.com/rovina/zsp-backend/internal/handler"
|
||||
"github.com/rovina/zsp-backend/internal/middleware"
|
||||
"github.com/rovina/zsp-backend/internal/repository"
|
||||
"github.com/rovina/zsp-backend/internal/server"
|
||||
"github.com/rovina/zsp-backend/internal/service"
|
||||
"github.com/rovina/zsp-backend/pkg/config"
|
||||
"github.com/rovina/zsp-backend/pkg/database"
|
||||
)
|
||||
|
||||
func main() {
|
||||
configPath := os.Getenv("CONFIG_PATH")
|
||||
if configPath == "" {
|
||||
configPath = "./configs/config.dev.yaml"
|
||||
}
|
||||
cfg, err := config.Load(configPath)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// 自动创建数据库
|
||||
err = database.EnsureDatabase(&cfg.Database)
|
||||
if err != nil {
|
||||
log.Fatalf("create db failed: %v", err)
|
||||
}
|
||||
|
||||
// 连接数据库
|
||||
db, err := database.NewDB(&cfg.Database)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// 自动建表
|
||||
err = database.AutoMigrate(db)
|
||||
if err != nil {
|
||||
log.Fatalf("migrate failed: %v", err)
|
||||
}
|
||||
|
||||
userRepo := repository.NewUserRepository(db)
|
||||
contractRepo := repository.NewContractRepository(db)
|
||||
contractFolderRepo := repository.NewContractFolderRepository(db)
|
||||
contractBatchRepo := repository.NewContractBatchRepository(db)
|
||||
zspContractRepo := repository.NewZSPContractRepository(db)
|
||||
userService, err := service.NewUserService(userRepo, cfg.JWT.Secret)
|
||||
if err != nil {
|
||||
log.Fatalf("error in userService Create: %s", err.Error())
|
||||
return
|
||||
}
|
||||
contractService, err := service.NewContractService(contractRepo)
|
||||
if err != nil {
|
||||
log.Fatalf("error in contractService Create: %s", err.Error())
|
||||
return
|
||||
}
|
||||
contractFolderService := service.NewContractFolderService(contractFolderRepo)
|
||||
contractBatchService := service.NewContractBatchService(contractBatchRepo)
|
||||
zspContractService, err := service.NewZSPContractService(zspContractRepo)
|
||||
if err != nil {
|
||||
log.Fatalf("error in zspContractService Create: %s", err.Error())
|
||||
return
|
||||
}
|
||||
zspFinancesService := service.NewZSPFinancesService(db)
|
||||
companyRepo := repository.NewCompanyRepository(db)
|
||||
companyService := service.NewCompanyService(companyRepo)
|
||||
|
||||
userHandler := handler.NewUserHandler(userService)
|
||||
contractHandler := handler.NewContractHandler(contractService, contractFolderService, contractBatchService)
|
||||
zspContractHandler := handler.NewZSPContractHandler(zspContractService)
|
||||
zspFinancesHandler := handler.NewZSPFinancesHandler(zspFinancesService)
|
||||
companyHandler := handler.NewCompanyHandler(companyService)
|
||||
deliveryRepo := repository.NewDeliveryRepository(db)
|
||||
deliveryService := service.NewDeliveryService(deliveryRepo)
|
||||
deliveryHandler := handler.NewDeliveryHandler(deliveryService)
|
||||
authMiddleware := middleware.AuthMiddleware(cfg.JWT.Secret)
|
||||
|
||||
router := server.SetupRouter(userHandler, contractHandler, zspContractHandler, zspFinancesHandler, companyHandler, deliveryHandler, authMiddleware)
|
||||
|
||||
go func() {
|
||||
log.Printf("Server starting on port %d", cfg.Server.Port)
|
||||
if err := router.Run(":" + strconv.Itoa(cfg.Server.Port)); err != nil {
|
||||
log.Fatalf("Failed to start server: %v", err)
|
||||
}
|
||||
}()
|
||||
quit := make(chan os.Signal, 1)
|
||||
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
|
||||
<-quit
|
||||
log.Println("Shutting down server...")
|
||||
}
|
||||
20
backend/configs/config.dev.yaml
Executable file
20
backend/configs/config.dev.yaml
Executable file
@@ -0,0 +1,20 @@
|
||||
# configs/config.yaml
|
||||
server:
|
||||
port: 8080
|
||||
mode: "debug"
|
||||
read_timeout: 30
|
||||
write_timeout: 30
|
||||
|
||||
database:
|
||||
host: "mysql"
|
||||
port: 3306
|
||||
user: "root"
|
||||
password: "password"
|
||||
dbname: "myapp"
|
||||
max_open_conns: 100
|
||||
max_idle_conns: 10
|
||||
reset_database: false
|
||||
|
||||
jwt:
|
||||
secret: "2Jkh4BdL6TVURttUEXlMeyD7rCLCkVvwdzPnddEIBRs="
|
||||
expire_hours: 24
|
||||
20
backend/configs/config.prod.yaml
Executable file
20
backend/configs/config.prod.yaml
Executable file
@@ -0,0 +1,20 @@
|
||||
# 生产环境配置
|
||||
server:
|
||||
port: 8080
|
||||
mode: "release"
|
||||
read_timeout: 30
|
||||
write_timeout: 30
|
||||
|
||||
database:
|
||||
host: "mysql"
|
||||
port: 3306
|
||||
user: "root"
|
||||
password: "password"
|
||||
dbname: "myapp"
|
||||
max_open_conns: 100
|
||||
max_idle_conns: 10
|
||||
reset_database: false # 生产环境务必关闭,否则重启会清空所有数据
|
||||
|
||||
jwt:
|
||||
secret: "2Jkh4BdL6TVURttUEXlMeyD7rCLCkVvwdzPnddEIBRs=" # 生产环境请更换为强密钥
|
||||
expire_hours: 24
|
||||
64
backend/docker-compose.dev.yml
Executable file
64
backend/docker-compose.dev.yml
Executable file
@@ -0,0 +1,64 @@
|
||||
# 开发环境部署(支持热重载)
|
||||
# 启动方式: docker-compose -f docker-compose.dev.yml up
|
||||
version: '3.8'
|
||||
services:
|
||||
app:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile.dev
|
||||
ports:
|
||||
- "8080:8080"
|
||||
depends_on:
|
||||
mysql:
|
||||
condition: service_healthy
|
||||
volumes:
|
||||
# 关键:将宿主机代码挂载到 /workspace,而不是 /app
|
||||
- .:/workspace
|
||||
# 缓存 Go 模块到宿主机,避免重复下载
|
||||
- go-mod-cache-dev:/go/pkg/mod
|
||||
- go-build-cache-dev:/root/.cache/go-build
|
||||
working_dir: /workspace # 设置容器工作目录
|
||||
environment:
|
||||
- GIN_MODE=debug
|
||||
- GOFLAGS=-mod=readonly
|
||||
|
||||
|
||||
# 设置用户为 root 避免权限问题(开发环境)
|
||||
user: root
|
||||
# 保持容器运行
|
||||
stdin_open: true
|
||||
tty: true
|
||||
networks:
|
||||
- app-network
|
||||
env_file:
|
||||
- ./.env.dev
|
||||
|
||||
mysql:
|
||||
image: mysql:8.0
|
||||
environment:
|
||||
MYSQL_ROOT_PASSWORD: password
|
||||
MYSQL_DATABASE: myapp
|
||||
MYSQL_PASSWORD: password
|
||||
ports:
|
||||
- "15782:3306"
|
||||
volumes:
|
||||
- mysql_data_dev:/var/lib/mysql
|
||||
healthcheck:
|
||||
test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "root", "-ppassword"]
|
||||
timeout: 20s
|
||||
retries: 10
|
||||
start_period: 30s
|
||||
networks:
|
||||
- app-network
|
||||
|
||||
volumes:
|
||||
mysql_data_dev:
|
||||
driver: local
|
||||
go-mod-cache-dev:
|
||||
driver: local
|
||||
go-build-cache-dev:
|
||||
driver: local
|
||||
|
||||
networks:
|
||||
app-network:
|
||||
driver: bridge
|
||||
56
backend/docker-compose.yml
Executable file
56
backend/docker-compose.yml
Executable file
@@ -0,0 +1,56 @@
|
||||
# 生产环境部署
|
||||
# 启动方式: docker-compose --env-file .env.prod up -d
|
||||
version: '3.8'
|
||||
services:
|
||||
app:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
ports:
|
||||
- "8080:8080"
|
||||
depends_on:
|
||||
mysql:
|
||||
condition: service_healthy
|
||||
working_dir: /app
|
||||
environment:
|
||||
- GIN_MODE=release
|
||||
- CONFIG_PATH=/app/configs/config.prod.yaml
|
||||
volumes:
|
||||
# 持久化上传文件、合同等业务数据
|
||||
- app_data:/app/data
|
||||
- ./configs:/app/configs
|
||||
- ./docker-entrypoint.sh:/docker-entrypoint.sh
|
||||
stdin_open: true
|
||||
tty: true
|
||||
networks:
|
||||
- app-network
|
||||
env_file:
|
||||
- ./.env.prod
|
||||
|
||||
mysql:
|
||||
image: mysql:8.0
|
||||
environment:
|
||||
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD:-password}
|
||||
MYSQL_DATABASE: ${MYSQL_DATABASE:-myapp}
|
||||
MYSQL_PASSWORD: ${MYSQL_PASSWORD:-password}
|
||||
ports:
|
||||
- "15782:3306"
|
||||
volumes:
|
||||
- mysql_data:/var/lib/mysql
|
||||
healthcheck:
|
||||
test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "root", "-p${MYSQL_ROOT_PASSWORD:-password}"]
|
||||
timeout: 20s
|
||||
retries: 10
|
||||
start_period: 30s
|
||||
networks:
|
||||
- app-network
|
||||
|
||||
volumes:
|
||||
mysql_data:
|
||||
driver: local
|
||||
app_data:
|
||||
driver: local
|
||||
|
||||
networks:
|
||||
app-network:
|
||||
driver: bridge
|
||||
5
backend/docker-entrypoint.sh
Executable file
5
backend/docker-entrypoint.sh
Executable file
@@ -0,0 +1,5 @@
|
||||
#!/bin/sh
|
||||
set -e
|
||||
# 确保 data 目录(volume 挂载点)对 appuser 可写
|
||||
chown -R appuser:appuser /app/data 2>/dev/null || true
|
||||
exec su-exec appuser /app/main "$@"
|
||||
432
backend/docs/DEVELOPMENT.md
Executable file
432
backend/docs/DEVELOPMENT.md
Executable file
@@ -0,0 +1,432 @@
|
||||
# 开发指南
|
||||
|
||||
本文档提供 ZSP 后端系统的详细开发说明。
|
||||
|
||||
## 开发环境设置
|
||||
|
||||
### 1. 环境准备
|
||||
|
||||
#### 使用 Docker(推荐)
|
||||
```bash
|
||||
# 1. 克隆项目
|
||||
git clone <repository-url>
|
||||
cd zsp-backend
|
||||
|
||||
# 2. 复制环境变量文件
|
||||
cp .env.dev.example .env.dev
|
||||
|
||||
# 3. 启动开发环境
|
||||
docker-compose -f docker-compose.dev.yml up --build
|
||||
|
||||
# 4. 查看日志
|
||||
docker-compose -f docker-compose.dev.yml logs -f app
|
||||
```
|
||||
|
||||
#### 本地开发
|
||||
```bash
|
||||
# 1. 安装 Go 1.25.4+
|
||||
# 2. 安装 Air
|
||||
go install github.com/air-verse/air@latest
|
||||
|
||||
# 3. 启动 MySQL
|
||||
docker run --name mysql-dev \
|
||||
-e MYSQL_ROOT_PASSWORD=password \
|
||||
-e MYSQL_DATABASE=myapp \
|
||||
-p 3306:3306 \
|
||||
-d mysql:8.0
|
||||
|
||||
# 4. 运行应用
|
||||
air -c .air.toml
|
||||
```
|
||||
|
||||
### 2. 项目结构说明
|
||||
|
||||
```
|
||||
internal/
|
||||
├── handler/ # HTTP 请求处理器
|
||||
├── middleware/ # 中间件(认证、日志等)
|
||||
├── model/ # 数据模型定义
|
||||
├── repository/ # 数据访问层
|
||||
├── service/ # 业务逻辑层
|
||||
└── server/ # 服务器配置和路由
|
||||
```
|
||||
|
||||
## 开发流程
|
||||
|
||||
### 1. 添加新功能模块
|
||||
|
||||
#### 步骤 1: 创建数据模型
|
||||
在 `internal/model/` 目录下创建新的模型文件:
|
||||
|
||||
```go
|
||||
// internal/model/product.go
|
||||
package model
|
||||
|
||||
type Product struct {
|
||||
ID uint `gorm:"primarykey" json:"id"`
|
||||
Name string `gorm:"size:100" json:"name"`
|
||||
Description string `gorm:"size:255" json:"description"`
|
||||
Price int `json:"price"`
|
||||
// ... 其他字段
|
||||
}
|
||||
```
|
||||
|
||||
#### 步骤 2: 创建 Repository
|
||||
在 `internal/repository/` 目录下创建数据访问层:
|
||||
|
||||
```go
|
||||
// internal/repository/product_repository.go
|
||||
package repository
|
||||
|
||||
import (
|
||||
"github.com/rovina/zsp-backend/internal/model"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type ProductRepository interface {
|
||||
Create(product *model.Product) error
|
||||
FindByID(id uint) (*model.Product, error)
|
||||
FindAll() ([]model.Product, error)
|
||||
Update(product *model.Product) error
|
||||
Delete(id uint) error
|
||||
}
|
||||
|
||||
type productRepository struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewProductRepository(db *gorm.DB) ProductRepository {
|
||||
return &productRepository{db: db}
|
||||
}
|
||||
|
||||
// 实现接口方法...
|
||||
```
|
||||
|
||||
#### 步骤 3: 创建 Service
|
||||
在 `internal/service/` 目录下创建业务逻辑层:
|
||||
|
||||
```go
|
||||
// internal/service/product_service.go
|
||||
package service
|
||||
|
||||
import (
|
||||
"github.com/rovina/zsp-backend/internal/model"
|
||||
"github.com/rovina/zsp-backend/internal/repository"
|
||||
)
|
||||
|
||||
type ProductService interface {
|
||||
CreateProduct(req *model.CreateProductRequest) (*model.Product, error)
|
||||
GetProduct(id uint) (*model.Product, error)
|
||||
ListProducts() ([]model.Product, error)
|
||||
}
|
||||
|
||||
type productService struct {
|
||||
repo repository.ProductRepository
|
||||
}
|
||||
|
||||
func NewProductService(repo repository.ProductRepository) ProductService {
|
||||
return &productService{repo: repo}
|
||||
}
|
||||
|
||||
// 实现业务逻辑...
|
||||
```
|
||||
|
||||
#### 步骤 4: 创建 Handler
|
||||
在 `internal/handler/` 目录下创建 HTTP 处理器:
|
||||
|
||||
```go
|
||||
// internal/handler/product_handler.go
|
||||
package handler
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/rovina/zsp-backend/internal/service"
|
||||
)
|
||||
|
||||
type ProductHandler struct {
|
||||
service service.ProductService
|
||||
}
|
||||
|
||||
func NewProductHandler(service service.ProductService) *ProductHandler {
|
||||
return &ProductHandler{service: service}
|
||||
}
|
||||
|
||||
func (h *ProductHandler) CreateProduct(c *gin.Context) {
|
||||
var req model.CreateProductRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
product, err := h.service.CreateProduct(&req)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusCreated, product)
|
||||
}
|
||||
|
||||
// 其他处理器方法...
|
||||
```
|
||||
|
||||
#### 步骤 5: 注册路由
|
||||
在 `internal/server/router.go` 中添加路由:
|
||||
|
||||
```go
|
||||
// internal/server/router.go
|
||||
func SetupRouter(userHandler *handler.UserHandler,
|
||||
contractHandler *handler.ContractHandler,
|
||||
productHandler *handler.ProductHandler, // 新增
|
||||
authMiddleware gin.HandlerFunc) *gin.Engine {
|
||||
router := gin.Default()
|
||||
|
||||
// 公共路由
|
||||
public := router.Group("/api/v1")
|
||||
{
|
||||
public.POST("/users/register", userHandler.Register)
|
||||
public.POST("/users/login", userHandler.Login)
|
||||
}
|
||||
|
||||
// 需要认证的路由
|
||||
authorized := router.Group("/api/v1")
|
||||
authorized.Use(authMiddleware)
|
||||
{
|
||||
// 用户相关
|
||||
authorized.GET("/users/profile", userHandler.GetProfile)
|
||||
|
||||
// 合同相关
|
||||
authorized.POST("/contracts", contractHandler.CreateContract)
|
||||
authorized.GET("/contracts", contractHandler.GetContracts)
|
||||
|
||||
// 产品相关(新增)
|
||||
authorized.POST("/products", productHandler.CreateProduct)
|
||||
authorized.GET("/products", productHandler.GetProducts)
|
||||
authorized.GET("/products/:id", productHandler.GetProduct)
|
||||
authorized.PUT("/products/:id", productHandler.UpdateProduct)
|
||||
authorized.DELETE("/products/:id", productHandler.DeleteProduct)
|
||||
}
|
||||
|
||||
return router
|
||||
}
|
||||
```
|
||||
|
||||
#### 步骤 6: 在主程序中初始化
|
||||
在 `cmd/api/main.go` 中初始化新的组件:
|
||||
|
||||
```go
|
||||
// cmd/api/main.go
|
||||
func main() {
|
||||
// ... 现有代码
|
||||
|
||||
// 初始化新的 Repository 和 Service
|
||||
productRepo := repository.NewProductRepository(db)
|
||||
productService, err := service.NewProductService(productRepo)
|
||||
if err != nil {
|
||||
log.Fatalf("error in productService Create: %s", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// 创建 Handler
|
||||
productHandler := handler.NewProductHandler(productService)
|
||||
|
||||
// 更新路由设置
|
||||
router := server.SetupRouter(userHandler, contractHandler, productHandler, authMiddleware)
|
||||
|
||||
// ... 剩余代码
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 数据库迁移
|
||||
|
||||
#### 自动迁移
|
||||
在开发环境中,设置 `reset_database: true` 会自动执行 GORM 的 AutoMigrate。
|
||||
|
||||
#### 手动创建迁移文件
|
||||
```bash
|
||||
# 创建迁移目录
|
||||
mkdir -p migrations
|
||||
|
||||
# 创建迁移文件
|
||||
cat > migrations/001_create_products_table.sql << 'EOF'
|
||||
CREATE TABLE IF NOT EXISTS products (
|
||||
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
name VARCHAR(100) NOT NULL,
|
||||
description VARCHAR(255),
|
||||
price INT NOT NULL DEFAULT 0,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
EOF
|
||||
```
|
||||
|
||||
### 3. 测试开发
|
||||
|
||||
#### 单元测试
|
||||
```go
|
||||
// internal/service/product_service_test.go
|
||||
package service
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
)
|
||||
|
||||
// 创建 Mock Repository
|
||||
type mockProductRepository struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
func (m *mockProductRepository) Create(product *model.Product) error {
|
||||
args := m.Called(product)
|
||||
return args.Error(0)
|
||||
}
|
||||
|
||||
func TestProductService_CreateProduct(t *testing.T) {
|
||||
// 测试用例...
|
||||
}
|
||||
```
|
||||
|
||||
#### API 测试
|
||||
使用 curl 或 Postman 测试 API:
|
||||
|
||||
```bash
|
||||
# 测试用户登录
|
||||
curl -X POST http://localhost:8080/api/v1/users/login \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"email":"test@example.com","password":"password123"}'
|
||||
|
||||
# 测试创建合同(需要认证)
|
||||
curl -X POST http://localhost:8080/api/v1/contracts \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Authorization: Bearer <jwt-token>" \
|
||||
-d '{
|
||||
"name": "测试合同",
|
||||
"contract_type": "采购",
|
||||
"contract_code": "TEST001"
|
||||
}'
|
||||
```
|
||||
|
||||
## 代码规范
|
||||
|
||||
### 1. 命名约定
|
||||
- 包名:小写,单数形式
|
||||
- 接口名:以 "er" 结尾(如 Repository, Handler)
|
||||
- 结构体名:首字母大写
|
||||
- 变量名:驼峰式
|
||||
|
||||
### 2. 错误处理
|
||||
```go
|
||||
// 正确:返回错误
|
||||
func GetUser(id uint) (*model.User, error) {
|
||||
user, err := repo.FindByID(id)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get user: %w", err)
|
||||
}
|
||||
return user, nil
|
||||
}
|
||||
|
||||
// 错误:panic 或忽略错误
|
||||
func BadExample() {
|
||||
user, _ := repo.FindByID(1) // 不要忽略错误
|
||||
// 或
|
||||
panic("something went wrong") // 不要使用 panic
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 日志记录
|
||||
```go
|
||||
import "log"
|
||||
|
||||
// 使用结构化日志
|
||||
log.Printf("User %d logged in", userID)
|
||||
log.Printf("Failed to create contract: %v", err)
|
||||
|
||||
// 在生产环境中考虑使用更高级的日志库
|
||||
```
|
||||
|
||||
## 调试技巧
|
||||
|
||||
### 1. 使用 Air 热重载
|
||||
Air 会自动检测文件变化并重新编译。修改代码后无需手动重启。
|
||||
|
||||
### 2. 数据库调试
|
||||
```bash
|
||||
# 进入 MySQL 容器
|
||||
docker exec -it zsp-backend-mysql-1 mysql -uroot -ppassword myapp
|
||||
|
||||
# 查看表结构
|
||||
DESCRIBE users;
|
||||
DESCRIBE contracts;
|
||||
|
||||
# 查询数据
|
||||
SELECT * FROM users;
|
||||
SELECT * FROM contracts;
|
||||
```
|
||||
|
||||
### 3. 查看应用日志
|
||||
```bash
|
||||
# Docker 环境
|
||||
docker-compose -f docker-compose.dev.yml logs -f app
|
||||
|
||||
# 查看特定服务的日志
|
||||
docker-compose -f docker-compose.dev.yml logs app --tail=100
|
||||
|
||||
# 本地环境
|
||||
tail -f tmp/air.log
|
||||
```
|
||||
|
||||
## 常见问题
|
||||
|
||||
### 1. 数据库连接失败
|
||||
**问题**: `Failed to connect to database`
|
||||
**解决**:
|
||||
1. 检查 MySQL 容器是否运行: `docker ps | grep mysql`
|
||||
2. 检查连接配置: `configs/config.dev.yaml`
|
||||
3. 检查网络: `docker network ls`
|
||||
|
||||
### 2. 热重载不工作
|
||||
**问题**: Air 不检测文件变化
|
||||
**解决**:
|
||||
1. 检查 `.air.toml` 配置
|
||||
2. 确保文件在挂载的卷中
|
||||
3. 重启 Air: `docker-compose restart app`
|
||||
|
||||
### 3. 权限问题
|
||||
**问题**: 文件写入权限错误
|
||||
**解决**:
|
||||
```bash
|
||||
# 在容器内运行
|
||||
docker exec -it zsp-backend-app-1 chmod -R 755 /workspace/uploads
|
||||
```
|
||||
|
||||
## 性能优化建议
|
||||
|
||||
### 1. 数据库优化
|
||||
- 为常用查询字段添加索引
|
||||
- 使用连接池配置
|
||||
- 避免 N+1 查询问题
|
||||
|
||||
### 2. API 优化
|
||||
- 实现分页查询
|
||||
- 使用缓存(Redis)
|
||||
- 压缩响应数据
|
||||
|
||||
### 3. 内存优化
|
||||
- 及时关闭数据库连接
|
||||
- 使用连接池
|
||||
- 避免内存泄漏
|
||||
|
||||
## 下一步
|
||||
|
||||
1. ✅ 完成基础框架搭建
|
||||
2. ✅ 实现用户认证模块
|
||||
3. ✅ 实现合同管理模块
|
||||
4. 🔄 添加文件上传功能
|
||||
5. 🔄 实现权限控制(RBAC)
|
||||
6. 🔄 添加 API 文档(Swagger)
|
||||
7. 🔄 实现监控和日志收集
|
||||
8. 🔄 编写完整的测试套件
|
||||
61
backend/go.mod
Executable file
61
backend/go.mod
Executable file
@@ -0,0 +1,61 @@
|
||||
module github.com/rovina/zsp-backend
|
||||
|
||||
go 1.25.4
|
||||
|
||||
require (
|
||||
github.com/gin-contrib/cors v1.7.6
|
||||
github.com/gin-gonic/gin v1.10.1
|
||||
github.com/spf13/viper v1.21.0
|
||||
github.com/xuri/excelize/v2 v2.9.0
|
||||
gorm.io/driver/mysql v1.5.2
|
||||
gorm.io/gorm v1.31.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/bytedance/sonic/loader v0.2.4 // indirect
|
||||
github.com/cloudwego/base64x v0.1.5 // indirect
|
||||
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect
|
||||
github.com/richardlehane/mscfb v1.0.4 // indirect
|
||||
github.com/richardlehane/msoleps v1.0.4 // indirect
|
||||
github.com/xuri/efp v0.0.0-20240408161823-9ad904a10d6d // indirect
|
||||
github.com/xuri/nfp v0.0.0-20240318013403-ab9948c2c4a7 // indirect
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/bytedance/sonic v1.13.3 // indirect
|
||||
github.com/fsnotify/fsnotify v1.9.0 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.9 // indirect
|
||||
github.com/gin-contrib/sse v1.1.0 // indirect
|
||||
github.com/go-playground/locales v0.14.1 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/go-playground/validator/v10 v10.26.0 // indirect
|
||||
github.com/go-sql-driver/mysql v1.7.0 // indirect
|
||||
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
|
||||
github.com/goccy/go-json v0.10.5 // indirect
|
||||
github.com/golang-jwt/jwt/v4 v4.5.2
|
||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||
github.com/jinzhu/now v1.1.5 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.10 // indirect
|
||||
github.com/leodido/go-urn v1.4.0 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||
github.com/sagikazarmark/locafero v0.11.0 // indirect
|
||||
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect
|
||||
github.com/spf13/afero v1.15.0 // indirect
|
||||
github.com/spf13/cast v1.10.0 // indirect
|
||||
github.com/spf13/pflag v1.0.10 // indirect
|
||||
github.com/subosito/gotenv v1.6.0 // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/ugorji/go/codec v1.3.0 // indirect
|
||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||
golang.org/x/arch v0.18.0 // indirect
|
||||
golang.org/x/crypto v0.39.0
|
||||
golang.org/x/net v0.41.0 // indirect
|
||||
golang.org/x/sys v0.33.0 // indirect
|
||||
golang.org/x/text v0.28.0 // indirect
|
||||
google.golang.org/protobuf v1.36.6 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
141
backend/go.sum
Executable file
141
backend/go.sum
Executable file
@@ -0,0 +1,141 @@
|
||||
github.com/bytedance/sonic v1.13.3 h1:MS8gmaH16Gtirygw7jV91pDCN33NyMrPbN7qiYhEsF0=
|
||||
github.com/bytedance/sonic v1.13.3/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1+KgkJhz4=
|
||||
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
|
||||
github.com/bytedance/sonic/loader v0.2.4 h1:ZWCw4stuXUsn1/+zQDqeE7JKP+QO47tz7QCNan80NzY=
|
||||
github.com/bytedance/sonic/loader v0.2.4/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
|
||||
github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4=
|
||||
github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
|
||||
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
|
||||
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
||||
github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY=
|
||||
github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok=
|
||||
github.com/gin-contrib/cors v1.7.6 h1:3gQ8GMzs1Ylpf70y8bMw4fVpycXIeX1ZemuSQIsnQQY=
|
||||
github.com/gin-contrib/cors v1.7.6/go.mod h1:Ulcl+xN4jel9t1Ry8vqph23a60FwH9xVLd+3ykmTjOk=
|
||||
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
|
||||
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
|
||||
github.com/gin-gonic/gin v1.10.1 h1:T0ujvqyCSqRopADpgPgiTT63DUQVSfojyME59Ei63pQ=
|
||||
github.com/gin-gonic/gin v1.10.1/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
|
||||
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||
github.com/go-playground/validator/v10 v10.26.0 h1:SP05Nqhjcvz81uJaRfEV0YBSSSGMc/iMaVtFbr3Sw2k=
|
||||
github.com/go-playground/validator/v10 v10.26.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
|
||||
github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc=
|
||||
github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
|
||||
github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
|
||||
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
||||
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
|
||||
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||
github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI=
|
||||
github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
||||
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
||||
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||
github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=
|
||||
github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw=
|
||||
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8=
|
||||
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
|
||||
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/richardlehane/mscfb v1.0.4 h1:WULscsljNPConisD5hR0+OyZjwK46Pfyr6mPu5ZawpM=
|
||||
github.com/richardlehane/mscfb v1.0.4/go.mod h1:YzVpcZg9czvAuhk9T+a3avCpcFPMUWm7gK3DypaEsUk=
|
||||
github.com/richardlehane/msoleps v1.0.1/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg=
|
||||
github.com/richardlehane/msoleps v1.0.4 h1:WuESlvhX3gH2IHcd8UqyCuFY5yiq/GR/yqaSM/9/g00=
|
||||
github.com/richardlehane/msoleps v1.0.4/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg=
|
||||
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
|
||||
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||
github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc=
|
||||
github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik=
|
||||
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw=
|
||||
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U=
|
||||
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
|
||||
github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=
|
||||
github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY=
|
||||
github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=
|
||||
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
|
||||
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU=
|
||||
github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
|
||||
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||
github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA=
|
||||
github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
|
||||
github.com/xuri/efp v0.0.0-20240408161823-9ad904a10d6d h1:llb0neMWDQe87IzJLS4Ci7psK/lVsjIS2otl+1WyRyY=
|
||||
github.com/xuri/efp v0.0.0-20240408161823-9ad904a10d6d/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI=
|
||||
github.com/xuri/excelize/v2 v2.9.0 h1:1tgOaEq92IOEumR1/JfYS/eR0KHOCsRv/rYXXh6YJQE=
|
||||
github.com/xuri/excelize/v2 v2.9.0/go.mod h1:uqey4QBZ9gdMeWApPLdhm9x+9o2lq4iVmjiLfBS5hdE=
|
||||
github.com/xuri/nfp v0.0.0-20240318013403-ab9948c2c4a7 h1:hPVCafDV85blFTabnqKgNhDCkJX25eik94Si9cTER4A=
|
||||
github.com/xuri/nfp v0.0.0-20240318013403-ab9948c2c4a7/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ=
|
||||
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||
golang.org/x/arch v0.18.0 h1:WN9poc33zL4AzGxqf8VtpKUnGvMi8O9lhNyBMF/85qc=
|
||||
golang.org/x/arch v0.18.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk=
|
||||
golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM=
|
||||
golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U=
|
||||
golang.org/x/image v0.18.0 h1:jGzIakQa/ZXI1I0Fxvaa9W7yP25TqT6cHIHn+6CqvSQ=
|
||||
golang.org/x/image v0.18.0/go.mod h1:4yyo5vMFQjVjUcVk4jEQcU9MGy/rulF5WvUILseCM2E=
|
||||
golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw=
|
||||
golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
|
||||
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
|
||||
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
|
||||
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
|
||||
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gorm.io/driver/mysql v1.5.2 h1:QC2HRskSE75wBuOxe0+iCkyJZ+RqpudsQtqkp+IMuXs=
|
||||
gorm.io/driver/mysql v1.5.2/go.mod h1:pQLhh1Ut/WUAySdTHwBpBv6+JKcj+ua4ZFx1QQTBzb8=
|
||||
gorm.io/gorm v1.25.2-0.20230530020048-26663ab9bf55/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k=
|
||||
gorm.io/gorm v1.31.1 h1:7CA8FTFz/gRfgqgpeKIBcervUn3xSyPUmr6B2WXJ7kg=
|
||||
gorm.io/gorm v1.31.1/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs=
|
||||
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
|
||||
6
backend/internal/dto/error.go
Executable file
6
backend/internal/dto/error.go
Executable file
@@ -0,0 +1,6 @@
|
||||
package dto
|
||||
|
||||
type ErrorResponse struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
18
backend/internal/dto/user.go
Executable file
18
backend/internal/dto/user.go
Executable file
@@ -0,0 +1,18 @@
|
||||
package dto
|
||||
|
||||
type CreateUserRequest struct {
|
||||
Username string `json:"username" binding:"required,min=3,max=50"`
|
||||
Email string `json:"email" binding:"required,email"`
|
||||
Password string `json:"password" binding:"required,min=6"`
|
||||
}
|
||||
|
||||
type LoginRequest struct {
|
||||
Email string `json:"email" binding:"required,email"`
|
||||
Password string `json:"password" binding:"required"`
|
||||
}
|
||||
|
||||
type UserResponse struct {
|
||||
ID uint `json:"id"`
|
||||
Username string `json:"username"`
|
||||
Email string `json:"email"`
|
||||
}
|
||||
19
backend/internal/dto/zsp_contract.go
Executable file
19
backend/internal/dto/zsp_contract.go
Executable file
@@ -0,0 +1,19 @@
|
||||
package dto
|
||||
|
||||
type ZSPContractCreateRequest struct {
|
||||
ContractNumber string `json:"contract_number" binding:"required"`
|
||||
ContractType string `json:"contract_type" binding:"required"`
|
||||
FolderID uint `json:"folder_id" binding:"required"`
|
||||
SignDate string `json:"sign_date" binding:"required"`
|
||||
CompanyName string `json:"company_name" binding:"required"`
|
||||
Commodity string `json:"commodity" binding:"required"`
|
||||
}
|
||||
|
||||
type ZSPContractUpdateRequest struct {
|
||||
ContractNumber string `json:"contractNumber"`
|
||||
CompanyName string `json:"companyName"`
|
||||
Commodity string `json:"commodity"`
|
||||
SignDate string `json:"signDate"`
|
||||
ContractType string `json:"contractType"`
|
||||
FolderID uint `json:"folderId"`
|
||||
}
|
||||
50
backend/internal/dto/zsp_contract_detail.go
Executable file
50
backend/internal/dto/zsp_contract_detail.go
Executable file
@@ -0,0 +1,50 @@
|
||||
package dto
|
||||
|
||||
// ZSPContractDetailCreateRequest 创建合同详情请求
|
||||
type ZSPContractDetailCreateRequest struct {
|
||||
ContractFileID uint `json:"contract_file_id" binding:"required"`
|
||||
CommodityName string `json:"commodity_name" binding:"required"`
|
||||
CommodityCode string `json:"commodity_code"`
|
||||
TotalQuantity float64 `json:"total_quantity" binding:"required"`
|
||||
Unit string `json:"unit"`
|
||||
UnitPrice float64 `json:"unit_price" binding:"required"`
|
||||
DeliveredQty float64 `json:"delivered_qty"`
|
||||
BillOfLading string `json:"bill_of_lading"`
|
||||
Remark string `json:"remark"`
|
||||
}
|
||||
|
||||
// ZSPContractDetailUpdateRequest 更新合同详情请求
|
||||
type ZSPContractDetailUpdateRequest struct {
|
||||
ID uint `json:"id" binding:"required"`
|
||||
CommodityName string `json:"commodity_name"`
|
||||
CommodityCode string `json:"commodity_code"`
|
||||
TotalQuantity float64 `json:"total_quantity"`
|
||||
Unit string `json:"unit"`
|
||||
UnitPrice float64 `json:"unit_price"`
|
||||
DeliveredQty float64 `json:"delivered_qty"`
|
||||
BillOfLading string `json:"bill_of_lading"`
|
||||
Remark string `json:"remark"`
|
||||
}
|
||||
|
||||
// ZSPContractDetailBatchCreateRequest 批量创建合同详情请求
|
||||
type ZSPContractDetailBatchCreateRequest struct {
|
||||
ContractFileID uint `json:"contract_file_id" binding:"required"`
|
||||
Details []ZSPContractDetailCreateRequest `json:"details" binding:"required"`
|
||||
}
|
||||
|
||||
// ZSPContractDetailResponse 合同详情响应
|
||||
type ZSPContractDetailResponse struct {
|
||||
ID uint `json:"id"`
|
||||
ContractFileID uint `json:"contract_file_id"`
|
||||
CommodityName string `json:"commodity_name"`
|
||||
CommodityCode string `json:"commodity_code"`
|
||||
TotalQuantity float64 `json:"total_quantity"`
|
||||
Unit string `json:"unit"`
|
||||
UnitPrice float64 `json:"unit_price"`
|
||||
DeliveredQty float64 `json:"delivered_qty"`
|
||||
PendingQty float64 `json:"pending_qty"`
|
||||
BillOfLading string `json:"bill_of_lading"`
|
||||
Remark string `json:"remark"`
|
||||
CreateTime string `json:"create_time"`
|
||||
UpdateTime string `json:"update_time"`
|
||||
}
|
||||
101
backend/internal/dto/zsp_finances.go
Executable file
101
backend/internal/dto/zsp_finances.go
Executable file
@@ -0,0 +1,101 @@
|
||||
package dto
|
||||
|
||||
// ==================== 货代费用 ====================
|
||||
|
||||
type ZSPFreightChargesCreateRequest struct {
|
||||
BillOfLading string `json:"billOfLading"`
|
||||
ContractNumber string `json:"contractNumber"`
|
||||
ExpenseItem string `json:"expenseItem" binding:"required"`
|
||||
Amount float64 `json:"amount" binding:"required"`
|
||||
Currency string `json:"currency"`
|
||||
PaymentDate string `json:"paymentDate" binding:"required"`
|
||||
FreightCompanyName string `json:"freightCompanyName" binding:"required"`
|
||||
Remark string `json:"remark"`
|
||||
}
|
||||
|
||||
type ZSPFreightChargesUpdateRequest struct {
|
||||
BillOfLading string `json:"billOfLading"`
|
||||
ContractNumber string `json:"contractNumber"`
|
||||
ExpenseItem string `json:"expenseItem"`
|
||||
Amount float64 `json:"amount"`
|
||||
Currency string `json:"currency"`
|
||||
PaymentDate string `json:"paymentDate"`
|
||||
FreightCompanyName string `json:"freightCompanyName"`
|
||||
Remark string `json:"remark"`
|
||||
}
|
||||
|
||||
// ==================== 其他杂费 ====================
|
||||
|
||||
type ZSPMiscChargesCreateRequest struct {
|
||||
BillOfLading string `json:"billOfLading"`
|
||||
ContractNumber string `json:"contractNumber"`
|
||||
ExpenseItem string `json:"expenseItem" binding:"required"`
|
||||
Amount float64 `json:"amount" binding:"required"`
|
||||
Currency string `json:"currency"`
|
||||
PaymentDate string `json:"paymentDate" binding:"required"`
|
||||
CompanyName string `json:"companyName"`
|
||||
Remark string `json:"remark"`
|
||||
}
|
||||
|
||||
type ZSPMiscChargesUpdateRequest struct {
|
||||
BillOfLading string `json:"billOfLading"`
|
||||
ContractNumber string `json:"contractNumber"`
|
||||
ExpenseItem string `json:"expenseItem"`
|
||||
Amount float64 `json:"amount"`
|
||||
Currency string `json:"currency"`
|
||||
PaymentDate string `json:"paymentDate"`
|
||||
CompanyName string `json:"companyName"`
|
||||
Remark string `json:"remark"`
|
||||
}
|
||||
|
||||
// ==================== 服务费 ====================
|
||||
|
||||
type ZSPServiceFeesCreateRequest struct {
|
||||
BillOfLading string `json:"billOfLading"`
|
||||
ContractNumber string `json:"contractNumber"`
|
||||
Name string `json:"name" binding:"required"`
|
||||
Amount float64 `json:"amount" binding:"required"`
|
||||
Currency string `json:"currency"`
|
||||
Date string `json:"date" binding:"required"`
|
||||
Remark string `json:"remark"`
|
||||
}
|
||||
|
||||
type ZSPServiceFeesUpdateRequest struct {
|
||||
BillOfLading string `json:"billOfLading"`
|
||||
ContractNumber string `json:"contractNumber"`
|
||||
Name string `json:"name"`
|
||||
Amount float64 `json:"amount"`
|
||||
Currency string `json:"currency"`
|
||||
Date string `json:"date"`
|
||||
Remark string `json:"remark"`
|
||||
}
|
||||
|
||||
// ==================== 成本构成 ====================
|
||||
|
||||
type ZSPCostBreakdownCreateRequest struct {
|
||||
BusinessID string `json:"businessId" binding:"required"`
|
||||
BusinessType string `json:"businessType" binding:"required"`
|
||||
}
|
||||
|
||||
type ZSPCostBreakdownUpdateRequest struct {
|
||||
FreightChargeTotal float64 `json:"freightChargeTotal"`
|
||||
MiscChargeTotal float64 `json:"miscChargeTotal"`
|
||||
ServiceFeeTotal float64 `json:"serviceFeeTotal"`
|
||||
}
|
||||
|
||||
// ==================== 利润记录 ====================
|
||||
|
||||
type ZSPProfitRecordCreateRequest struct {
|
||||
BusinessID string `json:"businessId" binding:"required"`
|
||||
BusinessType string `json:"businessType" binding:"required"`
|
||||
Revenue float64 `json:"revenue" binding:"required"`
|
||||
TotalCost float64 `json:"totalCost" binding:"required"`
|
||||
RecordDate string `json:"recordDate" binding:"required"`
|
||||
Remark string `json:"remark"`
|
||||
}
|
||||
|
||||
type ZSPProfitRecordUpdateRequest struct {
|
||||
Revenue float64 `json:"revenue"`
|
||||
TotalCost float64 `json:"totalCost"`
|
||||
Remark string `json:"remark"`
|
||||
}
|
||||
11
backend/internal/dto/zsp_folder.go
Executable file
11
backend/internal/dto/zsp_folder.go
Executable file
@@ -0,0 +1,11 @@
|
||||
package dto
|
||||
|
||||
type ZSPFolderCreateRequest struct {
|
||||
Name string `json:"name" binding:"required"`
|
||||
Description string `json:"description"`
|
||||
}
|
||||
|
||||
type ZSPFolderUpdateRequest struct {
|
||||
Name string `json:"name" binding:"required"`
|
||||
Description string `json:"description"`
|
||||
}
|
||||
176
backend/internal/handler/company_handler.go
Executable file
176
backend/internal/handler/company_handler.go
Executable file
@@ -0,0 +1,176 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/rovina/zsp-backend/internal/service"
|
||||
)
|
||||
|
||||
type CompanyHandler struct {
|
||||
service service.CompanyService
|
||||
}
|
||||
|
||||
func NewCompanyHandler(s service.CompanyService) *CompanyHandler {
|
||||
return &CompanyHandler{service: s}
|
||||
}
|
||||
|
||||
func (h *CompanyHandler) ListFolders(c *gin.Context) {
|
||||
list, err := h.service.ListFolders()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "data": list})
|
||||
}
|
||||
|
||||
func (h *CompanyHandler) CreateFolder(c *gin.Context) {
|
||||
var body struct {
|
||||
Name string `json:"name" binding:"required"`
|
||||
Description string `json:"description"`
|
||||
Category string `json:"category"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&body); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
f, err := h.service.CreateFolder(body.Name, body.Description, body.Category)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "data": f})
|
||||
}
|
||||
|
||||
func (h *CompanyHandler) UpdateFolder(c *gin.Context) {
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "invalid id"})
|
||||
return
|
||||
}
|
||||
var body struct {
|
||||
Name string `json:"name" binding:"required"`
|
||||
Description string `json:"description"`
|
||||
Category string `json:"category"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&body); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
if err := h.service.UpdateFolder(uint(id), body.Name, body.Description, body.Category); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "message": "updated"})
|
||||
}
|
||||
|
||||
func (h *CompanyHandler) DeleteFolder(c *gin.Context) {
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "invalid id"})
|
||||
return
|
||||
}
|
||||
if err := h.service.DeleteFolder(uint(id)); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "message": "deleted"})
|
||||
}
|
||||
|
||||
func (h *CompanyHandler) ListFiles(c *gin.Context) {
|
||||
var folderID *uint
|
||||
if idStr := c.Query("folderId"); idStr != "" {
|
||||
if id, err := strconv.ParseUint(idStr, 10, 32); err == nil {
|
||||
uid := uint(id)
|
||||
folderID = &uid
|
||||
}
|
||||
}
|
||||
list, err := h.service.ListFiles(folderID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "data": list})
|
||||
}
|
||||
|
||||
func (h *CompanyHandler) UploadFiles(c *gin.Context) {
|
||||
form, err := c.MultipartForm()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
files := form.File["files"]
|
||||
if len(files) == 0 {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "no files"})
|
||||
return
|
||||
}
|
||||
|
||||
var dataStr string
|
||||
if v := form.Value["data"]; len(v) > 0 {
|
||||
dataStr = v[0]
|
||||
}
|
||||
var data struct {
|
||||
CompanyName string `json:"companyName"`
|
||||
FileCategory string `json:"fileCategory"`
|
||||
ExpireDate string `json:"expireDate"`
|
||||
Description string `json:"description"`
|
||||
FolderID string `json:"folderId"`
|
||||
}
|
||||
if dataStr != "" {
|
||||
_ = json.Unmarshal([]byte(dataStr), &data)
|
||||
}
|
||||
|
||||
var folderID *uint
|
||||
if data.FolderID != "" {
|
||||
if id, err := strconv.ParseUint(data.FolderID, 10, 32); err == nil {
|
||||
uid := uint(id)
|
||||
folderID = &uid
|
||||
}
|
||||
}
|
||||
|
||||
count, err := h.service.UploadFiles(folderID, data.CompanyName, data.FileCategory, data.ExpireDate, data.Description, files)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "data": gin.H{"uploadedCount": count}, "message": "uploaded"})
|
||||
}
|
||||
|
||||
func (h *CompanyHandler) UpdateFile(c *gin.Context) {
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "invalid id"})
|
||||
return
|
||||
}
|
||||
var body struct {
|
||||
CompanyName string `json:"companyName"`
|
||||
FileCategory string `json:"fileCategory"`
|
||||
ExpireDate string `json:"expireDate"`
|
||||
Description string `json:"description"`
|
||||
FolderID *uint `json:"folderId"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&body); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
if err := h.service.UpdateFile(uint(id), body.CompanyName, body.FileCategory, body.ExpireDate, body.Description, body.FolderID); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "message": "updated"})
|
||||
}
|
||||
|
||||
func (h *CompanyHandler) DeleteFile(c *gin.Context) {
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "invalid id"})
|
||||
return
|
||||
}
|
||||
if err := h.service.DeleteFile(uint(id)); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "message": "deleted"})
|
||||
}
|
||||
482
backend/internal/handler/contract_handler.go
Executable file
482
backend/internal/handler/contract_handler.go
Executable file
@@ -0,0 +1,482 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/rovina/zsp-backend/internal/model"
|
||||
"github.com/rovina/zsp-backend/internal/service"
|
||||
)
|
||||
|
||||
func (h *ContractHandler) List(c *gin.Context) {
|
||||
contractType := c.Query("contractType")
|
||||
list, err := h.service.List(contractType)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "data": list})
|
||||
}
|
||||
|
||||
func (h *ContractHandler) Get(c *gin.Context) {
|
||||
idStr := c.Param("id")
|
||||
id, err := strconv.ParseUint(idStr, 10, 32)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "invalid id"})
|
||||
return
|
||||
}
|
||||
contract, err := h.service.GetByID(uint(id))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"success": false, "message": "contract not found"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "data": contract})
|
||||
}
|
||||
|
||||
func (h *ContractHandler) Update(c *gin.Context) {
|
||||
idStr := c.Param("id")
|
||||
id, err := strconv.ParseUint(idStr, 10, 32)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "invalid id"})
|
||||
return
|
||||
}
|
||||
existing, err := h.service.GetByID(uint(id))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"success": false, "message": "contract not found"})
|
||||
return
|
||||
}
|
||||
var body struct {
|
||||
Name string `json:"name"`
|
||||
ContractType string `json:"contractType"`
|
||||
ContractCode string `json:"contractCode"`
|
||||
BuyParty string `json:"buyParty"`
|
||||
SellParty string `json:"sellParty"`
|
||||
Commodity string `json:"commodity"`
|
||||
Count float64 `json:"count"`
|
||||
UnitPrice float64 `json:"unitPrice"`
|
||||
BLNumber string `json:"blNumber"`
|
||||
DeliveryTime string `json:"deliveryTime"`
|
||||
PickUpTime string `json:"pickUpTime"`
|
||||
Packaging string `json:"packaging"`
|
||||
Remarks string `json:"remarks"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&body); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
existing.Name = body.Name
|
||||
existing.ContractType = body.ContractType
|
||||
existing.ContractCode = body.ContractCode
|
||||
existing.BuyParty = body.BuyParty
|
||||
existing.SellParty = body.SellParty
|
||||
existing.Commodity = body.Commodity
|
||||
existing.Count = body.Count
|
||||
existing.UnitPrice = body.UnitPrice
|
||||
existing.BLNumber = body.BLNumber
|
||||
existing.DeliveryTime = body.DeliveryTime
|
||||
existing.PickUpTime = body.PickUpTime
|
||||
existing.Packaging = body.Packaging
|
||||
existing.Remarks = body.Remarks
|
||||
existing.UpdateTime = time.Now()
|
||||
if err := h.service.Update(existing); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "message": "updated"})
|
||||
}
|
||||
|
||||
func (h *ContractHandler) UpdateWithFiles(c *gin.Context) {
|
||||
idStr := c.Param("id")
|
||||
id, err := strconv.ParseUint(idStr, 10, 32)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "invalid id"})
|
||||
return
|
||||
}
|
||||
|
||||
form, err := c.MultipartForm()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
formMap := make(map[string]string)
|
||||
for k, v := range form.Value {
|
||||
if len(v) > 0 {
|
||||
formMap[k] = v[0]
|
||||
}
|
||||
}
|
||||
|
||||
keepFileIds := parseUintSlice(formMap["keepFileIds"])
|
||||
newFileHeaders := form.File["files"]
|
||||
|
||||
if len(keepFileIds) == 0 && len(newFileHeaders) == 0 {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "至少需要保留一个合同扫描件"})
|
||||
return
|
||||
}
|
||||
|
||||
var newFileModels []model.ContractFile
|
||||
path := "./data/contract/uploads"
|
||||
os.MkdirAll(path, os.ModePerm)
|
||||
|
||||
for _, fh := range newFileHeaders {
|
||||
filename := fmt.Sprintf("%d_%s", time.Now().UnixNano(), fh.Filename)
|
||||
savePath := filepath.Join(path, filename)
|
||||
if err := c.SaveUploadedFile(fh, savePath); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
newFileModels = append(newFileModels, model.ContractFile{
|
||||
Name: fh.Filename,
|
||||
URL: "uploads/" + filename,
|
||||
})
|
||||
}
|
||||
|
||||
if len(keepFileIds)+len(newFileModels) == 0 {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "至少需要保留一个合同扫描件"})
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.service.UpdateWithFiles(uint(id), keepFileIds, newFileModels, formMap); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "message": "updated"})
|
||||
}
|
||||
|
||||
func parseUintSlice(s string) []uint {
|
||||
if s == "" {
|
||||
return nil
|
||||
}
|
||||
parts := strings.Split(s, ",")
|
||||
var result []uint
|
||||
for _, p := range parts {
|
||||
p = strings.TrimSpace(p)
|
||||
if p == "" {
|
||||
continue
|
||||
}
|
||||
if v, err := strconv.ParseUint(p, 10, 32); err == nil {
|
||||
result = append(result, uint(v))
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func (h *ContractHandler) Delete(c *gin.Context) {
|
||||
idStr := c.Param("id")
|
||||
id, err := strconv.ParseUint(idStr, 10, 32)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "invalid id"})
|
||||
return
|
||||
}
|
||||
if err := h.service.Delete(uint(id)); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "message": "deleted"})
|
||||
}
|
||||
|
||||
type ContractHandler struct {
|
||||
service service.ContractService
|
||||
folderService service.ContractFolderService
|
||||
batchService service.ContractBatchService
|
||||
}
|
||||
|
||||
func NewContractHandler(
|
||||
service service.ContractService,
|
||||
folderService service.ContractFolderService,
|
||||
batchService service.ContractBatchService,
|
||||
) *ContractHandler {
|
||||
return &ContractHandler{
|
||||
service: service,
|
||||
folderService: folderService,
|
||||
batchService: batchService,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *ContractHandler) Upload(c *gin.Context) {
|
||||
|
||||
var contract model.Contract
|
||||
|
||||
contract.Name = c.PostForm("name")
|
||||
contract.ContractType = c.PostForm("contractType")
|
||||
contract.ContractCode = c.PostForm("contractCode")
|
||||
contract.BuyParty = c.PostForm("buyParty")
|
||||
contract.SellParty = c.PostForm("sellParty")
|
||||
contract.Commodity = c.PostForm("commodity")
|
||||
contract.BLNumber = c.PostForm("BLNumber")
|
||||
contract.DeliveryTime = c.PostForm("deliveryTime")
|
||||
contract.PickUpTime = c.PostForm("pickUpTime")
|
||||
contract.Packaging = c.PostForm("packaging")
|
||||
contract.Remarks = c.PostForm("remarks")
|
||||
|
||||
if countStr := c.PostForm("count"); countStr != "" {
|
||||
if v, err := strconv.ParseFloat(countStr, 64); err == nil {
|
||||
contract.Count = v
|
||||
}
|
||||
}
|
||||
if priceStr := c.PostForm("unitPrice"); priceStr != "" {
|
||||
if v, err := strconv.ParseFloat(priceStr, 64); err == nil {
|
||||
contract.UnitPrice = v
|
||||
}
|
||||
}
|
||||
|
||||
contract.CreateTime = time.Now()
|
||||
contract.UpdateTime = time.Now()
|
||||
|
||||
form, err := c.MultipartForm()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
files := form.File["files"]
|
||||
|
||||
if len(files) == 0 {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "no files"})
|
||||
return
|
||||
}
|
||||
path := "./data/contract/uploads"
|
||||
os.MkdirAll(path, os.ModePerm)
|
||||
|
||||
var fileModels []model.ContractFile
|
||||
|
||||
for _, file := range files {
|
||||
|
||||
filename := fmt.Sprintf("%d_%s", time.Now().UnixNano(), file.Filename)
|
||||
savePath := filepath.Join(path, filename)
|
||||
|
||||
if err := c.SaveUploadedFile(file, savePath); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
fileModels = append(fileModels, model.ContractFile{
|
||||
Name: file.Filename,
|
||||
URL: "uploads/" + filename,
|
||||
})
|
||||
}
|
||||
|
||||
if err := h.service.CreateContract(&contract, fileModels); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"message": "upload success",
|
||||
})
|
||||
}
|
||||
|
||||
func (h *ContractHandler) BatchUpload(c *gin.Context) {
|
||||
path := c.PostForm("path")
|
||||
if path == "" {
|
||||
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "path required"})
|
||||
return
|
||||
}
|
||||
|
||||
form, err := c.MultipartForm()
|
||||
if err != nil {
|
||||
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid form"})
|
||||
return
|
||||
}
|
||||
|
||||
files := form.File["files"]
|
||||
if len(files) == 0 {
|
||||
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "no files"})
|
||||
return
|
||||
}
|
||||
|
||||
saved, err := h.service.BatchSaveFiles(path, files)
|
||||
if err != nil {
|
||||
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"count": saved,
|
||||
})
|
||||
}
|
||||
|
||||
// ========== 批量归档文件夹 ==========
|
||||
|
||||
func (h *ContractHandler) ListFolders(c *gin.Context) {
|
||||
list, err := h.folderService.List()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "data": list})
|
||||
}
|
||||
|
||||
func (h *ContractHandler) CreateFolder(c *gin.Context) {
|
||||
var body struct {
|
||||
Name string `json:"name" binding:"required"`
|
||||
Description string `json:"description"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&body); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
f, err := h.folderService.Create(body.Name, body.Description)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "data": f})
|
||||
}
|
||||
|
||||
func (h *ContractHandler) GetFolder(c *gin.Context) {
|
||||
idStr := c.Param("id")
|
||||
id, err := strconv.ParseUint(idStr, 10, 32)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "invalid id"})
|
||||
return
|
||||
}
|
||||
f, err := h.folderService.GetByID(uint(id))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"success": false, "message": "not found"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "data": f})
|
||||
}
|
||||
|
||||
func (h *ContractHandler) UpdateFolder(c *gin.Context) {
|
||||
idStr := c.Param("id")
|
||||
id, err := strconv.ParseUint(idStr, 10, 32)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "invalid id"})
|
||||
return
|
||||
}
|
||||
var body struct {
|
||||
Name string `json:"name" binding:"required"`
|
||||
Description string `json:"description"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&body); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
if err := h.folderService.Update(uint(id), body.Name, body.Description); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "message": "updated"})
|
||||
}
|
||||
|
||||
func (h *ContractHandler) DeleteFolder(c *gin.Context) {
|
||||
idStr := c.Param("id")
|
||||
id, err := strconv.ParseUint(idStr, 10, 32)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "invalid id"})
|
||||
return
|
||||
}
|
||||
if err := h.folderService.Delete(uint(id)); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "message": "deleted"})
|
||||
}
|
||||
|
||||
// ========== 批量上传记录 ==========
|
||||
|
||||
func (h *ContractHandler) ListBatches(c *gin.Context) {
|
||||
var folderID *uint
|
||||
if idStr := c.Query("folderId"); idStr != "" {
|
||||
if id, err := strconv.ParseUint(idStr, 10, 32); err == nil {
|
||||
uid := uint(id)
|
||||
folderID = &uid
|
||||
}
|
||||
}
|
||||
list, err := h.batchService.List(folderID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "data": list})
|
||||
}
|
||||
|
||||
func (h *ContractHandler) CreateBatch(c *gin.Context) {
|
||||
folderIDStr := c.PostForm("folderId")
|
||||
batchName := c.PostForm("batchName")
|
||||
remark := c.PostForm("remark")
|
||||
if folderIDStr == "" || batchName == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "folderId and batchName required"})
|
||||
return
|
||||
}
|
||||
folderID, err := strconv.ParseUint(folderIDStr, 10, 32)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "invalid folderId"})
|
||||
return
|
||||
}
|
||||
form, err := c.MultipartForm()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
files := form.File["files"]
|
||||
if len(files) == 0 {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "no files"})
|
||||
return
|
||||
}
|
||||
path := "./data/contract/batch_archive"
|
||||
os.MkdirAll(path, os.ModePerm)
|
||||
var fileModels []model.ContractBatchFile
|
||||
for _, file := range files {
|
||||
filename := fmt.Sprintf("%d_%s", time.Now().UnixNano(), file.Filename)
|
||||
savePath := filepath.Join(path, filename)
|
||||
if err := c.SaveUploadedFile(file, savePath); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
fileModels = append(fileModels, model.ContractBatchFile{
|
||||
Name: file.Filename,
|
||||
URL: "batch_archive/" + filename,
|
||||
FileSize: file.Size,
|
||||
})
|
||||
}
|
||||
b, err := h.batchService.Create(uint(folderID), batchName, remark, fileModels)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "data": b, "message": "created"})
|
||||
}
|
||||
|
||||
func (h *ContractHandler) GetBatch(c *gin.Context) {
|
||||
idStr := c.Param("id")
|
||||
id, err := strconv.ParseUint(idStr, 10, 32)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "invalid id"})
|
||||
return
|
||||
}
|
||||
b, err := h.batchService.GetByID(uint(id))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"success": false, "message": "not found"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "data": b})
|
||||
}
|
||||
|
||||
func (h *ContractHandler) DeleteBatch(c *gin.Context) {
|
||||
idStr := c.Param("id")
|
||||
id, err := strconv.ParseUint(idStr, 10, 32)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "invalid id"})
|
||||
return
|
||||
}
|
||||
if err := h.batchService.Delete(uint(id)); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "message": "deleted"})
|
||||
}
|
||||
530
backend/internal/handler/delivery_handler.go
Executable file
530
backend/internal/handler/delivery_handler.go
Executable file
@@ -0,0 +1,530 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/rovina/zsp-backend/internal/model"
|
||||
"github.com/rovina/zsp-backend/internal/service"
|
||||
"github.com/rovina/zsp-backend/pkg/utils"
|
||||
)
|
||||
|
||||
type DeliveryHandler struct {
|
||||
svc service.DeliveryService
|
||||
}
|
||||
|
||||
func NewDeliveryHandler(svc service.DeliveryService) *DeliveryHandler {
|
||||
return &DeliveryHandler{svc: svc}
|
||||
}
|
||||
|
||||
// ========== 我要提货 /api/apply-delivery ==========
|
||||
func (h *DeliveryHandler) ListApply(c *gin.Context) {
|
||||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||
pageSize, _ := strconv.Atoi(c.DefaultQuery("pageSize", "10"))
|
||||
orderNumber := c.Query("orderNumber")
|
||||
blNumber := c.Query("blNumber")
|
||||
status := c.Query("status")
|
||||
list, total, err := h.svc.ListApply( page, pageSize, orderNumber, blNumber, status)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "data": gin.H{"items": list, "total": total, "page": page, "pageSize": pageSize}})
|
||||
}
|
||||
|
||||
func (h *DeliveryHandler) GetApply(c *gin.Context) {
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "invalid id"})
|
||||
return
|
||||
}
|
||||
e, err := h.svc.GetApplyByID( uint(id))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"success": false, "message": "not found"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "data": e})
|
||||
}
|
||||
|
||||
func (h *DeliveryHandler) CreateApply(c *gin.Context) {
|
||||
var body model.DeliveryApply
|
||||
if err := c.ShouldBindJSON(&body); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
body.OrderNumber = fmt.Sprintf("DA%d", time.Now().UnixNano()/1e6)
|
||||
body.Status = "pending"
|
||||
body.CreateTime = time.Now()
|
||||
body.UpdateTime = time.Now()
|
||||
if err := h.svc.CreateApply( &body); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "data": gin.H{"id": body.ID, "orderNumber": body.OrderNumber}})
|
||||
}
|
||||
|
||||
func (h *DeliveryHandler) UpdateApply(c *gin.Context) {
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "invalid id"})
|
||||
return
|
||||
}
|
||||
e, err := h.svc.GetApplyByID( uint(id))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"success": false, "message": "not found"})
|
||||
return
|
||||
}
|
||||
if err := c.ShouldBindJSON(e); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
e.UpdateTime = time.Now()
|
||||
if err := h.svc.UpdateApply( e); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "data": e})
|
||||
}
|
||||
|
||||
func (h *DeliveryHandler) CancelApply(c *gin.Context) {
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "invalid id"})
|
||||
return
|
||||
}
|
||||
e, err := h.svc.GetApplyByID( uint(id))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"success": false, "message": "not found"})
|
||||
return
|
||||
}
|
||||
e.Status = "cancelled"
|
||||
e.UpdateTime = time.Now()
|
||||
if err := h.svc.UpdateApply( e); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "data": e})
|
||||
}
|
||||
|
||||
func (h *DeliveryHandler) DeleteApply(c *gin.Context) {
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "invalid id"})
|
||||
return
|
||||
}
|
||||
if err := h.svc.DeleteApply( uint(id)); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "message": "deleted"})
|
||||
}
|
||||
|
||||
// ========== 提货明细 /api/delivery-details ==========
|
||||
func (h *DeliveryHandler) ListOutbound(c *gin.Context) {
|
||||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||
pageSize, _ := strconv.Atoi(c.DefaultQuery("pageSize", "10"))
|
||||
outboundNumber := c.Query("outboundNumber")
|
||||
blNumber := c.Query("blNumber")
|
||||
status := c.Query("outboundStatus")
|
||||
list, total, err := h.svc.ListOutbound( page, pageSize, outboundNumber, blNumber, status)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "data": gin.H{"items": list, "total": total, "page": page, "pageSize": pageSize}})
|
||||
}
|
||||
|
||||
func (h *DeliveryHandler) GetOutbound(c *gin.Context) {
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "invalid id"})
|
||||
return
|
||||
}
|
||||
e, err := h.svc.GetOutboundByID( uint(id))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"success": false, "message": "not found"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "data": e})
|
||||
}
|
||||
|
||||
func (h *DeliveryHandler) CreateOutbound(c *gin.Context) {
|
||||
var body model.DeliveryOutbound
|
||||
if err := c.ShouldBindJSON(&body); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
body.OutboundNumber = fmt.Sprintf("OB%d", time.Now().UnixNano()/1e6)
|
||||
body.OutboundStatus = "pending"
|
||||
body.ReceiptStatus = "pending"
|
||||
body.CreateTime = time.Now()
|
||||
body.UpdateTime = time.Now()
|
||||
if err := h.svc.CreateOutbound( &body); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "data": body})
|
||||
}
|
||||
|
||||
func (h *DeliveryHandler) UpdateOutbound(c *gin.Context) {
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "invalid id"})
|
||||
return
|
||||
}
|
||||
e, err := h.svc.GetOutboundByID( uint(id))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"success": false, "message": "not found"})
|
||||
return
|
||||
}
|
||||
if err := c.ShouldBindJSON(e); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
e.UpdateTime = time.Now()
|
||||
if err := h.svc.UpdateOutbound( e); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "data": e})
|
||||
}
|
||||
|
||||
func (h *DeliveryHandler) DeleteOutbound(c *gin.Context) {
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "invalid id"})
|
||||
return
|
||||
}
|
||||
if err := h.svc.DeleteOutbound( uint(id)); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "message": "deleted"})
|
||||
}
|
||||
|
||||
// ========== 库存 /api/inventory ==========
|
||||
func (h *DeliveryHandler) ListWarehouses(c *gin.Context) {
|
||||
list, err := h.svc.ListWarehouses()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "data": list})
|
||||
}
|
||||
|
||||
func (h *DeliveryHandler) CreateWarehouse(c *gin.Context) {
|
||||
var body model.Warehouse
|
||||
if err := c.ShouldBindJSON(&body); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
body.Status = "active"
|
||||
body.CreateTime = time.Now()
|
||||
body.UpdateTime = time.Now()
|
||||
if err := h.svc.CreateWarehouse( &body); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "data": body})
|
||||
}
|
||||
|
||||
func (h *DeliveryHandler) UpdateWarehouse(c *gin.Context) {
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "invalid id"})
|
||||
return
|
||||
}
|
||||
e, err := h.svc.GetWarehouseByID( uint(id))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"success": false, "message": "not found"})
|
||||
return
|
||||
}
|
||||
if err := c.ShouldBindJSON(e); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
e.UpdateTime = time.Now()
|
||||
if err := h.svc.UpdateWarehouse( e); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "data": e})
|
||||
}
|
||||
|
||||
func (h *DeliveryHandler) DeleteWarehouse(c *gin.Context) {
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "invalid id"})
|
||||
return
|
||||
}
|
||||
if err := h.svc.DeleteWarehouse( uint(id)); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "message": "deleted"})
|
||||
}
|
||||
|
||||
func (h *DeliveryHandler) ListInbound(c *gin.Context) {
|
||||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||
pageSize, _ := strconv.Atoi(c.DefaultQuery("pageSize", "10"))
|
||||
warehouse := c.Query("warehouse")
|
||||
commodity := c.Query("commodity")
|
||||
list, total, err := h.svc.ListInbound( page, pageSize, warehouse, commodity)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "data": gin.H{"items": list, "total": total, "page": page, "pageSize": pageSize}})
|
||||
}
|
||||
|
||||
func (h *DeliveryHandler) CreateInbound(c *gin.Context) {
|
||||
var body model.InventoryInbound
|
||||
if err := c.ShouldBindJSON(&body); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
body.InboundNumber = fmt.Sprintf("IN%d", time.Now().UnixNano()/1e6)
|
||||
body.CreateTime = time.Now()
|
||||
body.UpdateTime = time.Now()
|
||||
if err := h.svc.CreateInbound( &body); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "data": body})
|
||||
}
|
||||
|
||||
func (h *DeliveryHandler) UpdateInbound(c *gin.Context) {
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "invalid id"})
|
||||
return
|
||||
}
|
||||
e, err := h.svc.GetInboundByID( uint(id))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"success": false, "message": "not found"})
|
||||
return
|
||||
}
|
||||
if err := c.ShouldBindJSON(e); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
e.UpdateTime = time.Now()
|
||||
if err := h.svc.UpdateInbound( e); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "data": e})
|
||||
}
|
||||
|
||||
func (h *DeliveryHandler) DeleteInbound(c *gin.Context) {
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "invalid id"})
|
||||
return
|
||||
}
|
||||
if err := h.svc.DeleteInbound( uint(id)); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "message": "deleted"})
|
||||
}
|
||||
|
||||
// InventorySummary 库存汇总:入库 - 出库 - 货权转移 = 库存
|
||||
func (h *DeliveryHandler) InventorySummary(c *gin.Context) {
|
||||
warehouse := c.Query("warehouse")
|
||||
commodity := c.Query("commodity")
|
||||
|
||||
type row struct {
|
||||
Warehouse string `json:"warehouse"`
|
||||
Commodity string `json:"commodity"`
|
||||
TotalInbound float64 `json:"totalInbound"`
|
||||
TotalOutbound float64 `json:"totalOutbound"`
|
||||
TotalTransfer float64 `json:"totalTransfer"`
|
||||
CurrentStock float64 `json:"currentStock"`
|
||||
Unit string `json:"unit"`
|
||||
}
|
||||
|
||||
var inSum []struct {
|
||||
Warehouse string
|
||||
Commodity string
|
||||
Sum float64
|
||||
}
|
||||
qIn := h.svc.DB().Model(&model.InventoryInbound{}).Select("warehouse, commodity, SUM(inbound_weight) as sum").Group("warehouse, commodity")
|
||||
if warehouse != "" {
|
||||
qIn = qIn.Where("warehouse = ?", warehouse)
|
||||
}
|
||||
if commodity != "" {
|
||||
qIn = qIn.Where("commodity LIKE ?", "%"+commodity+"%")
|
||||
}
|
||||
qIn.Scan(&inSum)
|
||||
|
||||
var outSum []struct {
|
||||
Warehouse string
|
||||
Commodity string
|
||||
Sum float64
|
||||
}
|
||||
qOut := h.svc.DB().Model(&model.DeliveryOutbound{}).Select("warehouse, commodity, SUM(quantity) as sum").Group("warehouse, commodity")
|
||||
if warehouse != "" {
|
||||
qOut = qOut.Where("warehouse = ?", warehouse)
|
||||
}
|
||||
if commodity != "" {
|
||||
qOut = qOut.Where("commodity LIKE ?", "%"+commodity+"%")
|
||||
}
|
||||
qOut.Scan(&outSum)
|
||||
|
||||
var trSum []struct {
|
||||
Warehouse string
|
||||
Commodity string
|
||||
Sum float64
|
||||
}
|
||||
qTr := h.svc.DB().Model(&model.OwnershipTransfer{}).
|
||||
Select("ownership_transfers.warehouse, ownership_transfers.commodity, COALESCE((SELECT SUM(quantity) FROM ownership_transfer_bls WHERE transfer_id = ownership_transfers.id), 0) as sum").
|
||||
Group("ownership_transfers.warehouse, ownership_transfers.commodity")
|
||||
if warehouse != "" {
|
||||
qTr = qTr.Where("warehouse = ?", warehouse)
|
||||
}
|
||||
if commodity != "" {
|
||||
qTr = qTr.Where("commodity LIKE ?", "%"+commodity+"%")
|
||||
}
|
||||
qTr.Scan(&trSum)
|
||||
|
||||
m := make(map[string]*row)
|
||||
for _, r := range inSum {
|
||||
key := r.Warehouse + "|" + r.Commodity
|
||||
m[key] = &row{Warehouse: r.Warehouse, Commodity: r.Commodity, TotalInbound: r.Sum, Unit: "吨"}
|
||||
}
|
||||
for _, r := range outSum {
|
||||
key := r.Warehouse + "|" + r.Commodity
|
||||
if m[key] == nil {
|
||||
m[key] = &row{Warehouse: r.Warehouse, Commodity: r.Commodity, Unit: "吨"}
|
||||
}
|
||||
m[key].TotalOutbound = r.Sum
|
||||
}
|
||||
for _, r := range trSum {
|
||||
key := r.Warehouse + "|" + r.Commodity
|
||||
if m[key] == nil {
|
||||
m[key] = &row{Warehouse: r.Warehouse, Commodity: r.Commodity, Unit: "吨"}
|
||||
}
|
||||
m[key].TotalTransfer = r.Sum
|
||||
}
|
||||
var result []row
|
||||
for _, v := range m {
|
||||
v.CurrentStock = v.TotalInbound - v.TotalOutbound - v.TotalTransfer
|
||||
result = append(result, *v)
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "data": result})
|
||||
}
|
||||
|
||||
// ========== 货权转移 /api/ownership-transfer ==========
|
||||
func (h *DeliveryHandler) ListOwnership(c *gin.Context) {
|
||||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||
pageSize, _ := strconv.Atoi(c.DefaultQuery("pageSize", "10"))
|
||||
transferNumber := c.Query("transferNumber")
|
||||
blNumber := c.Query("blNumber")
|
||||
status := c.Query("status")
|
||||
list, total, err := h.svc.ListOwnership( page, pageSize, transferNumber, blNumber, status)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "data": gin.H{"items": list, "total": total}})
|
||||
}
|
||||
|
||||
func (h *DeliveryHandler) GetOwnership(c *gin.Context) {
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "invalid id"})
|
||||
return
|
||||
}
|
||||
e, err := h.svc.GetOwnershipByID( uint(id))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"success": false, "message": "not found"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "data": e})
|
||||
}
|
||||
|
||||
func (h *DeliveryHandler) CreateOwnership(c *gin.Context) {
|
||||
var body model.OwnershipTransfer
|
||||
if err := c.ShouldBindJSON(&body); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
body.TransferNumber = fmt.Sprintf("OT%d", time.Now().UnixNano()/1e6)
|
||||
body.Status = "draft"
|
||||
body.CreateTime = time.Now()
|
||||
body.UpdateTime = time.Now()
|
||||
if err := h.svc.CreateOwnership( &body); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "data": body})
|
||||
}
|
||||
|
||||
func (h *DeliveryHandler) CreateOwnershipWithFiles(c *gin.Context) {
|
||||
transferDate := c.PostForm("transferDate")
|
||||
commodity := c.PostForm("commodity")
|
||||
warehouse := c.PostForm("warehouse")
|
||||
transferor := c.PostForm("transferor")
|
||||
transferee := c.PostForm("transferee")
|
||||
remark := c.PostForm("remark")
|
||||
|
||||
// Parse dynamic BL items from JSON string field "blItems"
|
||||
var blItems []model.OwnershipTransferBL
|
||||
if blItemsJSON := c.PostForm("blItems"); blItemsJSON != "" {
|
||||
json.Unmarshal([]byte(blItemsJSON), &blItems)
|
||||
}
|
||||
|
||||
body := model.OwnershipTransfer{
|
||||
TransferNumber: fmt.Sprintf("OT%d", time.Now().UnixNano()/1e6),
|
||||
TransferDate: transferDate,
|
||||
Commodity: commodity,
|
||||
Warehouse: warehouse,
|
||||
Transferor: transferor,
|
||||
Transferee: transferee,
|
||||
Remark: remark,
|
||||
BLItems: blItems,
|
||||
Status: "draft",
|
||||
CreateTime: time.Now(),
|
||||
UpdateTime: time.Now(),
|
||||
}
|
||||
if err := h.svc.CreateOwnership( &body); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
form, _ := c.MultipartForm()
|
||||
files := form.File["files"]
|
||||
basePath := filepath.Join(".", "data", "ownership")
|
||||
if len(files) > 0 {
|
||||
_ = os.MkdirAll(basePath, os.ModePerm)
|
||||
for _, fh := range files {
|
||||
filename := fmt.Sprintf("%d_%s", time.Now().UnixNano(), fh.Filename)
|
||||
savePath := filepath.Join(basePath, filename)
|
||||
if err := utils.SaveUploadedFile(fh, savePath); err != nil {
|
||||
continue
|
||||
}
|
||||
fileRec := model.OwnershipTransferFile{TransferID: body.ID, Name: fh.Filename, URL: "ownership/" + filename}
|
||||
_ = h.svc.CreateOwnershipFiles( []model.OwnershipTransferFile{fileRec})
|
||||
}
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "data": body})
|
||||
}
|
||||
|
||||
func (h *DeliveryHandler) DeleteOwnership(c *gin.Context) {
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "invalid id"})
|
||||
return
|
||||
}
|
||||
if err := h.svc.DeleteOwnership( uint(id)); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "message": "deleted"})
|
||||
}
|
||||
76
backend/internal/handler/user_handler.go
Executable file
76
backend/internal/handler/user_handler.go
Executable file
@@ -0,0 +1,76 @@
|
||||
// internal/handler/user_handler.go
|
||||
package handler
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/rovina/zsp-backend/internal/dto"
|
||||
"github.com/rovina/zsp-backend/internal/service"
|
||||
)
|
||||
|
||||
type UserHandler struct {
|
||||
userService service.UserService
|
||||
}
|
||||
|
||||
func NewUserHandler(userService service.UserService) *UserHandler {
|
||||
return &UserHandler{userService: userService}
|
||||
}
|
||||
|
||||
func (h *UserHandler) Register(c *gin.Context) {
|
||||
var req dto.CreateUserRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("bind json error: %s", err.Error())})
|
||||
return
|
||||
}
|
||||
user, err := h.userService.Register(&req)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusCreated, gin.H{
|
||||
"message": "User registered successfully",
|
||||
"user": user,
|
||||
})
|
||||
}
|
||||
|
||||
func (h *UserHandler) Login(c *gin.Context) {
|
||||
var req dto.LoginRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
user, token, err := h.userService.Login(&req)
|
||||
fmt.Println(user)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"message": "Login successful",
|
||||
"user": user,
|
||||
"token": token,
|
||||
})
|
||||
}
|
||||
|
||||
func (h *UserHandler) GetUser(c *gin.Context) {
|
||||
idStr := c.Param("id")
|
||||
id, err := strconv.ParseUint(idStr, 10, 32)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid user ID"})
|
||||
return
|
||||
}
|
||||
|
||||
user, err := h.userService.GetUserByID(uint(id))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "User not found"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"user": user})
|
||||
}
|
||||
539
backend/internal/handler/zsp_contract_handler.go
Executable file
539
backend/internal/handler/zsp_contract_handler.go
Executable file
@@ -0,0 +1,539 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/rovina/zsp-backend/internal/dto"
|
||||
"github.com/rovina/zsp-backend/internal/service"
|
||||
"github.com/xuri/excelize/v2"
|
||||
)
|
||||
|
||||
type ZSPContractHandler struct {
|
||||
service service.ZSPContractService
|
||||
}
|
||||
|
||||
func NewZSPContractHandler(service service.ZSPContractService) *ZSPContractHandler {
|
||||
return &ZSPContractHandler{service: service}
|
||||
}
|
||||
|
||||
// zspContracts.POST("/folders", zspContractHandler.CreateFolder)
|
||||
// zspContracts.GET("/folders", zspContractHandler.GetFolders)
|
||||
// zspContracts.GET("/folders/:id", zspContractHandler.GetFolder)
|
||||
// zspContracts.DELETE("/folders/:id", zspContractHandler.DeleteFolder)
|
||||
// zspContracts.POST("/files", zspContractHandler.UploadFile)
|
||||
// zspContracts.GET("/files/:id", zspContractHandler.GetFile)
|
||||
// zspContracts.GET("/files", zspContractHandler.ListFiles)
|
||||
// zspContracts.DELETE("/files/:id", zspContractHandler.DeleteFile)
|
||||
|
||||
func (h *ZSPContractHandler) CreateFolder(c *gin.Context) {
|
||||
var req dto.ZSPFolderCreateRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(400, gin.H{"success": false, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
err := h.service.CreateFolder(&req)
|
||||
if err != nil {
|
||||
c.JSON(500, gin.H{"success": false, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(200, gin.H{"success": true, "message": "folder created"})
|
||||
}
|
||||
|
||||
func (h *ZSPContractHandler) GetFolders(c *gin.Context) {
|
||||
folders, err := h.service.GetFolders()
|
||||
if err != nil {
|
||||
c.JSON(500, gin.H{"success": false, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(200, gin.H{"success": true, "message": "folders retrieved", "data": folders})
|
||||
}
|
||||
|
||||
func (h *ZSPContractHandler) GetFolder(c *gin.Context) {
|
||||
folderID := c.Param("id")
|
||||
// 这里可以调用 service 获取单个文件夹的详情
|
||||
folder, err := h.service.GetFolderById(folderID)
|
||||
if err != nil {
|
||||
c.JSON(500, gin.H{"success": false, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(200, gin.H{"success": true, "message": "folder retrieved", "data": folder})
|
||||
}
|
||||
|
||||
func (h *ZSPContractHandler) DeleteFolder(c *gin.Context) {
|
||||
folderID := c.Param("id")
|
||||
err := h.service.DeleteFolderById(folderID)
|
||||
if err != nil {
|
||||
c.JSON(500, gin.H{"success": false, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(200, gin.H{"success": true, "message": "folder deleted"})
|
||||
}
|
||||
|
||||
func (h *ZSPContractHandler) UpdateFolder(c *gin.Context) {
|
||||
folderID := c.Param("id")
|
||||
var req dto.ZSPFolderUpdateRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(400, gin.H{"success": false, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
// 这里可以调用 service 更新文件夹的信息
|
||||
err := h.service.UpdateFolderById(folderID, &req)
|
||||
if err != nil {
|
||||
c.JSON(500, gin.H{"success": false, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(200, gin.H{"success": true, "message": "folder updated"})
|
||||
}
|
||||
|
||||
func (h *ZSPContractHandler) UploadContract(c *gin.Context) {
|
||||
var req dto.ZSPContractCreateRequest
|
||||
// FormData 方式上传文件,参数在 form 中
|
||||
req.Commodity = c.PostForm("commodity")
|
||||
req.SignDate = c.PostForm("signDate")
|
||||
req.ContractType = c.PostForm("contractType")
|
||||
folderIDStr := c.PostForm("folderId")
|
||||
if folderIDStr == "" {
|
||||
c.JSON(400, gin.H{"success": false, "message": "请选择合同文件夹"})
|
||||
return
|
||||
}
|
||||
folderID, err := strconv.ParseUint(folderIDStr, 10, 64)
|
||||
if err != nil {
|
||||
c.JSON(400, gin.H{"success": false, "message": "invalid folderId"})
|
||||
return
|
||||
}
|
||||
req.FolderID = uint(folderID)
|
||||
req.ContractNumber = c.PostForm("contractNumber")
|
||||
req.CompanyName = c.PostForm("companyName")
|
||||
|
||||
file, err := c.FormFile("file")
|
||||
|
||||
if err != nil {
|
||||
c.JSON(400, gin.H{"success": false, "message": "file is required"})
|
||||
return
|
||||
}
|
||||
err = h.service.UploadContract(&req, file)
|
||||
if err != nil {
|
||||
c.JSON(500, gin.H{"success": false, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(200, gin.H{"success": true, "message": "contract uploaded"})
|
||||
|
||||
}
|
||||
|
||||
func (h *ZSPContractHandler) GetContract(c *gin.Context) {
|
||||
contractID := c.Param("id")
|
||||
// 这里可以调用 service 获取单个合同的详情
|
||||
contract, err := h.service.GetContractById(contractID)
|
||||
if err != nil {
|
||||
c.JSON(500, gin.H{"success": false, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(200, gin.H{"success": true, "message": "contract retrieved", "data": contract})
|
||||
}
|
||||
|
||||
func (h *ZSPContractHandler) ListContracts(c *gin.Context) {
|
||||
folderID := c.Query("folderId")
|
||||
contracts, err := h.service.ListContracts(folderID)
|
||||
if err != nil {
|
||||
c.JSON(500, gin.H{"success": false, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(200, gin.H{"success": true, "message": "contracts retrieved", "data": contracts})
|
||||
}
|
||||
|
||||
func (h *ZSPContractHandler) DeleteContract(c *gin.Context) {
|
||||
contractID := c.Param("id")
|
||||
err := h.service.DeleteContractById(contractID)
|
||||
if err != nil {
|
||||
c.JSON(500, gin.H{"success": false, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(200, gin.H{"success": true, "message": "contract deleted"})
|
||||
}
|
||||
|
||||
func (h *ZSPContractHandler) UpdateContract(c *gin.Context) {
|
||||
contractID := c.Param("id")
|
||||
var req dto.ZSPContractUpdateRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(400, gin.H{"success": false, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
err := h.service.UpdateContractById(contractID, &req)
|
||||
if err != nil {
|
||||
c.JSON(500, gin.H{"success": false, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(200, gin.H{"success": true, "message": "contract updated"})
|
||||
}
|
||||
|
||||
// ============ 合同详情相关Handler ============
|
||||
|
||||
// GetContractWithDetails 获取合同及其详情列表
|
||||
func (h *ZSPContractHandler) GetContractWithDetails(c *gin.Context) {
|
||||
contractID := c.Param("id")
|
||||
contract, err := h.service.GetContractWithDetails(contractID)
|
||||
if err != nil {
|
||||
c.JSON(500, gin.H{"success": false, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(200, gin.H{"success": true, "message": "contract with details retrieved", "data": contract})
|
||||
}
|
||||
|
||||
// ListContractsWithDetails 获取合同列表(包含详情)
|
||||
func (h *ZSPContractHandler) ListContractsWithDetails(c *gin.Context) {
|
||||
folderID := c.Query("folderId")
|
||||
contracts, err := h.service.ListContractsWithDetails(folderID)
|
||||
if err != nil {
|
||||
c.JSON(500, gin.H{"success": false, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(200, gin.H{"success": true, "message": "contracts with details retrieved", "data": contracts})
|
||||
}
|
||||
|
||||
// CreateContractDetail 创建合同详情
|
||||
func (h *ZSPContractHandler) CreateContractDetail(c *gin.Context) {
|
||||
var req dto.ZSPContractDetailCreateRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(400, gin.H{"success": false, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
err := h.service.CreateContractDetail(&req)
|
||||
if err != nil {
|
||||
c.JSON(500, gin.H{"success": false, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(200, gin.H{"success": true, "message": "contract detail created"})
|
||||
}
|
||||
|
||||
// UpdateContractDetail 更新合同详情
|
||||
func (h *ZSPContractHandler) UpdateContractDetail(c *gin.Context) {
|
||||
var req dto.ZSPContractDetailUpdateRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(400, gin.H{"success": false, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
err := h.service.UpdateContractDetail(&req)
|
||||
if err != nil {
|
||||
c.JSON(500, gin.H{"success": false, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(200, gin.H{"success": true, "message": "contract detail updated"})
|
||||
}
|
||||
|
||||
// GetContractDetail 获取合同详情
|
||||
func (h *ZSPContractHandler) GetContractDetail(c *gin.Context) {
|
||||
detailID := c.Param("id")
|
||||
detail, err := h.service.GetContractDetailById(detailID)
|
||||
if err != nil {
|
||||
c.JSON(500, gin.H{"success": false, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(200, gin.H{"success": true, "message": "contract detail retrieved", "data": detail})
|
||||
}
|
||||
|
||||
// ListContractDetails 获取合同详情列表
|
||||
func (h *ZSPContractHandler) ListContractDetails(c *gin.Context) {
|
||||
contractFileID := c.Param("contractFileId")
|
||||
details, err := h.service.ListContractDetails(contractFileID)
|
||||
if err != nil {
|
||||
c.JSON(500, gin.H{"success": false, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(200, gin.H{"success": true, "message": "contract details retrieved", "data": details})
|
||||
}
|
||||
|
||||
// DeleteContractDetail 删除合同详情
|
||||
func (h *ZSPContractHandler) DeleteContractDetail(c *gin.Context) {
|
||||
detailID := c.Param("id")
|
||||
err := h.service.DeleteContractDetailById(detailID)
|
||||
if err != nil {
|
||||
c.JSON(500, gin.H{"success": false, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(200, gin.H{"success": true, "message": "contract detail deleted"})
|
||||
}
|
||||
|
||||
// BatchCreateContractDetails 批量创建合同详情
|
||||
func (h *ZSPContractHandler) BatchCreateContractDetails(c *gin.Context) {
|
||||
var req dto.ZSPContractDetailBatchCreateRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(400, gin.H{"success": false, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
err := h.service.BatchCreateContractDetails(&req)
|
||||
if err != nil {
|
||||
c.JSON(500, gin.H{"success": false, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(200, gin.H{"success": true, "message": fmt.Sprintf("%d contract details created", len(req.Details))})
|
||||
}
|
||||
|
||||
// ImportContractDetailsFromExcel 从Excel导入合同详情
|
||||
func (h *ZSPContractHandler) ImportContractDetailsFromExcel(c *gin.Context) {
|
||||
contractFileIDStr := c.PostForm("contractFileId")
|
||||
if contractFileIDStr == "" {
|
||||
c.JSON(400, gin.H{"success": false, "message": "contractFileId is required"})
|
||||
return
|
||||
}
|
||||
|
||||
contractFileID, err := strconv.ParseUint(contractFileIDStr, 10, 64)
|
||||
if err != nil {
|
||||
c.JSON(400, gin.H{"success": false, "message": "invalid contractFileId"})
|
||||
return
|
||||
}
|
||||
|
||||
file, err := c.FormFile("file")
|
||||
if err != nil {
|
||||
c.JSON(400, gin.H{"success": false, "message": "file is required"})
|
||||
return
|
||||
}
|
||||
|
||||
// 打开上传的Excel文件
|
||||
src, err := file.Open()
|
||||
if err != nil {
|
||||
c.JSON(500, gin.H{"success": false, "message": fmt.Sprintf("failed to open file: %v", err)})
|
||||
return
|
||||
}
|
||||
defer src.Close()
|
||||
|
||||
// 使用excelize读取Excel文件
|
||||
f, err := excelize.OpenReader(src)
|
||||
if err != nil {
|
||||
c.JSON(500, gin.H{"success": false, "message": fmt.Sprintf("failed to read Excel file: %v", err)})
|
||||
return
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
// 获取第一个sheet的名称
|
||||
sheets := f.GetSheetList()
|
||||
if len(sheets) == 0 {
|
||||
c.JSON(400, gin.H{"success": false, "message": "Excel file is empty"})
|
||||
return
|
||||
}
|
||||
sheetName := sheets[0]
|
||||
|
||||
// 读取所有行数据
|
||||
rows, err := f.GetRows(sheetName)
|
||||
if err != nil {
|
||||
c.JSON(500, gin.H{"success": false, "message": fmt.Sprintf("failed to read rows: %v", err)})
|
||||
return
|
||||
}
|
||||
|
||||
// 检查是否有数据
|
||||
if len(rows) <= 1 {
|
||||
c.JSON(400, gin.H{"success": false, "message": "Excel file has no data rows"})
|
||||
return
|
||||
}
|
||||
|
||||
// 解析数据(假设第一行是表头)
|
||||
var details []dto.ZSPContractDetailCreateRequest
|
||||
for i, row := range rows {
|
||||
// 跳过表头
|
||||
if i == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
// 确保至少有基本列
|
||||
if len(row) < 6 {
|
||||
continue
|
||||
}
|
||||
|
||||
detail := dto.ZSPContractDetailCreateRequest{
|
||||
ContractFileID: uint(contractFileID),
|
||||
}
|
||||
|
||||
// 商品名称 (第1列)
|
||||
detail.CommodityName = strings.TrimSpace(row[0])
|
||||
|
||||
// 单位 (第2列)
|
||||
detail.Unit = strings.TrimSpace(row[1])
|
||||
if detail.Unit == "" {
|
||||
detail.Unit = "吨"
|
||||
}
|
||||
|
||||
// 总量 (第3列)
|
||||
if row[2] != "" {
|
||||
detail.TotalQuantity, err = strconv.ParseFloat(strings.TrimSpace(row[2]), 64)
|
||||
if err != nil {
|
||||
c.JSON(400, gin.H{"success": false, "message": fmt.Sprintf("invalid total quantity at row %d: %v", i+1, err)})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 单价 (第4列)
|
||||
if row[3] != "" {
|
||||
detail.UnitPrice, err = strconv.ParseFloat(strings.TrimSpace(row[3]), 64)
|
||||
if err != nil {
|
||||
c.JSON(400, gin.H{"success": false, "message": fmt.Sprintf("invalid unit price at row %d: %v", i+1, err)})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 已提货数量 (第5列)
|
||||
if row[4] != "" {
|
||||
detail.DeliveredQty, err = strconv.ParseFloat(strings.TrimSpace(row[4]), 64)
|
||||
if err != nil {
|
||||
c.JSON(400, gin.H{"success": false, "message": fmt.Sprintf("invalid delivered quantity at row %d: %v", i+1, err)})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 提单号 (第6列)
|
||||
if len(row) > 5 {
|
||||
detail.BillOfLading = strings.TrimSpace(row[5])
|
||||
}
|
||||
|
||||
// 备注 (第7列)
|
||||
if len(row) > 6 {
|
||||
detail.Remark = strings.TrimSpace(row[6])
|
||||
}
|
||||
|
||||
// 验证必填字段
|
||||
if detail.CommodityName == "" {
|
||||
c.JSON(400, gin.H{"success": false, "message": fmt.Sprintf("commodity name is required at row %d", i+1)})
|
||||
return
|
||||
}
|
||||
if detail.TotalQuantity == 0 {
|
||||
c.JSON(400, gin.H{"success": false, "message": fmt.Sprintf("total quantity is required at row %d", i+1)})
|
||||
return
|
||||
}
|
||||
if detail.UnitPrice == 0 {
|
||||
c.JSON(400, gin.H{"success": false, "message": fmt.Sprintf("unit price is required at row %d", i+1)})
|
||||
return
|
||||
}
|
||||
|
||||
details = append(details, detail)
|
||||
}
|
||||
|
||||
// 批量创建合同详情
|
||||
if len(details) == 0 {
|
||||
c.JSON(400, gin.H{"success": false, "message": "no valid data found in Excel file"})
|
||||
return
|
||||
}
|
||||
|
||||
batchReq := &dto.ZSPContractDetailBatchCreateRequest{
|
||||
ContractFileID: uint(contractFileID),
|
||||
Details: details,
|
||||
}
|
||||
|
||||
err = h.service.BatchCreateContractDetails(batchReq)
|
||||
if err != nil {
|
||||
c.JSON(500, gin.H{"success": false, "message": fmt.Sprintf("failed to create contract details: %v", err)})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(200, gin.H{"success": true, "message": fmt.Sprintf("successfully imported %d contract details", len(details))})
|
||||
}
|
||||
|
||||
// ExportContractDetailsToExcel 导出合同详情到Excel
|
||||
func (h *ZSPContractHandler) ExportContractDetailsToExcel(c *gin.Context) {
|
||||
contractFileID := c.Param("id")
|
||||
details, err := h.service.ListContractDetails(contractFileID)
|
||||
if err != nil {
|
||||
c.JSON(500, gin.H{"success": false, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// 创建新的Excel文件
|
||||
f := excelize.NewFile()
|
||||
sheetName := "Sheet1"
|
||||
|
||||
// 设置表头
|
||||
headers := []string{"商品名称", "单位", "总量", "单价", "已提货数量", "应提货数量", "提单号", "备注"}
|
||||
for i, header := range headers {
|
||||
cell, _ := excelize.CoordinatesToCellName(i+1, 1)
|
||||
f.SetCellValue(sheetName, cell, header)
|
||||
|
||||
// 设置表头样式(加粗、背景色)
|
||||
style, _ := f.NewStyle(&excelize.Style{
|
||||
Font: &excelize.Font{
|
||||
Bold: true,
|
||||
},
|
||||
Fill: excelize.Fill{
|
||||
Type: "pattern",
|
||||
Color: []string{"#E0E0E0"},
|
||||
Pattern: 1,
|
||||
},
|
||||
Alignment: &excelize.Alignment{
|
||||
Horizontal: "center",
|
||||
Vertical: "center",
|
||||
},
|
||||
})
|
||||
f.SetCellStyle(sheetName, cell, cell, style)
|
||||
}
|
||||
|
||||
// 填充数据
|
||||
for i, detail := range details {
|
||||
row := i + 2
|
||||
|
||||
// 商品名称
|
||||
cell, _ := excelize.CoordinatesToCellName(1, row)
|
||||
f.SetCellValue(sheetName, cell, detail.CommodityName)
|
||||
|
||||
// 单位
|
||||
cell, _ = excelize.CoordinatesToCellName(2, row)
|
||||
f.SetCellValue(sheetName, cell, detail.Unit)
|
||||
|
||||
// 总量
|
||||
cell, _ = excelize.CoordinatesToCellName(3, row)
|
||||
f.SetCellValue(sheetName, cell, detail.TotalQuantity)
|
||||
|
||||
// 单价
|
||||
cell, _ = excelize.CoordinatesToCellName(4, row)
|
||||
f.SetCellValue(sheetName, cell, detail.UnitPrice)
|
||||
|
||||
// 已提货数量
|
||||
cell, _ = excelize.CoordinatesToCellName(5, row)
|
||||
f.SetCellValue(sheetName, cell, detail.DeliveredQty)
|
||||
|
||||
// 应提货数量
|
||||
cell, _ = excelize.CoordinatesToCellName(6, row)
|
||||
f.SetCellValue(sheetName, cell, detail.PendingQty)
|
||||
|
||||
// 提单号
|
||||
cell, _ = excelize.CoordinatesToCellName(7, row)
|
||||
f.SetCellValue(sheetName, cell, detail.BillOfLading)
|
||||
|
||||
// 备注
|
||||
cell, _ = excelize.CoordinatesToCellName(8, row)
|
||||
f.SetCellValue(sheetName, cell, detail.Remark)
|
||||
}
|
||||
|
||||
// 设置列宽
|
||||
f.SetColWidth(sheetName, "A", "A", 20) // 商品名称
|
||||
f.SetColWidth(sheetName, "B", "B", 10) // 单位
|
||||
f.SetColWidth(sheetName, "C", "C", 12) // 总量
|
||||
f.SetColWidth(sheetName, "D", "D", 12) // 单价
|
||||
f.SetColWidth(sheetName, "E", "E", 12) // 已提货数量
|
||||
f.SetColWidth(sheetName, "F", "F", 12) // 应提货数量
|
||||
f.SetColWidth(sheetName, "G", "G", 20) // 提单号
|
||||
f.SetColWidth(sheetName, "H", "H", 30) // 备注
|
||||
|
||||
// 使用buffer保存Excel数据
|
||||
var buf bytes.Buffer
|
||||
err = f.Write(&buf)
|
||||
if err != nil {
|
||||
c.JSON(500, gin.H{"success": false, "message": fmt.Sprintf("failed to write Excel buffer: %v", err)})
|
||||
return
|
||||
}
|
||||
|
||||
// 关闭文件
|
||||
f.Close()
|
||||
|
||||
// 设置响应头
|
||||
c.Header("Content-Type", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
|
||||
c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=contract_details_%s.xlsx", contractFileID))
|
||||
|
||||
// 返回Excel文件
|
||||
c.Data(200, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", buf.Bytes())
|
||||
}
|
||||
318
backend/internal/handler/zsp_finances_handler.go
Executable file
318
backend/internal/handler/zsp_finances_handler.go
Executable file
@@ -0,0 +1,318 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/rovina/zsp-backend/internal/dto"
|
||||
"github.com/rovina/zsp-backend/internal/service"
|
||||
)
|
||||
|
||||
type ZSPFinancesHandler struct {
|
||||
service service.ZSPFinancesService
|
||||
}
|
||||
|
||||
func NewZSPFinancesHandler(service service.ZSPFinancesService) *ZSPFinancesHandler {
|
||||
return &ZSPFinancesHandler{service: service}
|
||||
}
|
||||
|
||||
// ============ 货代费用 ============
|
||||
func (h *ZSPFinancesHandler) CreateFreightCharge(c *gin.Context) {
|
||||
var req dto.ZSPFreightChargesCreateRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(400, gin.H{"success": false, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
err := h.service.CreateFreightCharge(&req)
|
||||
if err != nil {
|
||||
c.JSON(500, gin.H{"success": false, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(200, gin.H{"success": true, "message": "freight charge created"})
|
||||
}
|
||||
|
||||
func (h *ZSPFinancesHandler) GetFreightCharge(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
charge, err := h.service.GetFreightCharge(id)
|
||||
if err != nil {
|
||||
c.JSON(500, gin.H{"success": false, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(200, gin.H{"success": true, "message": "freight charge retrieved", "data": charge})
|
||||
}
|
||||
|
||||
func (h *ZSPFinancesHandler) ListFreightCharges(c *gin.Context) {
|
||||
charges, err := h.service.ListFreightCharges()
|
||||
if err != nil {
|
||||
c.JSON(500, gin.H{"success": false, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(200, gin.H{"success": true, "message": "freight charges retrieved", "data": charges})
|
||||
}
|
||||
|
||||
func (h *ZSPFinancesHandler) UpdateFreightCharge(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
var req dto.ZSPFreightChargesUpdateRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(400, gin.H{"success": false, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
err := h.service.UpdateFreightCharge(id, &req)
|
||||
if err != nil {
|
||||
c.JSON(500, gin.H{"success": false, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(200, gin.H{"success": true, "message": "freight charge updated"})
|
||||
}
|
||||
|
||||
func (h *ZSPFinancesHandler) DeleteFreightCharge(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
err := h.service.DeleteFreightCharge(id)
|
||||
if err != nil {
|
||||
c.JSON(500, gin.H{"success": false, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(200, gin.H{"success": true, "message": "freight charge deleted"})
|
||||
}
|
||||
|
||||
// ============ 杂费 ============
|
||||
func (h *ZSPFinancesHandler) CreateMiscCharge(c *gin.Context) {
|
||||
var req dto.ZSPMiscChargesCreateRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(400, gin.H{"success": false, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
err := h.service.CreateMiscCharge(&req)
|
||||
if err != nil {
|
||||
c.JSON(500, gin.H{"success": false, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(200, gin.H{"success": true, "message": "misc charge created"})
|
||||
}
|
||||
|
||||
func (h *ZSPFinancesHandler) GetMiscCharge(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
charge, err := h.service.GetMiscCharge(id)
|
||||
if err != nil {
|
||||
c.JSON(500, gin.H{"success": false, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(200, gin.H{"success": true, "message": "misc charge retrieved", "data": charge})
|
||||
}
|
||||
|
||||
func (h *ZSPFinancesHandler) ListMiscCharges(c *gin.Context) {
|
||||
charges, err := h.service.ListMiscCharges()
|
||||
if err != nil {
|
||||
c.JSON(500, gin.H{"success": false, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(200, gin.H{"success": true, "message": "misc charges retrieved", "data": charges})
|
||||
}
|
||||
|
||||
func (h *ZSPFinancesHandler) UpdateMiscCharge(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
var req dto.ZSPMiscChargesUpdateRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(400, gin.H{"success": false, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
err := h.service.UpdateMiscCharge(id, &req)
|
||||
if err != nil {
|
||||
c.JSON(500, gin.H{"success": false, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(200, gin.H{"success": true, "message": "misc charge updated"})
|
||||
}
|
||||
|
||||
func (h *ZSPFinancesHandler) DeleteMiscCharge(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
err := h.service.DeleteMiscCharge(id)
|
||||
if err != nil {
|
||||
c.JSON(500, gin.H{"success": false, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(200, gin.H{"success": true, "message": "misc charge deleted"})
|
||||
}
|
||||
|
||||
// ============ 服务费 ============
|
||||
func (h *ZSPFinancesHandler) CreateServiceFee(c *gin.Context) {
|
||||
var req dto.ZSPServiceFeesCreateRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(400, gin.H{"success": false, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
err := h.service.CreateServiceFee(&req)
|
||||
if err != nil {
|
||||
c.JSON(500, gin.H{"success": false, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(200, gin.H{"success": true, "message": "service fee created"})
|
||||
}
|
||||
|
||||
func (h *ZSPFinancesHandler) GetServiceFee(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
fee, err := h.service.GetServiceFee(id)
|
||||
if err != nil {
|
||||
c.JSON(500, gin.H{"success": false, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(200, gin.H{"success": true, "message": "service fee retrieved", "data": fee})
|
||||
}
|
||||
|
||||
func (h *ZSPFinancesHandler) ListServiceFees(c *gin.Context) {
|
||||
fees, err := h.service.ListServiceFees()
|
||||
if err != nil {
|
||||
c.JSON(500, gin.H{"success": false, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(200, gin.H{"success": true, "message": "service fees retrieved", "data": fees})
|
||||
}
|
||||
|
||||
func (h *ZSPFinancesHandler) UpdateServiceFee(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
var req dto.ZSPServiceFeesUpdateRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(400, gin.H{"success": false, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
err := h.service.UpdateServiceFee(id, &req)
|
||||
if err != nil {
|
||||
c.JSON(500, gin.H{"success": false, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(200, gin.H{"success": true, "message": "service fee updated"})
|
||||
}
|
||||
|
||||
func (h *ZSPFinancesHandler) DeleteServiceFee(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
err := h.service.DeleteServiceFee(id)
|
||||
if err != nil {
|
||||
c.JSON(500, gin.H{"success": false, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(200, gin.H{"success": true, "message": "service fee deleted"})
|
||||
}
|
||||
|
||||
// ============ 成本构成 ============
|
||||
func (h *ZSPFinancesHandler) CreateCostBreakdown(c *gin.Context) {
|
||||
var req dto.ZSPCostBreakdownCreateRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(400, gin.H{"success": false, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
err := h.service.CreateCostBreakdown(&req)
|
||||
if err != nil {
|
||||
c.JSON(500, gin.H{"success": false, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(200, gin.H{"success": true, "message": "cost breakdown created"})
|
||||
}
|
||||
|
||||
func (h *ZSPFinancesHandler) GetCostBreakdown(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
breakdown, err := h.service.GetCostBreakdown(id)
|
||||
if err != nil {
|
||||
c.JSON(500, gin.H{"success": false, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(200, gin.H{"success": true, "message": "cost breakdown retrieved", "data": breakdown})
|
||||
}
|
||||
|
||||
func (h *ZSPFinancesHandler) ListCostBreakdowns(c *gin.Context) {
|
||||
businessID := c.Query("businessId")
|
||||
breakdowns, err := h.service.ListCostBreakdowns(businessID)
|
||||
if err != nil {
|
||||
c.JSON(500, gin.H{"success": false, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(200, gin.H{"success": true, "message": "cost breakdowns retrieved", "data": breakdowns})
|
||||
}
|
||||
|
||||
func (h *ZSPFinancesHandler) UpdateCostBreakdown(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
var req dto.ZSPCostBreakdownUpdateRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(400, gin.H{"success": false, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
err := h.service.UpdateCostBreakdown(id, &req)
|
||||
if err != nil {
|
||||
c.JSON(500, gin.H{"success": false, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(200, gin.H{"success": true, "message": "cost breakdown updated"})
|
||||
}
|
||||
|
||||
func (h *ZSPFinancesHandler) DeleteCostBreakdown(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
err := h.service.DeleteCostBreakdown(id)
|
||||
if err != nil {
|
||||
c.JSON(500, gin.H{"success": false, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(200, gin.H{"success": true, "message": "cost breakdown deleted"})
|
||||
}
|
||||
|
||||
// ============ 利润记录 ============
|
||||
func (h *ZSPFinancesHandler) CreateProfitRecord(c *gin.Context) {
|
||||
var req dto.ZSPProfitRecordCreateRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(400, gin.H{"success": false, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
err := h.service.CreateProfitRecord(&req)
|
||||
if err != nil {
|
||||
c.JSON(500, gin.H{"success": false, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(200, gin.H{"success": true, "message": "profit record created"})
|
||||
}
|
||||
|
||||
func (h *ZSPFinancesHandler) GetProfitRecord(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
record, err := h.service.GetProfitRecord(id)
|
||||
if err != nil {
|
||||
c.JSON(500, gin.H{"success": false, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(200, gin.H{"success": true, "message": "profit record retrieved", "data": record})
|
||||
}
|
||||
|
||||
func (h *ZSPFinancesHandler) ListProfitRecords(c *gin.Context) {
|
||||
filters := make(map[string]string)
|
||||
if c.Query("businessId") != "" {
|
||||
filters["businessId"] = c.Query("businessId")
|
||||
}
|
||||
if c.Query("businessType") != "" {
|
||||
filters["businessType"] = c.Query("businessType")
|
||||
}
|
||||
records, err := h.service.ListProfitRecords(filters)
|
||||
if err != nil {
|
||||
c.JSON(500, gin.H{"success": false, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(200, gin.H{"success": true, "message": "profit records retrieved", "data": records})
|
||||
}
|
||||
|
||||
func (h *ZSPFinancesHandler) UpdateProfitRecord(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
var req dto.ZSPProfitRecordUpdateRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(400, gin.H{"success": false, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
err := h.service.UpdateProfitRecord(id, &req)
|
||||
if err != nil {
|
||||
c.JSON(500, gin.H{"success": false, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(200, gin.H{"success": true, "message": "profit record updated"})
|
||||
}
|
||||
|
||||
func (h *ZSPFinancesHandler) DeleteProfitRecord(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
err := h.service.DeleteProfitRecord(id)
|
||||
if err != nil {
|
||||
c.JSON(500, gin.H{"success": false, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(200, gin.H{"success": true, "message": "profit record deleted"})
|
||||
}
|
||||
48
backend/internal/middleware/auth.go
Executable file
48
backend/internal/middleware/auth.go
Executable file
@@ -0,0 +1,48 @@
|
||||
// internal/middleware/auth.go
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/golang-jwt/jwt/v4"
|
||||
)
|
||||
|
||||
type Claims struct {
|
||||
UserID uint `json:"user_id"`
|
||||
jwt.RegisteredClaims
|
||||
}
|
||||
|
||||
func AuthMiddleware(jwtSecret string) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
authHeader := c.GetHeader("Authorization")
|
||||
if authHeader == "" {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "Authorization header is required"})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
tokenString := strings.TrimPrefix(authHeader, "Bearer ")
|
||||
if tokenString == authHeader {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "Bearer token is required"})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
claims := &Claims{}
|
||||
|
||||
token, err := jwt.ParseWithClaims(tokenString, claims, func(t *jwt.Token) (interface{}, error) {
|
||||
return []byte(jwtSecret), nil
|
||||
})
|
||||
|
||||
if err != nil || !token.Valid {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid token"})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
c.Set("userID", claims.ID) // 从token中解析出的用户ID
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
30
backend/internal/model/company.go
Executable file
30
backend/internal/model/company.go
Executable file
@@ -0,0 +1,30 @@
|
||||
package model
|
||||
|
||||
import "time"
|
||||
|
||||
// CompanyFolder 公司信息文件夹
|
||||
type CompanyFolder struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
Name string `gorm:"size:100;not null" json:"name"`
|
||||
Description string `gorm:"size:500" json:"description"`
|
||||
Category string `gorm:"size:50" json:"category"`
|
||||
CreateTime time.Time `json:"createTime"`
|
||||
UpdateTime time.Time `json:"updateTime"`
|
||||
|
||||
Files []CompanyFile `gorm:"foreignKey:FolderID" json:"files"`
|
||||
}
|
||||
|
||||
// CompanyFile 公司文件(营业执照、开票信息等)
|
||||
type CompanyFile struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
FolderID uint `gorm:"index" json:"folderId"`
|
||||
FileName string `gorm:"size:255;not null" json:"fileName"`
|
||||
FileType string `gorm:"size:50" json:"fileType"`
|
||||
FileSize int64 `json:"fileSize"`
|
||||
UploadTime time.Time `json:"uploadTime"`
|
||||
CompanyName string `gorm:"size:100" json:"companyName"`
|
||||
FileCategory string `gorm:"size:50" json:"fileCategory"` // license, tax, organization, bank, invoice, charter, legal, other
|
||||
ExpireDate *time.Time `json:"expireDate"`
|
||||
Description string `gorm:"size:500" json:"description"`
|
||||
FileURL string `gorm:"size:500" json:"fileUrl"`
|
||||
}
|
||||
25
backend/internal/model/contract.go
Executable file
25
backend/internal/model/contract.go
Executable file
@@ -0,0 +1,25 @@
|
||||
package model
|
||||
|
||||
import "time"
|
||||
|
||||
type Contract struct {
|
||||
ID uint `gorm:"primarykey" json:"id"`
|
||||
Name string `gorm:"size:100" json:"name"`
|
||||
ContractType string `gorm:"size:50" json:"contractType"`
|
||||
ContractCode string `gorm:"size:50" json:"contractCode"`
|
||||
BuyParty string `gorm:"size:100" json:"buyParty"`
|
||||
SellParty string `gorm:"size:100" json:"sellParty"`
|
||||
Commodity string `gorm:"size:100" json:"commodity"`
|
||||
Count float64 `gorm:"type:decimal(16,4);default:0" json:"count"`
|
||||
UnitPrice float64 `gorm:"type:decimal(16,4);default:0" json:"unitPrice"`
|
||||
BLNumber string `gorm:"size:500" json:"blNumber"`
|
||||
DeliveryTime string `gorm:"size:50" json:"deliveryTime"`
|
||||
PickUpTime string `gorm:"size:50" json:"pickUpTime"`
|
||||
Packaging string `gorm:"size:50" json:"packaging"`
|
||||
Remarks string `gorm:"size:255" json:"remarks"`
|
||||
|
||||
CreateTime time.Time `json:"createTime"`
|
||||
UpdateTime time.Time `json:"updateTime"`
|
||||
|
||||
Files []ContractFile `gorm:"foreignKey:ContractID" json:"files"`
|
||||
}
|
||||
8
backend/internal/model/contract_file.go
Executable file
8
backend/internal/model/contract_file.go
Executable file
@@ -0,0 +1,8 @@
|
||||
package model
|
||||
|
||||
type ContractFile struct {
|
||||
ID uint `gorm:"primarykey" json:"id"`
|
||||
ContractID uint `gorm:"index" json:"contractId"`
|
||||
Name string `gorm:"size:255" json:"name"`
|
||||
URL string `gorm:"size:500" json:"url"`
|
||||
}
|
||||
33
backend/internal/model/contract_folder.go
Executable file
33
backend/internal/model/contract_folder.go
Executable file
@@ -0,0 +1,33 @@
|
||||
package model
|
||||
|
||||
import "time"
|
||||
|
||||
// ContractFolder 业务合同批量归档文件夹(与 Contract 共用体系,用于归档批量上传)
|
||||
type ContractFolder struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
Name string `gorm:"size:100;not null" json:"name"`
|
||||
Description string `gorm:"size:500" json:"description"`
|
||||
CreateTime time.Time `json:"createTime"`
|
||||
UpdateTime time.Time `json:"updateTime"`
|
||||
}
|
||||
|
||||
// ContractBatch 一次批量上传记录(归属某文件夹)
|
||||
type ContractBatch struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
FolderID uint `gorm:"index;not null" json:"folderId"`
|
||||
BatchName string `gorm:"size:200;not null" json:"batchName"`
|
||||
Remark string `gorm:"size:500" json:"remark"`
|
||||
CreateTime time.Time `json:"createTime"`
|
||||
UpdateTime time.Time `json:"updateTime"`
|
||||
|
||||
Files []ContractBatchFile `gorm:"foreignKey:BatchID" json:"files"`
|
||||
}
|
||||
|
||||
// ContractBatchFile 批量上传中的单个文件
|
||||
type ContractBatchFile struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
BatchID uint `gorm:"index;not null" json:"batchId"`
|
||||
Name string `gorm:"size:255;not null" json:"name"`
|
||||
URL string `gorm:"size:500;not null" json:"url"`
|
||||
FileSize int64 `gorm:"default:0" json:"fileSize"`
|
||||
}
|
||||
115
backend/internal/model/delivery.go
Executable file
115
backend/internal/model/delivery.go
Executable file
@@ -0,0 +1,115 @@
|
||||
package model
|
||||
|
||||
import "time"
|
||||
|
||||
// DeliveryApply 我要提货 - 提货委托
|
||||
type DeliveryApply struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
OrderNumber string `gorm:"size:50;uniqueIndex" json:"orderNumber"`
|
||||
BLNumber string `gorm:"size:100" json:"blNumber"`
|
||||
BLNumber2 string `gorm:"size:100" json:"blNumber2"`
|
||||
LicensePlate string `gorm:"size:20" json:"licensePlate"`
|
||||
DriverName string `gorm:"size:50" json:"driverName"`
|
||||
DriverPhone string `gorm:"size:20" json:"driverPhone"`
|
||||
DriverIdCard string `gorm:"size:30" json:"driverIdCard"`
|
||||
Commodity string `gorm:"size:50" json:"commodity"`
|
||||
Quantity float64 `gorm:"type:decimal(16,4)" json:"quantity"`
|
||||
Unit string `gorm:"size:20" json:"unit"`
|
||||
DeliveryDate string `gorm:"size:20" json:"deliveryDate"`
|
||||
DeliveryAddress string `gorm:"size:200" json:"deliveryAddress"`
|
||||
Warehouse string `gorm:"size:100" json:"warehouse"`
|
||||
Status string `gorm:"size:20;default:pending" json:"status"` // pending, approved, in_progress, completed, cancelled
|
||||
Remark string `gorm:"size:500" json:"remark"`
|
||||
CreateTime time.Time `json:"createTime"`
|
||||
UpdateTime time.Time `json:"updateTime"`
|
||||
}
|
||||
|
||||
// DeliveryOutbound 提货明细 - 出库单
|
||||
type DeliveryOutbound struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
OutboundNumber string `gorm:"size:50;uniqueIndex" json:"outboundNumber"`
|
||||
DeliveryOrderNumber string `gorm:"size:50" json:"deliveryOrderNumber"`
|
||||
BLNumber string `gorm:"size:100" json:"blNumber"`
|
||||
LicensePlate string `gorm:"size:20" json:"licensePlate"`
|
||||
DriverName string `gorm:"size:50" json:"driverName"`
|
||||
DriverPhone string `gorm:"size:20" json:"driverPhone"`
|
||||
Commodity string `gorm:"size:50" json:"commodity"`
|
||||
CommodityCode string `gorm:"size:30" json:"commodityCode"`
|
||||
Quantity float64 `gorm:"type:decimal(16,4)" json:"quantity"`
|
||||
Unit string `gorm:"size:20" json:"unit"`
|
||||
OutboundDate string `gorm:"size:20" json:"outboundDate"`
|
||||
Warehouse string `gorm:"size:100" json:"warehouse"`
|
||||
Operator string `gorm:"size:50" json:"operator"`
|
||||
OutboundStatus string `gorm:"size:20;default:pending" json:"outboundStatus"`
|
||||
ReceiptStatus string `gorm:"size:20;default:pending" json:"receiptStatus"`
|
||||
Remark string `gorm:"size:500" json:"remark"`
|
||||
CreateTime time.Time `json:"createTime"`
|
||||
UpdateTime time.Time `json:"updateTime"`
|
||||
}
|
||||
|
||||
// Warehouse 仓库
|
||||
type Warehouse struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
Code string `gorm:"size:30;uniqueIndex" json:"code"`
|
||||
Name string `gorm:"size:100" json:"name"`
|
||||
Address string `gorm:"size:200" json:"address"`
|
||||
ContactPerson string `gorm:"size:50" json:"contactPerson"`
|
||||
ContactPhone string `gorm:"size:20" json:"contactPhone"`
|
||||
Status string `gorm:"size:20;default:active" json:"status"`
|
||||
Remark string `gorm:"size:500" json:"remark"`
|
||||
CreateTime time.Time `json:"createTime"`
|
||||
UpdateTime time.Time `json:"updateTime"`
|
||||
}
|
||||
|
||||
// InventoryInbound 入库单
|
||||
type InventoryInbound struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
InboundNumber string `gorm:"size:50;uniqueIndex" json:"inboundNumber"`
|
||||
Warehouse string `gorm:"size:100" json:"warehouse"`
|
||||
Commodity string `gorm:"size:50" json:"commodity"`
|
||||
CommodityCode string `gorm:"size:30" json:"commodityCode"`
|
||||
BLNumber string `gorm:"size:100" json:"blNumber"`
|
||||
BLWeight float64 `gorm:"type:decimal(16,4)" json:"blWeight"`
|
||||
InboundWeight float64 `gorm:"type:decimal(16,4)" json:"inboundWeight"`
|
||||
Owner string `gorm:"size:100" json:"owner"`
|
||||
ContractNumber string `gorm:"size:50" json:"contractNumber"`
|
||||
InboundDate string `gorm:"size:20" json:"inboundDate"`
|
||||
Operator string `gorm:"size:50" json:"operator"`
|
||||
Remark string `gorm:"size:500" json:"remark"`
|
||||
CreateTime time.Time `json:"createTime"`
|
||||
UpdateTime time.Time `json:"updateTime"`
|
||||
}
|
||||
|
||||
// OwnershipTransfer 货权转移
|
||||
type OwnershipTransfer struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
TransferNumber string `gorm:"size:50;uniqueIndex" json:"transferNumber"`
|
||||
TransferDate string `gorm:"size:20" json:"transferDate"`
|
||||
Commodity string `gorm:"size:50" json:"commodity"`
|
||||
Warehouse string `gorm:"size:100" json:"warehouse"`
|
||||
Transferor string `gorm:"size:100" json:"transferor"`
|
||||
Transferee string `gorm:"size:100" json:"transferee"`
|
||||
Status string `gorm:"size:20;default:draft" json:"status"`
|
||||
Remark string `gorm:"size:500" json:"remark"`
|
||||
CreateTime time.Time `json:"createTime"`
|
||||
UpdateTime time.Time `json:"updateTime"`
|
||||
|
||||
BLItems []OwnershipTransferBL `gorm:"foreignKey:TransferID" json:"blItems"`
|
||||
Files []OwnershipTransferFile `gorm:"foreignKey:TransferID" json:"fileList"`
|
||||
}
|
||||
|
||||
// OwnershipTransferBL 货权转移 - 提单明细(支持动态多条)
|
||||
type OwnershipTransferBL struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
TransferID uint `gorm:"index;not null" json:"transferId"`
|
||||
BLNumber string `gorm:"size:100" json:"blNumber"`
|
||||
Quantity float64 `gorm:"type:decimal(16,4)" json:"quantity"`
|
||||
}
|
||||
|
||||
// OwnershipTransferFile 货权转移附件
|
||||
type OwnershipTransferFile struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
TransferID uint `gorm:"index" json:"transferId"`
|
||||
Name string `gorm:"size:255" json:"name"`
|
||||
URL string `gorm:"size:500" json:"url"`
|
||||
}
|
||||
14
backend/internal/model/user.go
Executable file
14
backend/internal/model/user.go
Executable file
@@ -0,0 +1,14 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
type User struct {
|
||||
ID uint `gorm:"primarykey" json:"id"`
|
||||
Username string `gorm:"uniqueIndex;size:50" json:"username"`
|
||||
Email string `gorm:"uniqueIndex;size:100" json:"email"`
|
||||
Password string `gorm:"size:255" json:"-"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
99
backend/internal/model/zsp_contract.go
Executable file
99
backend/internal/model/zsp_contract.go
Executable file
@@ -0,0 +1,99 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// {
|
||||
// id: "1",
|
||||
// fileName: "上游合同-铜矿采购-2024-001.pdf",
|
||||
// fileType: "pdf",
|
||||
// fileSize: "2.3MB",
|
||||
// uploadTime: "2024-01-20",
|
||||
// folderId: "1",
|
||||
// contractNumber: "SC2024001",
|
||||
// companyName: "中尚鹏贸易有限公司",
|
||||
// commodity: "铜矿",
|
||||
// signDate: "2024-01-15",
|
||||
// contractType: "upstream"
|
||||
// }
|
||||
|
||||
type ZSPContractFolder struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
Name string `gorm:"size:100" json:"name"`
|
||||
Count int `json:"count"`
|
||||
CreateTime time.Time `json:"createTime"`
|
||||
Description string `gorm:"size:255" json:"description"`
|
||||
|
||||
Files []ZSPContractFile `gorm:"foreignKey:FolderID" json:"files"`
|
||||
}
|
||||
|
||||
type ZSPContractFile struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
FileName string `gorm:"size:255" json:"fileName"`
|
||||
FileURL string `gorm:"size:500" json:"fileUrl"`
|
||||
FileType string `gorm:"size:50" json:"fileType"`
|
||||
FileSize int64 `json:"fileSize"`
|
||||
UploadTime time.Time `json:"uploadTime"`
|
||||
|
||||
FolderID uint `gorm:"index" json:"folderId"`
|
||||
Folder ZSPContractFolder `gorm:"foreignKey:FolderID" json:"folder"`
|
||||
|
||||
ContractNumber string `gorm:"size:50" json:"contractNumber"`
|
||||
CompanyName string `gorm:"size:100" json:"companyName"`
|
||||
Commodity string `gorm:"size:100" json:"commodity"`
|
||||
SignDate time.Time `json:"signDate"`
|
||||
ContractType string `gorm:"size:50" json:"contractType"`
|
||||
|
||||
// 合同详情
|
||||
Details []ZSPContractDetail `gorm:"foreignKey:ContractFileID" json:"details"`
|
||||
}
|
||||
|
||||
// ZSPContractDetail 合同详情表(商品明细)
|
||||
type ZSPContractDetail struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
ContractFileID uint `gorm:"index;not null" json:"contractFileId"`
|
||||
ContractFile ZSPContractFile `gorm:"foreignKey:ContractFileID" json:"contractFile"`
|
||||
|
||||
// 商品信息
|
||||
CommodityName string `gorm:"size:100;not null" json:"commodityName"` // 商品名称
|
||||
CommodityCode string `gorm:"size:50" json:"commodityCode"` // 商品编码
|
||||
|
||||
// 数量信息
|
||||
TotalQuantity float64 `gorm:"not null" json:"totalQuantity"` // 合同总量
|
||||
Unit string `gorm:"size:20;default:'吨'" json:"unit"` // 单位
|
||||
|
||||
// 价格信息
|
||||
UnitPrice float64 `gorm:"not null" json:"unitPrice"` // 单价
|
||||
|
||||
// 提货信息
|
||||
DeliveredQty float64 `gorm:"default:0" json:"deliveredQty"` // 已提货数量
|
||||
PendingQty float64 `gorm:"default:0" json:"pendingQty"` // 应提货数量(自动计算 = 总量 - 已提货)
|
||||
|
||||
// 提单信息
|
||||
BillOfLading string `gorm:"size:100" json:"billOfLading"` // 提单号
|
||||
|
||||
// 备注
|
||||
Remark string `gorm:"size:500" json:"remark"`
|
||||
|
||||
// 时间戳
|
||||
CreateTime time.Time `json:"createTime"`
|
||||
UpdateTime time.Time `json:"updateTime"`
|
||||
}
|
||||
|
||||
// BeforeCreate GORM钩子,在创建前自动计算应提货数量
|
||||
func (d *ZSPContractDetail) BeforeCreate(tx *gorm.DB) error {
|
||||
d.PendingQty = d.TotalQuantity - d.DeliveredQty
|
||||
d.CreateTime = time.Now()
|
||||
d.UpdateTime = time.Now()
|
||||
return nil
|
||||
}
|
||||
|
||||
// BeforeUpdate GORM钩子,在更新前自动计算应提货数量
|
||||
func (d *ZSPContractDetail) BeforeUpdate(tx *gorm.DB) error {
|
||||
d.PendingQty = d.TotalQuantity - d.DeliveredQty
|
||||
d.UpdateTime = time.Now()
|
||||
return nil
|
||||
}
|
||||
77
backend/internal/model/zsp_freight_charges.go
Executable file
77
backend/internal/model/zsp_freight_charges.go
Executable file
@@ -0,0 +1,77 @@
|
||||
package model
|
||||
|
||||
import "time"
|
||||
|
||||
// ZSPFreightCharges 货代费用表
|
||||
type ZSPFreightCharges struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
BillOfLading string `gorm:"size:100;index" json:"billOfLading"`
|
||||
ContractNumber string `gorm:"size:100;index" json:"contractNumber"`
|
||||
ExpenseItem string `gorm:"size:200;not null" json:"expenseItem"`
|
||||
Amount float64 `gorm:"type:decimal(16,4);not null" json:"amount"`
|
||||
Currency string `gorm:"size:10;default:'CNY'" json:"currency"`
|
||||
PaymentDate time.Time `gorm:"not null" json:"paymentDate"`
|
||||
FreightCompanyName string `gorm:"size:200;not null" json:"freightCompanyName"`
|
||||
Remark string `gorm:"size:500" json:"remark"`
|
||||
CreateTime time.Time `json:"createTime"`
|
||||
UpdateTime time.Time `json:"updateTime"`
|
||||
}
|
||||
|
||||
// ZSPMiscCharges 其他杂费表
|
||||
type ZSPMiscCharges struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
BillOfLading string `gorm:"size:100;index" json:"billOfLading"`
|
||||
ContractNumber string `gorm:"size:100;index" json:"contractNumber"`
|
||||
ExpenseItem string `gorm:"size:200;not null" json:"expenseItem"`
|
||||
Amount float64 `gorm:"type:decimal(16,4);not null" json:"amount"`
|
||||
Currency string `gorm:"size:10;default:'CNY'" json:"currency"`
|
||||
PaymentDate time.Time `gorm:"not null" json:"paymentDate"`
|
||||
CompanyName string `gorm:"size:200" json:"companyName"`
|
||||
Remark string `gorm:"size:500" json:"remark"`
|
||||
CreateTime time.Time `json:"createTime"`
|
||||
UpdateTime time.Time `json:"updateTime"`
|
||||
}
|
||||
|
||||
// ZSPServiceFees 服务费表
|
||||
type ZSPServiceFees struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
BillOfLading string `gorm:"size:100;index" json:"billOfLading"`
|
||||
ContractNumber string `gorm:"size:100;index" json:"contractNumber"`
|
||||
Name string `gorm:"size:100;not null" json:"name"`
|
||||
Amount float64 `gorm:"type:decimal(16,4);not null" json:"amount"`
|
||||
Currency string `gorm:"size:10;default:'CNY'" json:"currency"`
|
||||
Date time.Time `gorm:"not null" json:"date"`
|
||||
Remark string `gorm:"size:500" json:"remark"`
|
||||
CreateTime time.Time `json:"createTime"`
|
||||
UpdateTime time.Time `json:"updateTime"`
|
||||
}
|
||||
|
||||
// ZSPCostBreakdown 成本构成明细表
|
||||
type ZSPCostBreakdown struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
BusinessID string `gorm:"size:100;not null;index" json:"businessId"`
|
||||
BusinessType string `gorm:"size:50;not null;index" json:"businessType"`
|
||||
TotalCost float64 `gorm:"type:decimal(16,4);not null" json:"totalCost"`
|
||||
|
||||
FreightChargeTotal float64 `gorm:"type:decimal(16,4);default:0" json:"freightChargeTotal"`
|
||||
MiscChargeTotal float64 `gorm:"type:decimal(16,4);default:0" json:"miscChargeTotal"`
|
||||
ServiceFeeTotal float64 `gorm:"type:decimal(16,4);default:0" json:"serviceFeeTotal"`
|
||||
|
||||
CreateTime time.Time `json:"createTime"`
|
||||
UpdateTime time.Time `json:"updateTime"`
|
||||
}
|
||||
|
||||
// ZSPProfitRecord 利润记录表
|
||||
type ZSPProfitRecord struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
BusinessID string `gorm:"size:100;not null;index" json:"businessId"`
|
||||
BusinessType string `gorm:"size:50;not null;index" json:"businessType"`
|
||||
Revenue float64 `gorm:"type:decimal(16,4);not null" json:"revenue"`
|
||||
TotalCost float64 `gorm:"type:decimal(16,4);not null" json:"totalCost"`
|
||||
Profit float64 `gorm:"type:decimal(16,4);not null" json:"profit"`
|
||||
ProfitRate float64 `gorm:"type:decimal(8,4)" json:"profitRate"`
|
||||
RecordDate time.Time `gorm:"not null" json:"recordDate"`
|
||||
Remark string `gorm:"size:500" json:"remark"`
|
||||
CreateTime time.Time `json:"createTime"`
|
||||
UpdateTime time.Time `json:"updateTime"`
|
||||
}
|
||||
63
backend/internal/model/zsp_invoice.go
Executable file
63
backend/internal/model/zsp_invoice.go
Executable file
@@ -0,0 +1,63 @@
|
||||
package model
|
||||
|
||||
import "time"
|
||||
|
||||
// ZSPUpstreamInvoice 上游发票表
|
||||
type ZSPUpstreamInvoice struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
InvoiceNumber string `gorm:"size:100;uniqueIndex;not null" json:"invoice_number"`
|
||||
ContractNumber string `gorm:"size:100;index" json:"contract_number"`
|
||||
BillOfLading string `gorm:"size:100;index" json:"bill_of_lading"`
|
||||
SupplierName string `gorm:"size:200;not null" json:"supplier_name"`
|
||||
Commodity string `gorm:"size:100" json:"commodity"`
|
||||
Amount float64 `gorm:"type:decimal(16,4);not null" json:"amount"`
|
||||
TaxAmount float64 `gorm:"type:decimal(16,4);default:0" json:"tax_amount"`
|
||||
TotalAmount float64 `gorm:"type:decimal(16,4);not null" json:"total_amount"`
|
||||
Currency string `gorm:"size:10;default:'CNY'" json:"currency"`
|
||||
InvoiceDate time.Time `gorm:"not null" json:"invoice_date"`
|
||||
Status string `gorm:"size:20;default:'pending'" json:"status"`
|
||||
FilePath string `gorm:"size:500" json:"file_path"`
|
||||
Remark string `gorm:"size:500" json:"remark"`
|
||||
CreateTime time.Time `json:"create_time"`
|
||||
UpdateTime time.Time `json:"update_time"`
|
||||
}
|
||||
|
||||
// ZSPDownstreamInvoice 下游发票表
|
||||
type ZSPDownstreamInvoice struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
InvoiceNumber string `gorm:"size:100;uniqueIndex;not null" json:"invoice_number"`
|
||||
ContractNumber string `gorm:"size:100;index" json:"contract_number"`
|
||||
BillOfLading string `gorm:"size:100;index" json:"bill_of_lading"`
|
||||
CustomerName string `gorm:"size:200;not null" json:"customer_name"`
|
||||
Commodity string `gorm:"size:100" json:"commodity"`
|
||||
Amount float64 `gorm:"type:decimal(16,4);not null" json:"amount"`
|
||||
TaxAmount float64 `gorm:"type:decimal(16,4);default:0" json:"tax_amount"`
|
||||
TotalAmount float64 `gorm:"type:decimal(16,4);not null" json:"total_amount"`
|
||||
Currency string `gorm:"size:10;default:'CNY'" json:"currency"`
|
||||
InvoiceDate time.Time `gorm:"not null" json:"invoice_date"`
|
||||
Status string `gorm:"size:20;default:'pending'" json:"status"`
|
||||
FilePath string `gorm:"size:500" json:"file_path"`
|
||||
Remark string `gorm:"size:500" json:"remark"`
|
||||
CreateTime time.Time `json:"create_time"`
|
||||
UpdateTime time.Time `json:"update_time"`
|
||||
}
|
||||
|
||||
// ZSPFreightMiscInvoice 货代杂费发票表
|
||||
type ZSPFreightMiscInvoice struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
InvoiceNumber string `gorm:"size:100;uniqueIndex;not null" json:"invoice_number"`
|
||||
ContractNumber string `gorm:"size:100;index" json:"contract_number"`
|
||||
BillOfLading string `gorm:"size:100;index" json:"bill_of_lading"`
|
||||
CompanyName string `gorm:"size:200;not null" json:"company_name"`
|
||||
ExpenseItem string `gorm:"size:200" json:"expense_item"`
|
||||
Amount float64 `gorm:"type:decimal(16,4);not null" json:"amount"`
|
||||
TaxAmount float64 `gorm:"type:decimal(16,4);default:0" json:"tax_amount"`
|
||||
TotalAmount float64 `gorm:"type:decimal(16,4);not null" json:"total_amount"`
|
||||
Currency string `gorm:"size:10;default:'CNY'" json:"currency"`
|
||||
InvoiceDate time.Time `gorm:"not null" json:"invoice_date"`
|
||||
Status string `gorm:"size:20;default:'pending'" json:"status"`
|
||||
FilePath string `gorm:"size:500" json:"file_path"`
|
||||
Remark string `gorm:"size:500" json:"remark"`
|
||||
CreateTime time.Time `json:"create_time"`
|
||||
UpdateTime time.Time `json:"update_time"`
|
||||
}
|
||||
42
backend/internal/model/zsp_settlement.go
Executable file
42
backend/internal/model/zsp_settlement.go
Executable file
@@ -0,0 +1,42 @@
|
||||
package model
|
||||
|
||||
import "time"
|
||||
|
||||
// ZSPSettlement 结算单表
|
||||
type ZSPSettlement struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
SettlementNumber string `gorm:"size:100;uniqueIndex;not null" json:"settlement_number"`
|
||||
ContractNumber string `gorm:"size:100;index;not null" json:"contract_number"`
|
||||
ContractType string `gorm:"size:50" json:"contract_type"`
|
||||
CompanyName string `gorm:"size:200;not null" json:"company_name"`
|
||||
Commodity string `gorm:"size:100" json:"commodity"`
|
||||
Quantity float64 `gorm:"type:decimal(16,4)" json:"quantity"`
|
||||
UnitPrice float64 `gorm:"type:decimal(16,4)" json:"unit_price"`
|
||||
Amount float64 `gorm:"type:decimal(16,4);not null" json:"amount"`
|
||||
Currency string `gorm:"size:10;default:'CNY'" json:"currency"`
|
||||
SettlementDate time.Time `gorm:"not null" json:"settlement_date"`
|
||||
Status string `gorm:"size:20;default:'pending'" json:"status"`
|
||||
FilePath string `gorm:"size:500" json:"file_path"`
|
||||
Remark string `gorm:"size:500" json:"remark"`
|
||||
CreateTime time.Time `json:"create_time"`
|
||||
UpdateTime time.Time `json:"update_time"`
|
||||
}
|
||||
|
||||
// ZSPReconciliation 对账单表
|
||||
type ZSPReconciliation struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
ReconciliationNumber string `gorm:"size:100;uniqueIndex;not null" json:"reconciliation_number"`
|
||||
ContractNumber string `gorm:"size:100;index" json:"contract_number"`
|
||||
CompanyName string `gorm:"size:200;not null" json:"company_name"`
|
||||
PeriodStart time.Time `json:"period_start"`
|
||||
PeriodEnd time.Time `json:"period_end"`
|
||||
TotalAmount float64 `gorm:"type:decimal(16,4)" json:"total_amount"`
|
||||
PaidAmount float64 `gorm:"type:decimal(16,4)" json:"paid_amount"`
|
||||
UnpaidAmount float64 `gorm:"type:decimal(16,4)" json:"unpaid_amount"`
|
||||
Currency string `gorm:"size:10;default:'CNY'" json:"currency"`
|
||||
Status string `gorm:"size:20;default:'draft'" json:"status"`
|
||||
FilePath string `gorm:"size:500" json:"file_path"`
|
||||
Remark string `gorm:"size:500" json:"remark"`
|
||||
CreateTime time.Time `json:"create_time"`
|
||||
UpdateTime time.Time `json:"update_time"`
|
||||
}
|
||||
86
backend/internal/repository/company_repository.go
Executable file
86
backend/internal/repository/company_repository.go
Executable file
@@ -0,0 +1,86 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"github.com/rovina/zsp-backend/internal/model"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type CompanyRepository interface {
|
||||
CreateFolder(f *model.CompanyFolder) error
|
||||
ListFolders() ([]model.CompanyFolder, error)
|
||||
GetFolderByID(id uint) (*model.CompanyFolder, error)
|
||||
UpdateFolder(f *model.CompanyFolder) error
|
||||
DeleteFolder(id uint) error
|
||||
|
||||
CreateFile(f *model.CompanyFile) error
|
||||
ListFiles(folderID *uint) ([]model.CompanyFile, error)
|
||||
GetFileByID(id uint) (*model.CompanyFile, error)
|
||||
UpdateFile(f *model.CompanyFile) error
|
||||
DeleteFile(id uint) error
|
||||
}
|
||||
|
||||
type companyRepository struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewCompanyRepository(db *gorm.DB) CompanyRepository {
|
||||
return &companyRepository{db: db}
|
||||
}
|
||||
|
||||
func (r *companyRepository) CreateFolder(f *model.CompanyFolder) error {
|
||||
return r.db.Create(f).Error
|
||||
}
|
||||
|
||||
func (r *companyRepository) ListFolders() ([]model.CompanyFolder, error) {
|
||||
var list []model.CompanyFolder
|
||||
err := r.db.Order("create_time DESC").Find(&list).Error
|
||||
return list, err
|
||||
}
|
||||
|
||||
func (r *companyRepository) GetFolderByID(id uint) (*model.CompanyFolder, error) {
|
||||
var f model.CompanyFolder
|
||||
err := r.db.First(&f, id).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &f, nil
|
||||
}
|
||||
|
||||
func (r *companyRepository) UpdateFolder(f *model.CompanyFolder) error {
|
||||
return r.db.Save(f).Error
|
||||
}
|
||||
|
||||
func (r *companyRepository) DeleteFolder(id uint) error {
|
||||
return r.db.Delete(&model.CompanyFolder{}, id).Error
|
||||
}
|
||||
|
||||
func (r *companyRepository) CreateFile(f *model.CompanyFile) error {
|
||||
return r.db.Create(f).Error
|
||||
}
|
||||
|
||||
func (r *companyRepository) ListFiles(folderID *uint) ([]model.CompanyFile, error) {
|
||||
var list []model.CompanyFile
|
||||
q := r.db.Order("upload_time DESC")
|
||||
if folderID != nil && *folderID > 0 {
|
||||
q = q.Where("folder_id = ?", *folderID)
|
||||
}
|
||||
err := q.Find(&list).Error
|
||||
return list, err
|
||||
}
|
||||
|
||||
func (r *companyRepository) GetFileByID(id uint) (*model.CompanyFile, error) {
|
||||
var f model.CompanyFile
|
||||
err := r.db.First(&f, id).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &f, nil
|
||||
}
|
||||
|
||||
func (r *companyRepository) UpdateFile(f *model.CompanyFile) error {
|
||||
return r.db.Save(f).Error
|
||||
}
|
||||
|
||||
func (r *companyRepository) DeleteFile(id uint) error {
|
||||
return r.db.Delete(&model.CompanyFile{}, id).Error
|
||||
}
|
||||
65
backend/internal/repository/contract_batch_repository.go
Executable file
65
backend/internal/repository/contract_batch_repository.go
Executable file
@@ -0,0 +1,65 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"github.com/rovina/zsp-backend/internal/model"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type ContractBatchRepository interface {
|
||||
List(folderID *uint) ([]model.ContractBatch, error)
|
||||
GetByID(id uint) (*model.ContractBatch, error)
|
||||
Create(b *model.ContractBatch, files []model.ContractBatchFile) error
|
||||
Delete(id uint) error
|
||||
}
|
||||
|
||||
type contractBatchRepository struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewContractBatchRepository(db *gorm.DB) ContractBatchRepository {
|
||||
return &contractBatchRepository{db: db}
|
||||
}
|
||||
|
||||
func (r *contractBatchRepository) List(folderID *uint) ([]model.ContractBatch, error) {
|
||||
var list []model.ContractBatch
|
||||
query := r.db.Model(&model.ContractBatch{}).Order("create_time DESC")
|
||||
if folderID != nil && *folderID > 0 {
|
||||
query = query.Where("folder_id = ?", *folderID)
|
||||
}
|
||||
err := query.Preload("Files").Find(&list).Error
|
||||
return list, err
|
||||
}
|
||||
|
||||
func (r *contractBatchRepository) GetByID(id uint) (*model.ContractBatch, error) {
|
||||
var b model.ContractBatch
|
||||
if err := r.db.Preload("Files").First(&b, id).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &b, nil
|
||||
}
|
||||
|
||||
func (r *contractBatchRepository) Create(b *model.ContractBatch, files []model.ContractBatchFile) error {
|
||||
return r.db.Transaction(func(tx *gorm.DB) error {
|
||||
if err := tx.Create(b).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
for i := range files {
|
||||
files[i].BatchID = b.ID
|
||||
}
|
||||
if len(files) > 0 {
|
||||
if err := tx.Create(&files).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (r *contractBatchRepository) Delete(id uint) error {
|
||||
return r.db.Transaction(func(tx *gorm.DB) error {
|
||||
if err := tx.Where("batch_id = ?", id).Delete(&model.ContractBatchFile{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return tx.Delete(&model.ContractBatch{}, id).Error
|
||||
})
|
||||
}
|
||||
48
backend/internal/repository/contract_folder_repository.go
Executable file
48
backend/internal/repository/contract_folder_repository.go
Executable file
@@ -0,0 +1,48 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"github.com/rovina/zsp-backend/internal/model"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type ContractFolderRepository interface {
|
||||
List() ([]model.ContractFolder, error)
|
||||
GetByID(id uint) (*model.ContractFolder, error)
|
||||
Create(f *model.ContractFolder) error
|
||||
Update(f *model.ContractFolder) error
|
||||
Delete(id uint) error
|
||||
}
|
||||
|
||||
type contractFolderRepository struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewContractFolderRepository(db *gorm.DB) ContractFolderRepository {
|
||||
return &contractFolderRepository{db: db}
|
||||
}
|
||||
|
||||
func (r *contractFolderRepository) List() ([]model.ContractFolder, error) {
|
||||
var list []model.ContractFolder
|
||||
err := r.db.Order("create_time DESC").Find(&list).Error
|
||||
return list, err
|
||||
}
|
||||
|
||||
func (r *contractFolderRepository) GetByID(id uint) (*model.ContractFolder, error) {
|
||||
var f model.ContractFolder
|
||||
if err := r.db.First(&f, id).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &f, nil
|
||||
}
|
||||
|
||||
func (r *contractFolderRepository) Create(f *model.ContractFolder) error {
|
||||
return r.db.Create(f).Error
|
||||
}
|
||||
|
||||
func (r *contractFolderRepository) Update(f *model.ContractFolder) error {
|
||||
return r.db.Save(f).Error
|
||||
}
|
||||
|
||||
func (r *contractFolderRepository) Delete(id uint) error {
|
||||
return r.db.Delete(&model.ContractFolder{}, id).Error
|
||||
}
|
||||
120
backend/internal/repository/contract_repository.go
Executable file
120
backend/internal/repository/contract_repository.go
Executable file
@@ -0,0 +1,120 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"github.com/rovina/zsp-backend/internal/model"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type ContractRepository interface {
|
||||
CreateWithFiles(contract *model.Contract, files []model.ContractFile) error
|
||||
CreateFiles(files []model.ContractFile) error
|
||||
BatchInsert(files []model.ContractFile) error
|
||||
List(contractType string) ([]model.Contract, error)
|
||||
GetByID(id uint) (*model.Contract, error)
|
||||
Update(contract *model.Contract) error
|
||||
Delete(id uint) error
|
||||
DeleteFilesByContractIDExcept(contractID uint, keepIDs []uint) error
|
||||
UpdateWithFiles(contract *model.Contract, keepFileIDs []uint, newFiles []model.ContractFile) error
|
||||
}
|
||||
|
||||
type contractRepository struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewContractRepository(db *gorm.DB) ContractRepository {
|
||||
return &contractRepository{db: db}
|
||||
}
|
||||
|
||||
func (r *contractRepository) CreateFiles(files []model.ContractFile) error {
|
||||
return r.db.Create(&files).Error
|
||||
}
|
||||
|
||||
func (r *contractRepository) CreateWithFiles(contract *model.Contract, files []model.ContractFile) error {
|
||||
return r.db.Transaction(func(tx *gorm.DB) error {
|
||||
|
||||
if err := tx.Create(contract).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for i := range files {
|
||||
files[i].ContractID = contract.ID
|
||||
}
|
||||
|
||||
if len(files) > 0 {
|
||||
if err := tx.Create(&files).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (r *contractRepository) BatchInsert(files []model.ContractFile) error {
|
||||
return r.db.Create(&files).Error
|
||||
}
|
||||
|
||||
func (r *contractRepository) List(contractType string) ([]model.Contract, error) {
|
||||
var list []model.Contract
|
||||
query := r.db.Model(&model.Contract{}).Order("create_time DESC")
|
||||
if contractType != "" {
|
||||
query = query.Where("contract_type = ?", contractType)
|
||||
}
|
||||
err := query.Preload("Files").Find(&list).Error
|
||||
return list, err
|
||||
}
|
||||
|
||||
func (r *contractRepository) GetByID(id uint) (*model.Contract, error) {
|
||||
var c model.Contract
|
||||
err := r.db.Preload("Files").First(&c, id).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &c, nil
|
||||
}
|
||||
|
||||
func (r *contractRepository) Update(contract *model.Contract) error {
|
||||
return r.db.Save(contract).Error
|
||||
}
|
||||
|
||||
func (r *contractRepository) Delete(id uint) error {
|
||||
return r.db.Transaction(func(tx *gorm.DB) error {
|
||||
if err := tx.Where("contract_id = ?", id).Delete(&model.ContractFile{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return tx.Delete(&model.Contract{}, id).Error
|
||||
})
|
||||
}
|
||||
|
||||
func (r *contractRepository) DeleteFilesByContractIDExcept(contractID uint, keepIDs []uint) error {
|
||||
if len(keepIDs) == 0 {
|
||||
return r.db.Where("contract_id = ?", contractID).Delete(&model.ContractFile{}).Error
|
||||
}
|
||||
return r.db.Where("contract_id = ? AND id NOT IN ?", contractID, keepIDs).Delete(&model.ContractFile{}).Error
|
||||
}
|
||||
|
||||
func (r *contractRepository) UpdateWithFiles(contract *model.Contract, keepFileIDs []uint, newFiles []model.ContractFile) error {
|
||||
return r.db.Transaction(func(tx *gorm.DB) error {
|
||||
if err := tx.Save(contract).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if len(keepFileIDs) == 0 {
|
||||
if err := tx.Where("contract_id = ?", contract.ID).Delete(&model.ContractFile{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
if err := tx.Where("contract_id = ? AND id NOT IN ?", contract.ID, keepFileIDs).Delete(&model.ContractFile{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
for i := range newFiles {
|
||||
newFiles[i].ContractID = contract.ID
|
||||
}
|
||||
if len(newFiles) > 0 {
|
||||
if err := tx.Create(&newFiles).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
269
backend/internal/repository/delivery_repository.go
Executable file
269
backend/internal/repository/delivery_repository.go
Executable file
@@ -0,0 +1,269 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"github.com/rovina/zsp-backend/internal/model"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// ----- 我要提货 -----
|
||||
func CreateDeliveryApply(db *gorm.DB, e *model.DeliveryApply) error { return db.Create(e).Error }
|
||||
func ListDeliveryApply(db *gorm.DB, page, pageSize int, orderNumber, blNumber, status string) ([]model.DeliveryApply, int64, error) {
|
||||
var list []model.DeliveryApply
|
||||
q := db.Model(&model.DeliveryApply{})
|
||||
if orderNumber != "" {
|
||||
q = q.Where("order_number LIKE ?", "%"+orderNumber+"%")
|
||||
}
|
||||
if blNumber != "" {
|
||||
q = q.Where("bl_number LIKE ?", "%"+blNumber+"%")
|
||||
}
|
||||
if status != "" {
|
||||
q = q.Where("status = ?", status)
|
||||
}
|
||||
var total int64
|
||||
if err := q.Count(&total).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
offset := (page - 1) * pageSize
|
||||
if offset < 0 {
|
||||
offset = 0
|
||||
}
|
||||
if pageSize <= 0 {
|
||||
pageSize = 10
|
||||
}
|
||||
err := q.Order("create_time DESC").Offset(offset).Limit(pageSize).Find(&list).Error
|
||||
return list, total, err
|
||||
}
|
||||
func GetDeliveryApplyByID(db *gorm.DB, id uint) (*model.DeliveryApply, error) {
|
||||
var e model.DeliveryApply
|
||||
if err := db.First(&e, id).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &e, nil
|
||||
}
|
||||
func UpdateDeliveryApply(db *gorm.DB, e *model.DeliveryApply) error { return db.Save(e).Error }
|
||||
func DeleteDeliveryApply(db *gorm.DB, id uint) error { return db.Delete(&model.DeliveryApply{}, id).Error }
|
||||
|
||||
// ----- 提货明细/出库单 -----
|
||||
func CreateDeliveryOutbound(db *gorm.DB, e *model.DeliveryOutbound) error { return db.Create(e).Error }
|
||||
func ListDeliveryOutbound(db *gorm.DB, page, pageSize int, outboundNumber, blNumber, status string) ([]model.DeliveryOutbound, int64, error) {
|
||||
var list []model.DeliveryOutbound
|
||||
q := db.Model(&model.DeliveryOutbound{})
|
||||
if outboundNumber != "" {
|
||||
q = q.Where("outbound_number LIKE ?", "%"+outboundNumber+"%")
|
||||
}
|
||||
if blNumber != "" {
|
||||
q = q.Where("bl_number LIKE ?", "%"+blNumber+"%")
|
||||
}
|
||||
if status != "" {
|
||||
q = q.Where("outbound_status = ?", status)
|
||||
}
|
||||
var total int64
|
||||
if err := q.Count(&total).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
offset := (page - 1) * pageSize
|
||||
if offset < 0 {
|
||||
offset = 0
|
||||
}
|
||||
if pageSize <= 0 {
|
||||
pageSize = 10
|
||||
}
|
||||
err := q.Order("create_time DESC").Offset(offset).Limit(pageSize).Find(&list).Error
|
||||
return list, total, err
|
||||
}
|
||||
func GetDeliveryOutboundByID(db *gorm.DB, id uint) (*model.DeliveryOutbound, error) {
|
||||
var e model.DeliveryOutbound
|
||||
if err := db.First(&e, id).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &e, nil
|
||||
}
|
||||
func UpdateDeliveryOutbound(db *gorm.DB, e *model.DeliveryOutbound) error { return db.Save(e).Error }
|
||||
func DeleteDeliveryOutbound(db *gorm.DB, id uint) error { return db.Delete(&model.DeliveryOutbound{}, id).Error }
|
||||
|
||||
// ----- 仓库 -----
|
||||
func CreateWarehouse(db *gorm.DB, e *model.Warehouse) error { return db.Create(e).Error }
|
||||
func ListWarehouses(db *gorm.DB) ([]model.Warehouse, error) { var list []model.Warehouse; err := db.Order("create_time DESC").Find(&list).Error; return list, err }
|
||||
func GetWarehouseByID(db *gorm.DB, id uint) (*model.Warehouse, error) {
|
||||
var e model.Warehouse
|
||||
if err := db.First(&e, id).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &e, nil
|
||||
}
|
||||
func UpdateWarehouse(db *gorm.DB, e *model.Warehouse) error { return db.Save(e).Error }
|
||||
func DeleteWarehouse(db *gorm.DB, id uint) error { return db.Delete(&model.Warehouse{}, id).Error }
|
||||
|
||||
// ----- 入库单 -----
|
||||
func CreateInventoryInbound(db *gorm.DB, e *model.InventoryInbound) error { return db.Create(e).Error }
|
||||
func ListInventoryInbound(db *gorm.DB, page, pageSize int, warehouse, commodity string) ([]model.InventoryInbound, int64, error) {
|
||||
var list []model.InventoryInbound
|
||||
q := db.Model(&model.InventoryInbound{})
|
||||
if warehouse != "" {
|
||||
q = q.Where("warehouse = ?", warehouse)
|
||||
}
|
||||
if commodity != "" {
|
||||
q = q.Where("commodity LIKE ?", "%"+commodity+"%")
|
||||
}
|
||||
var total int64
|
||||
if err := q.Count(&total).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
offset := (page - 1) * pageSize
|
||||
if offset < 0 {
|
||||
offset = 0
|
||||
}
|
||||
if pageSize <= 0 {
|
||||
pageSize = 10
|
||||
}
|
||||
err := q.Order("create_time DESC").Offset(offset).Limit(pageSize).Find(&list).Error
|
||||
return list, total, err
|
||||
}
|
||||
func GetInventoryInboundByID(db *gorm.DB, id uint) (*model.InventoryInbound, error) {
|
||||
var e model.InventoryInbound
|
||||
if err := db.First(&e, id).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &e, nil
|
||||
}
|
||||
func UpdateInventoryInbound(db *gorm.DB, e *model.InventoryInbound) error { return db.Save(e).Error }
|
||||
func DeleteInventoryInbound(db *gorm.DB, id uint) error { return db.Delete(&model.InventoryInbound{}, id).Error }
|
||||
|
||||
// ----- 货权转移 -----
|
||||
func CreateOwnershipTransfer(db *gorm.DB, e *model.OwnershipTransfer) error {
|
||||
return db.Transaction(func(tx *gorm.DB) error {
|
||||
if err := tx.Create(e).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
// Set TransferID for BL items after main record is created
|
||||
for i := range e.BLItems {
|
||||
e.BLItems[i].TransferID = e.ID
|
||||
}
|
||||
if len(e.BLItems) > 0 {
|
||||
return tx.Create(&e.BLItems).Error
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
func ListOwnershipTransfer(db *gorm.DB, page, pageSize int, transferNumber, blNumber, status string) ([]model.OwnershipTransfer, int64, error) {
|
||||
var list []model.OwnershipTransfer
|
||||
q := db.Model(&model.OwnershipTransfer{})
|
||||
if transferNumber != "" {
|
||||
q = q.Where("transfer_number LIKE ?", "%"+transferNumber+"%")
|
||||
}
|
||||
if blNumber != "" {
|
||||
q = q.Where("id IN (SELECT transfer_id FROM ownership_transfer_bls WHERE bl_number LIKE ?)", "%"+blNumber+"%")
|
||||
}
|
||||
if status != "" {
|
||||
q = q.Where("status = ?", status)
|
||||
}
|
||||
var total int64
|
||||
if err := q.Count(&total).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
offset := (page - 1) * pageSize
|
||||
if offset < 0 {
|
||||
offset = 0
|
||||
}
|
||||
if pageSize <= 0 {
|
||||
pageSize = 10
|
||||
}
|
||||
err := q.Preload("Files").Preload("BLItems").Order("create_time DESC").Offset(offset).Limit(pageSize).Find(&list).Error
|
||||
return list, total, err
|
||||
}
|
||||
func GetOwnershipTransferByID(db *gorm.DB, id uint) (*model.OwnershipTransfer, error) {
|
||||
var e model.OwnershipTransfer
|
||||
if err := db.Preload("Files").Preload("BLItems").First(&e, id).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &e, nil
|
||||
}
|
||||
func UpdateOwnershipTransfer(db *gorm.DB, e *model.OwnershipTransfer) error {
|
||||
return db.Transaction(func(tx *gorm.DB) error {
|
||||
if err := tx.Save(e).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
// Replace BL items: delete old, insert new
|
||||
if err := tx.Where("transfer_id = ?", e.ID).Delete(&model.OwnershipTransferBL{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
for i := range e.BLItems {
|
||||
e.BLItems[i].ID = 0
|
||||
e.BLItems[i].TransferID = e.ID
|
||||
}
|
||||
if len(e.BLItems) > 0 {
|
||||
return tx.Create(&e.BLItems).Error
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
func DeleteOwnershipTransfer(db *gorm.DB, id uint) error {
|
||||
return db.Transaction(func(tx *gorm.DB) error {
|
||||
if err := tx.Where("transfer_id = ?", id).Delete(&model.OwnershipTransferFile{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tx.Where("transfer_id = ?", id).Delete(&model.OwnershipTransferBL{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return tx.Delete(&model.OwnershipTransfer{}, id).Error
|
||||
})
|
||||
}
|
||||
func CreateOwnershipTransferFiles(db *gorm.DB, files []model.OwnershipTransferFile) error {
|
||||
if len(files) == 0 {
|
||||
return nil
|
||||
}
|
||||
return db.Create(&files).Error
|
||||
}
|
||||
|
||||
// DeliveryRepository wraps *gorm.DB and exposes all delivery-related repository methods
|
||||
type DeliveryRepository struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewDeliveryRepository(db *gorm.DB) *DeliveryRepository {
|
||||
return &DeliveryRepository{db: db}
|
||||
}
|
||||
|
||||
func (r *DeliveryRepository) CreateDeliveryApply(e *model.DeliveryApply) error { return CreateDeliveryApply(r.db, e) }
|
||||
func (r *DeliveryRepository) ListDeliveryApply(page, pageSize int, orderNumber, blNumber, status string) ([]model.DeliveryApply, int64, error) {
|
||||
return ListDeliveryApply(r.db, page, pageSize, orderNumber, blNumber, status)
|
||||
}
|
||||
func (r *DeliveryRepository) GetDeliveryApplyByID(id uint) (*model.DeliveryApply, error) { return GetDeliveryApplyByID(r.db, id) }
|
||||
func (r *DeliveryRepository) UpdateDeliveryApply(e *model.DeliveryApply) error { return UpdateDeliveryApply(r.db, e) }
|
||||
func (r *DeliveryRepository) DeleteDeliveryApply(id uint) error { return DeleteDeliveryApply(r.db, id) }
|
||||
|
||||
func (r *DeliveryRepository) CreateDeliveryOutbound(e *model.DeliveryOutbound) error { return CreateDeliveryOutbound(r.db, e) }
|
||||
func (r *DeliveryRepository) ListDeliveryOutbound(page, pageSize int, outboundNumber, blNumber, status string) ([]model.DeliveryOutbound, int64, error) {
|
||||
return ListDeliveryOutbound(r.db, page, pageSize, outboundNumber, blNumber, status)
|
||||
}
|
||||
func (r *DeliveryRepository) GetDeliveryOutboundByID(id uint) (*model.DeliveryOutbound, error) { return GetDeliveryOutboundByID(r.db, id) }
|
||||
func (r *DeliveryRepository) UpdateDeliveryOutbound(e *model.DeliveryOutbound) error { return UpdateDeliveryOutbound(r.db, e) }
|
||||
func (r *DeliveryRepository) DeleteDeliveryOutbound(id uint) error { return DeleteDeliveryOutbound(r.db, id) }
|
||||
|
||||
func (r *DeliveryRepository) CreateWarehouse(e *model.Warehouse) error { return CreateWarehouse(r.db, e) }
|
||||
func (r *DeliveryRepository) ListWarehouses() ([]model.Warehouse, error) { return ListWarehouses(r.db) }
|
||||
func (r *DeliveryRepository) GetWarehouseByID(id uint) (*model.Warehouse, error) { return GetWarehouseByID(r.db, id) }
|
||||
func (r *DeliveryRepository) UpdateWarehouse(e *model.Warehouse) error { return UpdateWarehouse(r.db, e) }
|
||||
func (r *DeliveryRepository) DeleteWarehouse(id uint) error { return DeleteWarehouse(r.db, id) }
|
||||
|
||||
func (r *DeliveryRepository) CreateInventoryInbound(e *model.InventoryInbound) error { return CreateInventoryInbound(r.db, e) }
|
||||
func (r *DeliveryRepository) ListInventoryInbound(page, pageSize int, warehouse, commodity string) ([]model.InventoryInbound, int64, error) {
|
||||
return ListInventoryInbound(r.db, page, pageSize, warehouse, commodity)
|
||||
}
|
||||
func (r *DeliveryRepository) GetInventoryInboundByID(id uint) (*model.InventoryInbound, error) { return GetInventoryInboundByID(r.db, id) }
|
||||
func (r *DeliveryRepository) UpdateInventoryInbound(e *model.InventoryInbound) error { return UpdateInventoryInbound(r.db, e) }
|
||||
func (r *DeliveryRepository) DeleteInventoryInbound(id uint) error { return DeleteInventoryInbound(r.db, id) }
|
||||
|
||||
func (r *DeliveryRepository) CreateOwnershipTransfer(e *model.OwnershipTransfer) error { return CreateOwnershipTransfer(r.db, e) }
|
||||
func (r *DeliveryRepository) ListOwnershipTransfer(page, pageSize int, transferNumber, blNumber, status string) ([]model.OwnershipTransfer, int64, error) {
|
||||
return ListOwnershipTransfer(r.db, page, pageSize, transferNumber, blNumber, status)
|
||||
}
|
||||
func (r *DeliveryRepository) GetOwnershipTransferByID(id uint) (*model.OwnershipTransfer, error) { return GetOwnershipTransferByID(r.db, id) }
|
||||
func (r *DeliveryRepository) UpdateOwnershipTransfer(e *model.OwnershipTransfer) error { return UpdateOwnershipTransfer(r.db, e) }
|
||||
func (r *DeliveryRepository) DeleteOwnershipTransfer(id uint) error { return DeleteOwnershipTransfer(r.db, id) }
|
||||
func (r *DeliveryRepository) CreateOwnershipTransferFiles(files []model.OwnershipTransferFile) error {
|
||||
return CreateOwnershipTransferFiles(r.db, files)
|
||||
}
|
||||
|
||||
// DB returns the underlying *gorm.DB for complex queries (e.g. InventorySummary)
|
||||
func (r *DeliveryRepository) DB() *gorm.DB { return r.db }
|
||||
66
backend/internal/repository/user_repository.go
Executable file
66
backend/internal/repository/user_repository.go
Executable file
@@ -0,0 +1,66 @@
|
||||
// internal/repository/user_repository.go
|
||||
package repository
|
||||
|
||||
import (
|
||||
"github.com/rovina/zsp-backend/internal/model"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type UserRepository interface {
|
||||
Create(user *model.User) error
|
||||
FindByID(id uint) (*model.User, error)
|
||||
FindByEmail(email string) (*model.User, error)
|
||||
FindByUsername(username string) (*model.User, error)
|
||||
Update(user *model.User) error
|
||||
Delete(id uint) error
|
||||
}
|
||||
|
||||
type userRepository struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewUserRepository(db *gorm.DB) UserRepository {
|
||||
return &userRepository{db: db}
|
||||
}
|
||||
|
||||
func (r *userRepository) Create(user *model.User) error {
|
||||
return r.db.Create(user).Error
|
||||
}
|
||||
|
||||
func (r *userRepository) FindByID(id uint) (*model.User, error) {
|
||||
var user model.User
|
||||
err := r.db.First(&user, id).Error
|
||||
return &user, err
|
||||
}
|
||||
|
||||
func (r *userRepository) FindByUsername(username string) (*model.User, error) {
|
||||
var user model.User
|
||||
err := r.db.Where("username = ?", username).First(&user).Error
|
||||
if err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return &user, nil
|
||||
}
|
||||
func (r *userRepository) FindByEmail(email string) (*model.User, error) {
|
||||
var user model.User
|
||||
err := r.db.Where("email = ?", email).First(&user).Error
|
||||
if err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
func (r *userRepository) Update(user *model.User) error {
|
||||
return r.db.Save(user).Error
|
||||
}
|
||||
|
||||
func (r *userRepository) Delete(id uint) error {
|
||||
return r.db.Delete(&model.User{}, id).Error
|
||||
}
|
||||
129
backend/internal/repository/zsp_contract_repository.go
Executable file
129
backend/internal/repository/zsp_contract_repository.go
Executable file
@@ -0,0 +1,129 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"github.com/rovina/zsp-backend/internal/model"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type ZSPContractRepository interface {
|
||||
CreateFolder(folder *model.ZSPContractFolder) error
|
||||
GetFolders() ([]model.ZSPContractFolder, error)
|
||||
GetFolderById(folderID string) (*model.ZSPContractFolder, error)
|
||||
DeleteFolderById(folderID string) error
|
||||
UpdateFolder(folder *model.ZSPContractFolder) error
|
||||
|
||||
CreateContract(contract *model.ZSPContractFile) error
|
||||
GetContractById(contractID string) (*model.ZSPContractFile, error)
|
||||
ListContracts(folderID string) ([]model.ZSPContractFile, error)
|
||||
ListContractsWithDetails(folderID string) ([]model.ZSPContractFile, error)
|
||||
DeleteContractById(contractID string) error
|
||||
UpdateContract(contract *model.ZSPContractFile) error
|
||||
|
||||
// 合同详情相关
|
||||
CreateContractDetail(detail *model.ZSPContractDetail) error
|
||||
GetContractDetailById(detailID string) (*model.ZSPContractDetail, error)
|
||||
ListContractDetails(contractFileID string) ([]model.ZSPContractDetail, error)
|
||||
UpdateContractDetail(detail *model.ZSPContractDetail) error
|
||||
DeleteContractDetailById(detailID string) error
|
||||
BatchCreateContractDetails(details []model.ZSPContractDetail) error
|
||||
}
|
||||
|
||||
type zspContractRepository struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewZSPContractRepository(db *gorm.DB) ZSPContractRepository {
|
||||
return &zspContractRepository{db: db}
|
||||
}
|
||||
|
||||
func (r *zspContractRepository) CreateFolder(folder *model.ZSPContractFolder) error {
|
||||
// 这里可以根据实际需求创建一个 Folder 模型并保存到数据库
|
||||
return r.db.Create(folder).Error
|
||||
}
|
||||
|
||||
func (r *zspContractRepository) GetFolders() ([]model.ZSPContractFolder, error) {
|
||||
var folders []model.ZSPContractFolder
|
||||
err := r.db.Find(&folders).Error
|
||||
return folders, err
|
||||
}
|
||||
|
||||
func (r *zspContractRepository) GetFolderById(folderID string) (*model.ZSPContractFolder, error) {
|
||||
var folder model.ZSPContractFolder
|
||||
err := r.db.First(&folder, folderID).Error
|
||||
return &folder, err
|
||||
}
|
||||
|
||||
func (r *zspContractRepository) DeleteFolderById(folderID string) error {
|
||||
return r.db.Delete(&model.ZSPContractFolder{}, folderID).Error
|
||||
}
|
||||
|
||||
func (r *zspContractRepository) UpdateFolder(folder *model.ZSPContractFolder) error {
|
||||
return r.db.Save(folder).Error
|
||||
}
|
||||
|
||||
func (r *zspContractRepository) CreateContract(contract *model.ZSPContractFile) error {
|
||||
return r.db.Create(contract).Error
|
||||
}
|
||||
|
||||
func (r *zspContractRepository) GetContractById(contractID string) (*model.ZSPContractFile, error) {
|
||||
var contract model.ZSPContractFile
|
||||
err := r.db.First(&contract, contractID).Error
|
||||
return &contract, err
|
||||
}
|
||||
|
||||
func (r *zspContractRepository) ListContracts(folderID string) ([]model.ZSPContractFile, error) {
|
||||
var contracts []model.ZSPContractFile
|
||||
err := r.db.Where("folder_id = ?", folderID).Find(&contracts).Error
|
||||
return contracts, err
|
||||
}
|
||||
|
||||
func (r *zspContractRepository) DeleteContractById(contractID string) error {
|
||||
return r.db.Delete(&model.ZSPContractFile{}, contractID).Error
|
||||
}
|
||||
|
||||
func (r *zspContractRepository) UpdateContract(contract *model.ZSPContractFile) error {
|
||||
return r.db.Save(contract).Error
|
||||
}
|
||||
|
||||
func (r *zspContractRepository) ListContractsWithDetails(folderID string) ([]model.ZSPContractFile, error) {
|
||||
var contracts []model.ZSPContractFile
|
||||
query := r.db.Preload("Details")
|
||||
if folderID != "" {
|
||||
query = query.Where("folder_id = ?", folderID)
|
||||
}
|
||||
err := query.Find(&contracts).Error
|
||||
return contracts, err
|
||||
}
|
||||
|
||||
// ============ 合同详情相关方法 ============
|
||||
|
||||
func (r *zspContractRepository) CreateContractDetail(detail *model.ZSPContractDetail) error {
|
||||
return r.db.Create(detail).Error
|
||||
}
|
||||
|
||||
func (r *zspContractRepository) GetContractDetailById(detailID string) (*model.ZSPContractDetail, error) {
|
||||
var detail model.ZSPContractDetail
|
||||
err := r.db.First(&detail, detailID).Error
|
||||
return &detail, err
|
||||
}
|
||||
|
||||
func (r *zspContractRepository) ListContractDetails(contractFileID string) ([]model.ZSPContractDetail, error) {
|
||||
var details []model.ZSPContractDetail
|
||||
err := r.db.Where("contract_file_id = ?", contractFileID).Find(&details).Error
|
||||
return details, err
|
||||
}
|
||||
|
||||
func (r *zspContractRepository) UpdateContractDetail(detail *model.ZSPContractDetail) error {
|
||||
return r.db.Save(detail).Error
|
||||
}
|
||||
|
||||
func (r *zspContractRepository) DeleteContractDetailById(detailID string) error {
|
||||
return r.db.Delete(&model.ZSPContractDetail{}, detailID).Error
|
||||
}
|
||||
|
||||
func (r *zspContractRepository) BatchCreateContractDetails(details []model.ZSPContractDetail) error {
|
||||
if len(details) == 0 {
|
||||
return nil
|
||||
}
|
||||
return r.db.Create(&details).Error
|
||||
}
|
||||
182
backend/internal/repository/zsp_finances.go
Executable file
182
backend/internal/repository/zsp_finances.go
Executable file
@@ -0,0 +1,182 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"github.com/rovina/zsp-backend/internal/model"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type ZSPFinancesRepository interface {
|
||||
CreateFreightCharge(charge *model.ZSPFreightCharges) error
|
||||
GetFreightCharge(id string) (*model.ZSPFreightCharges, error)
|
||||
ListFreightCharges() ([]model.ZSPFreightCharges, error)
|
||||
UpdateFreightCharge(charge *model.ZSPFreightCharges) error
|
||||
DeleteFreightCharge(id string) error
|
||||
|
||||
CreateMiscCharge(charge *model.ZSPMiscCharges) error
|
||||
GetMiscCharge(id string) (*model.ZSPMiscCharges, error)
|
||||
ListMiscCharges() ([]model.ZSPMiscCharges, error)
|
||||
UpdateMiscCharge(charge *model.ZSPMiscCharges) error
|
||||
DeleteMiscCharge(id string) error
|
||||
|
||||
CreateServiceFee(fee *model.ZSPServiceFees) error
|
||||
GetServiceFee(id string) (*model.ZSPServiceFees, error)
|
||||
ListServiceFees() ([]model.ZSPServiceFees, error)
|
||||
UpdateServiceFee(fee *model.ZSPServiceFees) error
|
||||
DeleteServiceFee(id string) error
|
||||
|
||||
CreateCostBreakdown(breakdown *model.ZSPCostBreakdown) error
|
||||
GetCostBreakdown(id string) (*model.ZSPCostBreakdown, error)
|
||||
ListCostBreakdowns(businessID string) ([]model.ZSPCostBreakdown, error)
|
||||
UpdateCostBreakdown(breakdown *model.ZSPCostBreakdown) error
|
||||
DeleteCostBreakdown(id string) error
|
||||
|
||||
CreateProfitRecord(record *model.ZSPProfitRecord) error
|
||||
GetProfitRecord(id string) (*model.ZSPProfitRecord, error)
|
||||
ListProfitRecords(filters map[string]string) ([]model.ZSPProfitRecord, error)
|
||||
UpdateProfitRecord(id string, req *model.ZSPProfitRecord) error
|
||||
DeleteProfitRecord(id string) error
|
||||
}
|
||||
|
||||
type zspFinancesRepository struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewZSPFinancesRepository(db *gorm.DB) ZSPFinancesRepository {
|
||||
return &zspFinancesRepository{db: db}
|
||||
}
|
||||
|
||||
// ============ 货代费用 ============
|
||||
func (r *zspFinancesRepository) CreateFreightCharge(charge *model.ZSPFreightCharges) error {
|
||||
return r.db.Create(charge).Error
|
||||
}
|
||||
|
||||
func (r *zspFinancesRepository) GetFreightCharge(id string) (*model.ZSPFreightCharges, error) {
|
||||
var charge model.ZSPFreightCharges
|
||||
err := r.db.First(&charge, id).Error
|
||||
return &charge, err
|
||||
}
|
||||
|
||||
func (r *zspFinancesRepository) ListFreightCharges() ([]model.ZSPFreightCharges, error) {
|
||||
var charges []model.ZSPFreightCharges
|
||||
err := r.db.Order("create_time desc").Find(&charges).Error
|
||||
return charges, err
|
||||
}
|
||||
|
||||
func (r *zspFinancesRepository) UpdateFreightCharge(charge *model.ZSPFreightCharges) error {
|
||||
return r.db.Save(charge).Error
|
||||
}
|
||||
|
||||
func (r *zspFinancesRepository) DeleteFreightCharge(id string) error {
|
||||
return r.db.Delete(&model.ZSPFreightCharges{}, id).Error
|
||||
}
|
||||
|
||||
// ============ 杂费 ============
|
||||
func (r *zspFinancesRepository) CreateMiscCharge(charge *model.ZSPMiscCharges) error {
|
||||
return r.db.Create(charge).Error
|
||||
}
|
||||
|
||||
func (r *zspFinancesRepository) GetMiscCharge(id string) (*model.ZSPMiscCharges, error) {
|
||||
var charge model.ZSPMiscCharges
|
||||
err := r.db.First(&charge, id).Error
|
||||
return &charge, err
|
||||
}
|
||||
|
||||
func (r *zspFinancesRepository) ListMiscCharges() ([]model.ZSPMiscCharges, error) {
|
||||
var charges []model.ZSPMiscCharges
|
||||
err := r.db.Order("create_time desc").Find(&charges).Error
|
||||
return charges, err
|
||||
}
|
||||
|
||||
func (r *zspFinancesRepository) UpdateMiscCharge(charge *model.ZSPMiscCharges) error {
|
||||
return r.db.Save(charge).Error
|
||||
}
|
||||
|
||||
func (r *zspFinancesRepository) DeleteMiscCharge(id string) error {
|
||||
return r.db.Delete(&model.ZSPMiscCharges{}, id).Error
|
||||
}
|
||||
|
||||
// ============ 服务费 ============
|
||||
func (r *zspFinancesRepository) CreateServiceFee(fee *model.ZSPServiceFees) error {
|
||||
return r.db.Create(fee).Error
|
||||
}
|
||||
|
||||
func (r *zspFinancesRepository) GetServiceFee(id string) (*model.ZSPServiceFees, error) {
|
||||
var fee model.ZSPServiceFees
|
||||
err := r.db.First(&fee, id).Error
|
||||
return &fee, err
|
||||
}
|
||||
|
||||
func (r *zspFinancesRepository) ListServiceFees() ([]model.ZSPServiceFees, error) {
|
||||
var fees []model.ZSPServiceFees
|
||||
err := r.db.Order("create_time desc").Find(&fees).Error
|
||||
return fees, err
|
||||
}
|
||||
|
||||
func (r *zspFinancesRepository) UpdateServiceFee(fee *model.ZSPServiceFees) error {
|
||||
return r.db.Save(fee).Error
|
||||
}
|
||||
|
||||
func (r *zspFinancesRepository) DeleteServiceFee(id string) error {
|
||||
return r.db.Delete(&model.ZSPServiceFees{}, id).Error
|
||||
}
|
||||
|
||||
// ============ 成本构成 ============
|
||||
func (r *zspFinancesRepository) CreateCostBreakdown(breakdown *model.ZSPCostBreakdown) error {
|
||||
return r.db.Create(breakdown).Error
|
||||
}
|
||||
|
||||
func (r *zspFinancesRepository) GetCostBreakdown(id string) (*model.ZSPCostBreakdown, error) {
|
||||
var breakdown model.ZSPCostBreakdown
|
||||
err := r.db.First(&breakdown, id).Error
|
||||
return &breakdown, err
|
||||
}
|
||||
|
||||
func (r *zspFinancesRepository) ListCostBreakdowns(businessID string) ([]model.ZSPCostBreakdown, error) {
|
||||
var breakdowns []model.ZSPCostBreakdown
|
||||
query := r.db.Model(&model.ZSPCostBreakdown{})
|
||||
if businessID != "" {
|
||||
query = query.Where("business_id = ?", businessID)
|
||||
}
|
||||
err := query.Order("create_time desc").Find(&breakdowns).Error
|
||||
return breakdowns, err
|
||||
}
|
||||
|
||||
func (r *zspFinancesRepository) UpdateCostBreakdown(breakdown *model.ZSPCostBreakdown) error {
|
||||
return r.db.Save(breakdown).Error
|
||||
}
|
||||
|
||||
func (r *zspFinancesRepository) DeleteCostBreakdown(id string) error {
|
||||
return r.db.Delete(&model.ZSPCostBreakdown{}, id).Error
|
||||
}
|
||||
|
||||
// ============ 利润记录 ============
|
||||
func (r *zspFinancesRepository) CreateProfitRecord(record *model.ZSPProfitRecord) error {
|
||||
return r.db.Create(record).Error
|
||||
}
|
||||
|
||||
func (r *zspFinancesRepository) GetProfitRecord(id string) (*model.ZSPProfitRecord, error) {
|
||||
var record model.ZSPProfitRecord
|
||||
err := r.db.First(&record, id).Error
|
||||
return &record, err
|
||||
}
|
||||
|
||||
func (r *zspFinancesRepository) ListProfitRecords(filters map[string]string) ([]model.ZSPProfitRecord, error) {
|
||||
var records []model.ZSPProfitRecord
|
||||
query := r.db.Model(&model.ZSPProfitRecord{})
|
||||
if filters["businessId"] != "" {
|
||||
query = query.Where("business_id = ?", filters["businessId"])
|
||||
}
|
||||
if filters["businessType"] != "" {
|
||||
query = query.Where("business_type = ?", filters["businessType"])
|
||||
}
|
||||
err := query.Order("create_time desc").Find(&records).Error
|
||||
return records, err
|
||||
}
|
||||
|
||||
func (r *zspFinancesRepository) UpdateProfitRecord(id string, req *model.ZSPProfitRecord) error {
|
||||
return r.db.Save(req).Error
|
||||
}
|
||||
|
||||
func (r *zspFinancesRepository) DeleteProfitRecord(id string) error {
|
||||
return r.db.Delete(&model.ZSPProfitRecord{}, id).Error
|
||||
}
|
||||
190
backend/internal/server/route.go
Executable file
190
backend/internal/server/route.go
Executable file
@@ -0,0 +1,190 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
|
||||
"github.com/gin-contrib/cors"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/rovina/zsp-backend/internal/handler"
|
||||
)
|
||||
|
||||
func SetupRouter(userHandler *handler.UserHandler,
|
||||
contractHandler *handler.ContractHandler,
|
||||
zspContractHandler *handler.ZSPContractHandler,
|
||||
zspFinancesHandler *handler.ZSPFinancesHandler,
|
||||
companyHandler *handler.CompanyHandler,
|
||||
deliveryHandler *handler.DeliveryHandler,
|
||||
authMiddleware gin.HandlerFunc,
|
||||
) *gin.Engine {
|
||||
router := gin.Default()
|
||||
router.Use(gin.Logger())
|
||||
router.Use(gin.Recovery())
|
||||
// 允许跨域,并放行 Authorization 等请求头(解决前端直连 8080 时的 CORS 报错)
|
||||
router.Use(cors.New(cors.Config{
|
||||
AllowOrigins: []string{"*"},
|
||||
AllowMethods: []string{"GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS", "HEAD"},
|
||||
AllowHeaders: []string{"Origin", "Content-Type", "Accept", "Authorization", "X-Requested-With"},
|
||||
ExposeHeaders: []string{"Content-Length"},
|
||||
AllowCredentials: false,
|
||||
MaxAge: 12 * 3600,
|
||||
}))
|
||||
// user routes
|
||||
api := router.Group("/api")
|
||||
{
|
||||
router.GET("/health", func(c *gin.Context) { c.JSON(200, gin.H{"status": "ok"}) })
|
||||
auth := api.Group("/auth")
|
||||
{
|
||||
auth.POST("/register", userHandler.Register)
|
||||
auth.POST("/login", userHandler.Login)
|
||||
}
|
||||
// 静态文件资源(公开访问,供前端预览/下载)
|
||||
api.Static("/contract-files", filepath.Join(".", "data", "contract"))
|
||||
api.Static("/zsp-contract-files", filepath.Join(".", "data", "zsp-contract"))
|
||||
api.Static("/company-files", filepath.Join(".", "data", "company"))
|
||||
|
||||
// 以下所有路由需要 JWT 认证
|
||||
authorized := api.Group("/")
|
||||
authorized.Use(authMiddleware)
|
||||
{
|
||||
users := authorized.Group("/users")
|
||||
{
|
||||
users.GET("/:id", userHandler.GetUser)
|
||||
}
|
||||
|
||||
contracts := authorized.Group("/contract")
|
||||
{
|
||||
contracts.GET("", contractHandler.List)
|
||||
contracts.POST("/upload", contractHandler.Upload)
|
||||
contracts.POST("/batch-upload", contractHandler.BatchUpload)
|
||||
contracts.GET("/folders", contractHandler.ListFolders)
|
||||
contracts.POST("/folders", contractHandler.CreateFolder)
|
||||
contracts.GET("/folders/:id", contractHandler.GetFolder)
|
||||
contracts.PUT("/folders/:id", contractHandler.UpdateFolder)
|
||||
contracts.DELETE("/folders/:id", contractHandler.DeleteFolder)
|
||||
contracts.GET("/batches", contractHandler.ListBatches)
|
||||
contracts.POST("/batches", contractHandler.CreateBatch)
|
||||
contracts.GET("/batches/:id", contractHandler.GetBatch)
|
||||
contracts.DELETE("/batches/:id", contractHandler.DeleteBatch)
|
||||
contracts.POST("/:id/update-files", contractHandler.UpdateWithFiles)
|
||||
contracts.GET("/:id", contractHandler.Get)
|
||||
contracts.PUT("/:id", contractHandler.Update)
|
||||
contracts.DELETE("/:id", contractHandler.Delete)
|
||||
}
|
||||
|
||||
zspContracts := authorized.Group("/zsp-contract")
|
||||
{
|
||||
zspContracts.POST("/folders", zspContractHandler.CreateFolder)
|
||||
zspContracts.GET("/folders", zspContractHandler.GetFolders)
|
||||
zspContracts.GET("/folders/:id", zspContractHandler.GetFolder)
|
||||
zspContracts.DELETE("/folders/:id", zspContractHandler.DeleteFolder)
|
||||
zspContracts.PUT("/folders/:id", zspContractHandler.UpdateFolder)
|
||||
|
||||
zspContracts.POST("/contracts", zspContractHandler.UploadContract)
|
||||
zspContracts.GET("/contracts/:id", zspContractHandler.GetContractWithDetails)
|
||||
zspContracts.GET("/contracts", zspContractHandler.ListContractsWithDetails)
|
||||
zspContracts.PUT("/contracts/:id", zspContractHandler.UpdateContract)
|
||||
zspContracts.DELETE("/contracts/:id", zspContractHandler.DeleteContract)
|
||||
|
||||
zspContracts.POST("/contracts/:id/details", zspContractHandler.CreateContractDetail)
|
||||
zspContracts.GET("/contracts/:id/details", zspContractHandler.ListContractDetails)
|
||||
zspContracts.POST("/details/batch", zspContractHandler.BatchCreateContractDetails)
|
||||
zspContracts.GET("/details/:id", zspContractHandler.GetContractDetail)
|
||||
zspContracts.PUT("/details/:id", zspContractHandler.UpdateContractDetail)
|
||||
zspContracts.DELETE("/details/:id", zspContractHandler.DeleteContractDetail)
|
||||
zspContracts.POST("/contracts/:id/details/import", zspContractHandler.ImportContractDetailsFromExcel)
|
||||
zspContracts.GET("/contracts/:id/details/export", zspContractHandler.ExportContractDetailsToExcel)
|
||||
}
|
||||
|
||||
company := authorized.Group("/company")
|
||||
{
|
||||
company.GET("/folders", companyHandler.ListFolders)
|
||||
company.POST("/folders", companyHandler.CreateFolder)
|
||||
company.PUT("/folders/:id", companyHandler.UpdateFolder)
|
||||
company.DELETE("/folders/:id", companyHandler.DeleteFolder)
|
||||
company.GET("/files", companyHandler.ListFiles)
|
||||
company.POST("/files/upload", companyHandler.UploadFiles)
|
||||
company.PUT("/files/:id", companyHandler.UpdateFile)
|
||||
company.DELETE("/files/:id", companyHandler.DeleteFile)
|
||||
}
|
||||
|
||||
// 提货管理 (RESTful)
|
||||
applyDelivery := authorized.Group("/apply-delivery")
|
||||
{
|
||||
applyDelivery.GET("", deliveryHandler.ListApply)
|
||||
applyDelivery.GET("/:id", deliveryHandler.GetApply)
|
||||
applyDelivery.POST("", deliveryHandler.CreateApply)
|
||||
applyDelivery.PUT("/:id", deliveryHandler.UpdateApply)
|
||||
applyDelivery.PUT("/:id/cancel", deliveryHandler.CancelApply)
|
||||
applyDelivery.DELETE("/:id", deliveryHandler.DeleteApply)
|
||||
}
|
||||
deliveryDetails := authorized.Group("/delivery-details")
|
||||
{
|
||||
deliveryDetails.GET("", deliveryHandler.ListOutbound)
|
||||
deliveryDetails.GET("/:id", deliveryHandler.GetOutbound)
|
||||
deliveryDetails.POST("", deliveryHandler.CreateOutbound)
|
||||
deliveryDetails.PUT("/:id", deliveryHandler.UpdateOutbound)
|
||||
deliveryDetails.DELETE("/:id", deliveryHandler.DeleteOutbound)
|
||||
}
|
||||
inventory := authorized.Group("/inventory")
|
||||
{
|
||||
inventory.GET("/warehouses", deliveryHandler.ListWarehouses)
|
||||
inventory.POST("/warehouses", deliveryHandler.CreateWarehouse)
|
||||
inventory.PUT("/warehouses/:id", deliveryHandler.UpdateWarehouse)
|
||||
inventory.DELETE("/warehouses/:id", deliveryHandler.DeleteWarehouse)
|
||||
inventory.GET("/inbound", deliveryHandler.ListInbound)
|
||||
inventory.POST("/inbound", deliveryHandler.CreateInbound)
|
||||
inventory.PUT("/inbound/:id", deliveryHandler.UpdateInbound)
|
||||
inventory.DELETE("/inbound/:id", deliveryHandler.DeleteInbound)
|
||||
inventory.GET("/summary", deliveryHandler.InventorySummary)
|
||||
}
|
||||
ownershipTransfer := authorized.Group("/ownership-transfer")
|
||||
{
|
||||
ownershipTransfer.GET("", deliveryHandler.ListOwnership)
|
||||
ownershipTransfer.GET("/:id", deliveryHandler.GetOwnership)
|
||||
ownershipTransfer.POST("", deliveryHandler.CreateOwnership)
|
||||
ownershipTransfer.POST("/with-files", deliveryHandler.CreateOwnershipWithFiles)
|
||||
ownershipTransfer.DELETE("/:id", deliveryHandler.DeleteOwnership)
|
||||
}
|
||||
|
||||
zspFinances := authorized.Group("/zsp-finances")
|
||||
{
|
||||
// 货代费用
|
||||
zspFinances.POST("/freight-charges", zspFinancesHandler.CreateFreightCharge)
|
||||
zspFinances.GET("/freight-charges/:id", zspFinancesHandler.GetFreightCharge)
|
||||
zspFinances.GET("/freight-charges", zspFinancesHandler.ListFreightCharges)
|
||||
zspFinances.PUT("/freight-charges/:id", zspFinancesHandler.UpdateFreightCharge)
|
||||
zspFinances.DELETE("/freight-charges/:id", zspFinancesHandler.DeleteFreightCharge)
|
||||
|
||||
// 杂费
|
||||
zspFinances.POST("/misc-charges", zspFinancesHandler.CreateMiscCharge)
|
||||
zspFinances.GET("/misc-charges/:id", zspFinancesHandler.GetMiscCharge)
|
||||
zspFinances.GET("/misc-charges", zspFinancesHandler.ListMiscCharges)
|
||||
zspFinances.PUT("/misc-charges/:id", zspFinancesHandler.UpdateMiscCharge)
|
||||
zspFinances.DELETE("/misc-charges/:id", zspFinancesHandler.DeleteMiscCharge)
|
||||
|
||||
// 服务费
|
||||
zspFinances.POST("/service-fees", zspFinancesHandler.CreateServiceFee)
|
||||
zspFinances.GET("/service-fees/:id", zspFinancesHandler.GetServiceFee)
|
||||
zspFinances.GET("/service-fees", zspFinancesHandler.ListServiceFees)
|
||||
zspFinances.PUT("/service-fees/:id", zspFinancesHandler.UpdateServiceFee)
|
||||
zspFinances.DELETE("/service-fees/:id", zspFinancesHandler.DeleteServiceFee)
|
||||
|
||||
// 成本构成
|
||||
zspFinances.POST("/cost-breakdowns", zspFinancesHandler.CreateCostBreakdown)
|
||||
zspFinances.GET("/cost-breakdowns/:id", zspFinancesHandler.GetCostBreakdown)
|
||||
zspFinances.GET("/cost-breakdowns", zspFinancesHandler.ListCostBreakdowns)
|
||||
zspFinances.PUT("/cost-breakdowns/:id", zspFinancesHandler.UpdateCostBreakdown)
|
||||
zspFinances.DELETE("/cost-breakdowns/:id", zspFinancesHandler.DeleteCostBreakdown)
|
||||
|
||||
// 利润记录
|
||||
zspFinances.POST("/profit-records", zspFinancesHandler.CreateProfitRecord)
|
||||
zspFinances.GET("/profit-records/:id", zspFinancesHandler.GetProfitRecord)
|
||||
zspFinances.GET("/profit-records", zspFinancesHandler.ListProfitRecords)
|
||||
zspFinances.PUT("/profit-records/:id", zspFinancesHandler.UpdateProfitRecord)
|
||||
zspFinances.DELETE("/profit-records/:id", zspFinancesHandler.DeleteProfitRecord)
|
||||
}
|
||||
} // end authorized
|
||||
|
||||
} // end api
|
||||
return router
|
||||
}
|
||||
156
backend/internal/service/company_service.go
Executable file
156
backend/internal/service/company_service.go
Executable file
@@ -0,0 +1,156 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"mime/multipart"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/rovina/zsp-backend/internal/model"
|
||||
"github.com/rovina/zsp-backend/internal/repository"
|
||||
"github.com/rovina/zsp-backend/pkg/utils"
|
||||
)
|
||||
|
||||
type CompanyService interface {
|
||||
CreateFolder(name, description, category string) (*model.CompanyFolder, error)
|
||||
ListFolders() ([]model.CompanyFolder, error)
|
||||
GetFolderByID(id uint) (*model.CompanyFolder, error)
|
||||
UpdateFolder(id uint, name, description, category string) error
|
||||
DeleteFolder(id uint) error
|
||||
|
||||
UploadFiles(folderID *uint, companyName, fileCategory, expireDate, description string, files []*multipart.FileHeader) (int, error)
|
||||
ListFiles(folderID *uint) ([]model.CompanyFile, error)
|
||||
GetFileByID(id uint) (*model.CompanyFile, error)
|
||||
UpdateFile(id uint, companyName, fileCategory, expireDate, description string, folderID *uint) error
|
||||
DeleteFile(id uint) error
|
||||
}
|
||||
|
||||
type companyService struct {
|
||||
repo repository.CompanyRepository
|
||||
}
|
||||
|
||||
func NewCompanyService(repo repository.CompanyRepository) CompanyService {
|
||||
return &companyService{repo: repo}
|
||||
}
|
||||
|
||||
func (s *companyService) CreateFolder(name, description, category string) (*model.CompanyFolder, error) {
|
||||
f := &model.CompanyFolder{
|
||||
Name: name,
|
||||
Description: description,
|
||||
Category: category,
|
||||
CreateTime: time.Now(),
|
||||
UpdateTime: time.Now(),
|
||||
}
|
||||
if err := s.repo.CreateFolder(f); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return f, nil
|
||||
}
|
||||
|
||||
func (s *companyService) ListFolders() ([]model.CompanyFolder, error) {
|
||||
return s.repo.ListFolders()
|
||||
}
|
||||
|
||||
func (s *companyService) GetFolderByID(id uint) (*model.CompanyFolder, error) {
|
||||
return s.repo.GetFolderByID(id)
|
||||
}
|
||||
|
||||
func (s *companyService) UpdateFolder(id uint, name, description, category string) error {
|
||||
f, err := s.repo.GetFolderByID(id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
f.Name = name
|
||||
f.Description = description
|
||||
f.Category = category
|
||||
f.UpdateTime = time.Now()
|
||||
return s.repo.UpdateFolder(f)
|
||||
}
|
||||
|
||||
func (s *companyService) DeleteFolder(id uint) error {
|
||||
return s.repo.DeleteFolder(id)
|
||||
}
|
||||
|
||||
func (s *companyService) UploadFiles(folderID *uint, companyName, fileCategory, expireDate, description string, files []*multipart.FileHeader) (int, error) {
|
||||
basePath := "./data/company/files"
|
||||
if err := os.MkdirAll(basePath, os.ModePerm); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
var expire *time.Time
|
||||
if expireDate != "" {
|
||||
if t, err := time.Parse("2006-01-02", expireDate); err == nil {
|
||||
expire = &t
|
||||
}
|
||||
}
|
||||
|
||||
count := 0
|
||||
for _, fh := range files {
|
||||
filename := fmt.Sprintf("%d_%s", time.Now().UnixNano(), fh.Filename)
|
||||
savePath := filepath.Join(basePath, filename)
|
||||
if err := utils.SaveUploadedFile(fh, savePath); err != nil {
|
||||
return count, err
|
||||
}
|
||||
|
||||
ext := filepath.Ext(fh.Filename)
|
||||
if len(ext) > 0 {
|
||||
ext = ext[1:]
|
||||
}
|
||||
|
||||
file := &model.CompanyFile{
|
||||
FolderID: 0,
|
||||
FileName: fh.Filename,
|
||||
FileType: ext,
|
||||
FileSize: fh.Size,
|
||||
UploadTime: time.Now(),
|
||||
CompanyName: companyName,
|
||||
FileCategory: fileCategory,
|
||||
ExpireDate: expire,
|
||||
Description: description,
|
||||
FileURL: "files/" + filename,
|
||||
}
|
||||
if folderID != nil && *folderID > 0 {
|
||||
file.FolderID = *folderID
|
||||
}
|
||||
|
||||
if err := s.repo.CreateFile(file); err != nil {
|
||||
return count, err
|
||||
}
|
||||
count++
|
||||
}
|
||||
return count, nil
|
||||
}
|
||||
|
||||
func (s *companyService) ListFiles(folderID *uint) ([]model.CompanyFile, error) {
|
||||
return s.repo.ListFiles(folderID)
|
||||
}
|
||||
|
||||
func (s *companyService) GetFileByID(id uint) (*model.CompanyFile, error) {
|
||||
return s.repo.GetFileByID(id)
|
||||
}
|
||||
|
||||
func (s *companyService) UpdateFile(id uint, companyName, fileCategory, expireDate, description string, folderID *uint) error {
|
||||
f, err := s.repo.GetFileByID(id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
f.CompanyName = companyName
|
||||
f.FileCategory = fileCategory
|
||||
f.Description = description
|
||||
if expireDate != "" {
|
||||
if t, err := time.Parse("2006-01-02", expireDate); err == nil {
|
||||
f.ExpireDate = &t
|
||||
}
|
||||
} else {
|
||||
f.ExpireDate = nil
|
||||
}
|
||||
if folderID != nil {
|
||||
f.FolderID = *folderID
|
||||
}
|
||||
return s.repo.UpdateFile(f)
|
||||
}
|
||||
|
||||
func (s *companyService) DeleteFile(id uint) error {
|
||||
return s.repo.DeleteFile(id)
|
||||
}
|
||||
49
backend/internal/service/contract_batch_service.go
Executable file
49
backend/internal/service/contract_batch_service.go
Executable file
@@ -0,0 +1,49 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/rovina/zsp-backend/internal/model"
|
||||
"github.com/rovina/zsp-backend/internal/repository"
|
||||
)
|
||||
|
||||
type ContractBatchService interface {
|
||||
List(folderID *uint) ([]model.ContractBatch, error)
|
||||
GetByID(id uint) (*model.ContractBatch, error)
|
||||
Create(folderID uint, batchName, remark string, files []model.ContractBatchFile) (*model.ContractBatch, error)
|
||||
Delete(id uint) error
|
||||
}
|
||||
|
||||
type contractBatchService struct {
|
||||
repo repository.ContractBatchRepository
|
||||
}
|
||||
|
||||
func NewContractBatchService(repo repository.ContractBatchRepository) ContractBatchService {
|
||||
return &contractBatchService{repo: repo}
|
||||
}
|
||||
|
||||
func (s *contractBatchService) List(folderID *uint) ([]model.ContractBatch, error) {
|
||||
return s.repo.List(folderID)
|
||||
}
|
||||
|
||||
func (s *contractBatchService) GetByID(id uint) (*model.ContractBatch, error) {
|
||||
return s.repo.GetByID(id)
|
||||
}
|
||||
|
||||
func (s *contractBatchService) Create(folderID uint, batchName, remark string, files []model.ContractBatchFile) (*model.ContractBatch, error) {
|
||||
b := &model.ContractBatch{
|
||||
FolderID: folderID,
|
||||
BatchName: batchName,
|
||||
Remark: remark,
|
||||
CreateTime: time.Now(),
|
||||
UpdateTime: time.Now(),
|
||||
}
|
||||
if err := s.repo.Create(b, files); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return s.repo.GetByID(b.ID)
|
||||
}
|
||||
|
||||
func (s *contractBatchService) Delete(id uint) error {
|
||||
return s.repo.Delete(id)
|
||||
}
|
||||
60
backend/internal/service/contract_folder_service.go
Executable file
60
backend/internal/service/contract_folder_service.go
Executable file
@@ -0,0 +1,60 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/rovina/zsp-backend/internal/model"
|
||||
"github.com/rovina/zsp-backend/internal/repository"
|
||||
)
|
||||
|
||||
type ContractFolderService interface {
|
||||
List() ([]model.ContractFolder, error)
|
||||
GetByID(id uint) (*model.ContractFolder, error)
|
||||
Create(name, description string) (*model.ContractFolder, error)
|
||||
Update(id uint, name, description string) error
|
||||
Delete(id uint) error
|
||||
}
|
||||
|
||||
type contractFolderService struct {
|
||||
repo repository.ContractFolderRepository
|
||||
}
|
||||
|
||||
func NewContractFolderService(repo repository.ContractFolderRepository) ContractFolderService {
|
||||
return &contractFolderService{repo: repo}
|
||||
}
|
||||
|
||||
func (s *contractFolderService) List() ([]model.ContractFolder, error) {
|
||||
return s.repo.List()
|
||||
}
|
||||
|
||||
func (s *contractFolderService) GetByID(id uint) (*model.ContractFolder, error) {
|
||||
return s.repo.GetByID(id)
|
||||
}
|
||||
|
||||
func (s *contractFolderService) Create(name, description string) (*model.ContractFolder, error) {
|
||||
f := &model.ContractFolder{
|
||||
Name: name,
|
||||
Description: description,
|
||||
CreateTime: time.Now(),
|
||||
UpdateTime: time.Now(),
|
||||
}
|
||||
if err := s.repo.Create(f); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return f, nil
|
||||
}
|
||||
|
||||
func (s *contractFolderService) Update(id uint, name, description string) error {
|
||||
f, err := s.repo.GetByID(id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
f.Name = name
|
||||
f.Description = description
|
||||
f.UpdateTime = time.Now()
|
||||
return s.repo.Update(f)
|
||||
}
|
||||
|
||||
func (s *contractFolderService) Delete(id uint) error {
|
||||
return s.repo.Delete(id)
|
||||
}
|
||||
152
backend/internal/service/contract_service.go
Executable file
152
backend/internal/service/contract_service.go
Executable file
@@ -0,0 +1,152 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"mime/multipart"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/rovina/zsp-backend/internal/model"
|
||||
"github.com/rovina/zsp-backend/internal/repository"
|
||||
)
|
||||
|
||||
type ContractService interface {
|
||||
CreateContract(contract *model.Contract, files []model.ContractFile) error
|
||||
SaveFiles(files []model.ContractFile) error
|
||||
BatchSaveFiles(
|
||||
basePath string,
|
||||
files []*multipart.FileHeader,
|
||||
) (int, error)
|
||||
List(contractType string) ([]model.Contract, error)
|
||||
GetByID(id uint) (*model.Contract, error)
|
||||
Update(contract *model.Contract) error
|
||||
UpdateWithFiles(id uint, keepFileIDs []uint, newFileModels []model.ContractFile, form map[string]string) error
|
||||
Delete(id uint) error
|
||||
}
|
||||
|
||||
type contractService struct {
|
||||
repo repository.ContractRepository
|
||||
}
|
||||
|
||||
func NewContractService(repo repository.ContractRepository) (ContractService, error) {
|
||||
return &contractService{repo: repo}, nil
|
||||
}
|
||||
|
||||
func (s *contractService) CreateContract(contract *model.Contract, files []model.ContractFile) error {
|
||||
return s.repo.CreateWithFiles(contract, files)
|
||||
}
|
||||
|
||||
func (s *contractService) SaveFiles(files []model.ContractFile) error {
|
||||
return s.repo.CreateFiles(files)
|
||||
}
|
||||
|
||||
func (s *contractService) BatchSaveFiles(
|
||||
basePath string,
|
||||
files []*multipart.FileHeader,
|
||||
) (int, error) {
|
||||
|
||||
// 相对路径根目录
|
||||
root := filepath.Join("./data/contract/batch", basePath)
|
||||
|
||||
if err := os.MkdirAll(root, os.ModePerm); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
count := 0
|
||||
|
||||
for _, file := range files {
|
||||
filename := fmt.Sprintf("%d_%s", time.Now().UnixNano(), file.Filename)
|
||||
savePath := filepath.Join(root, filename)
|
||||
|
||||
if err := saveUploadedFile(file, savePath); err != nil {
|
||||
return count, err
|
||||
}
|
||||
|
||||
count++
|
||||
}
|
||||
|
||||
return count, nil
|
||||
}
|
||||
|
||||
func (s *contractService) List(contractType string) ([]model.Contract, error) {
|
||||
return s.repo.List(contractType)
|
||||
}
|
||||
|
||||
func (s *contractService) GetByID(id uint) (*model.Contract, error) {
|
||||
return s.repo.GetByID(id)
|
||||
}
|
||||
|
||||
func (s *contractService) Update(contract *model.Contract) error {
|
||||
return s.repo.Update(contract)
|
||||
}
|
||||
|
||||
func (s *contractService) UpdateWithFiles(id uint, keepFileIDs []uint, newFileModels []model.ContractFile, form map[string]string) error {
|
||||
contract, err := s.repo.GetByID(id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// 更新合同元信息
|
||||
get := func(k string) string { return form[k] }
|
||||
if v := get("name"); v != "" {
|
||||
contract.Name = v
|
||||
}
|
||||
if v := get("contractType"); v != "" {
|
||||
contract.ContractType = v
|
||||
}
|
||||
if v := get("contractCode"); v != "" {
|
||||
contract.ContractCode = v
|
||||
}
|
||||
if v := get("buyParty"); v != "" {
|
||||
contract.BuyParty = v
|
||||
}
|
||||
if v := get("sellParty"); v != "" {
|
||||
contract.SellParty = v
|
||||
}
|
||||
if v := get("commodity"); v != "" {
|
||||
contract.Commodity = v
|
||||
}
|
||||
if v := get("count"); v != "" {
|
||||
if n, e := strconv.ParseFloat(v, 64); e == nil {
|
||||
contract.Count = n
|
||||
}
|
||||
}
|
||||
if v := get("unitPrice"); v != "" {
|
||||
if n, e := strconv.ParseFloat(v, 64); e == nil {
|
||||
contract.UnitPrice = n
|
||||
}
|
||||
}
|
||||
if v := get("blNumber"); v != "" {
|
||||
contract.BLNumber = v
|
||||
}
|
||||
contract.DeliveryTime = get("deliveryTime")
|
||||
contract.PickUpTime = get("pickUpTime")
|
||||
contract.Packaging = get("packaging")
|
||||
contract.Remarks = get("remarks")
|
||||
contract.UpdateTime = time.Now()
|
||||
|
||||
return s.repo.UpdateWithFiles(contract, keepFileIDs, newFileModels)
|
||||
}
|
||||
|
||||
func (s *contractService) Delete(id uint) error {
|
||||
return s.repo.Delete(id)
|
||||
}
|
||||
|
||||
func saveUploadedFile(file *multipart.FileHeader, dst string) error {
|
||||
src, err := file.Open()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer src.Close()
|
||||
|
||||
out, err := os.Create(dst)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer out.Close()
|
||||
|
||||
_, err = io.Copy(out, src)
|
||||
return err
|
||||
}
|
||||
153
backend/internal/service/delivery_service.go
Executable file
153
backend/internal/service/delivery_service.go
Executable file
@@ -0,0 +1,153 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/rovina/zsp-backend/internal/model"
|
||||
"github.com/rovina/zsp-backend/internal/repository"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type DeliveryService interface {
|
||||
// 我要提货
|
||||
ListApply(page, pageSize int, orderNumber, blNumber, status string) ([]model.DeliveryApply, int64, error)
|
||||
GetApplyByID(id uint) (*model.DeliveryApply, error)
|
||||
CreateApply(e *model.DeliveryApply) error
|
||||
UpdateApply(e *model.DeliveryApply) error
|
||||
DeleteApply(id uint) error
|
||||
|
||||
// 出库单
|
||||
ListOutbound(page, pageSize int, outboundNumber, blNumber, status string) ([]model.DeliveryOutbound, int64, error)
|
||||
GetOutboundByID(id uint) (*model.DeliveryOutbound, error)
|
||||
CreateOutbound(e *model.DeliveryOutbound) error
|
||||
UpdateOutbound(e *model.DeliveryOutbound) error
|
||||
DeleteOutbound(id uint) error
|
||||
|
||||
// 仓库
|
||||
ListWarehouses() ([]model.Warehouse, error)
|
||||
GetWarehouseByID(id uint) (*model.Warehouse, error)
|
||||
CreateWarehouse(e *model.Warehouse) error
|
||||
UpdateWarehouse(e *model.Warehouse) error
|
||||
DeleteWarehouse(id uint) error
|
||||
|
||||
// 入库
|
||||
ListInbound(page, pageSize int, warehouse, commodity string) ([]model.InventoryInbound, int64, error)
|
||||
GetInboundByID(id uint) (*model.InventoryInbound, error)
|
||||
CreateInbound(e *model.InventoryInbound) error
|
||||
UpdateInbound(e *model.InventoryInbound) error
|
||||
DeleteInbound(id uint) error
|
||||
|
||||
// 货权转移
|
||||
ListOwnership(page, pageSize int, transferNumber, blNumber, status string) ([]model.OwnershipTransfer, int64, error)
|
||||
GetOwnershipByID(id uint) (*model.OwnershipTransfer, error)
|
||||
CreateOwnership(e *model.OwnershipTransfer) error
|
||||
DeleteOwnership(id uint) error
|
||||
CreateOwnershipFiles(files []model.OwnershipTransferFile) error
|
||||
|
||||
// DB for complex queries (InventorySummary)
|
||||
DB() *gorm.DB
|
||||
}
|
||||
|
||||
type deliveryService struct {
|
||||
repo *repository.DeliveryRepository
|
||||
}
|
||||
|
||||
func NewDeliveryService(repo *repository.DeliveryRepository) DeliveryService {
|
||||
return &deliveryService{repo: repo}
|
||||
}
|
||||
|
||||
func (s *deliveryService) DB() *gorm.DB { return s.repo.DB() }
|
||||
|
||||
func (s *deliveryService) ListApply(page, pageSize int, orderNumber, blNumber, status string) ([]model.DeliveryApply, int64, error) {
|
||||
return s.repo.ListDeliveryApply(page, pageSize, orderNumber, blNumber, status)
|
||||
}
|
||||
func (s *deliveryService) GetApplyByID(id uint) (*model.DeliveryApply, error) {
|
||||
return s.repo.GetDeliveryApplyByID(id)
|
||||
}
|
||||
func (s *deliveryService) CreateApply(e *model.DeliveryApply) error {
|
||||
e.CreateTime = time.Now()
|
||||
e.UpdateTime = time.Now()
|
||||
return s.repo.CreateDeliveryApply(e)
|
||||
}
|
||||
func (s *deliveryService) UpdateApply(e *model.DeliveryApply) error {
|
||||
e.UpdateTime = time.Now()
|
||||
return s.repo.UpdateDeliveryApply(e)
|
||||
}
|
||||
func (s *deliveryService) DeleteApply(id uint) error {
|
||||
return s.repo.DeleteDeliveryApply(id)
|
||||
}
|
||||
|
||||
func (s *deliveryService) ListOutbound(page, pageSize int, outboundNumber, blNumber, status string) ([]model.DeliveryOutbound, int64, error) {
|
||||
return s.repo.ListDeliveryOutbound(page, pageSize, outboundNumber, blNumber, status)
|
||||
}
|
||||
func (s *deliveryService) GetOutboundByID(id uint) (*model.DeliveryOutbound, error) {
|
||||
return s.repo.GetDeliveryOutboundByID(id)
|
||||
}
|
||||
func (s *deliveryService) CreateOutbound(e *model.DeliveryOutbound) error {
|
||||
e.CreateTime = time.Now()
|
||||
e.UpdateTime = time.Now()
|
||||
return s.repo.CreateDeliveryOutbound(e)
|
||||
}
|
||||
func (s *deliveryService) UpdateOutbound(e *model.DeliveryOutbound) error {
|
||||
e.UpdateTime = time.Now()
|
||||
return s.repo.UpdateDeliveryOutbound(e)
|
||||
}
|
||||
func (s *deliveryService) DeleteOutbound(id uint) error {
|
||||
return s.repo.DeleteDeliveryOutbound(id)
|
||||
}
|
||||
|
||||
func (s *deliveryService) ListWarehouses() ([]model.Warehouse, error) {
|
||||
return s.repo.ListWarehouses()
|
||||
}
|
||||
func (s *deliveryService) GetWarehouseByID(id uint) (*model.Warehouse, error) {
|
||||
return s.repo.GetWarehouseByID(id)
|
||||
}
|
||||
func (s *deliveryService) CreateWarehouse(e *model.Warehouse) error {
|
||||
e.CreateTime = time.Now()
|
||||
e.UpdateTime = time.Now()
|
||||
return s.repo.CreateWarehouse(e)
|
||||
}
|
||||
func (s *deliveryService) UpdateWarehouse(e *model.Warehouse) error {
|
||||
e.UpdateTime = time.Now()
|
||||
return s.repo.UpdateWarehouse(e)
|
||||
}
|
||||
func (s *deliveryService) DeleteWarehouse(id uint) error {
|
||||
return s.repo.DeleteWarehouse(id)
|
||||
}
|
||||
|
||||
func (s *deliveryService) ListInbound(page, pageSize int, warehouse, commodity string) ([]model.InventoryInbound, int64, error) {
|
||||
return s.repo.ListInventoryInbound(page, pageSize, warehouse, commodity)
|
||||
}
|
||||
func (s *deliveryService) GetInboundByID(id uint) (*model.InventoryInbound, error) {
|
||||
return s.repo.GetInventoryInboundByID(id)
|
||||
}
|
||||
func (s *deliveryService) CreateInbound(e *model.InventoryInbound) error {
|
||||
e.CreateTime = time.Now()
|
||||
e.UpdateTime = time.Now()
|
||||
return s.repo.CreateInventoryInbound(e)
|
||||
}
|
||||
func (s *deliveryService) UpdateInbound(e *model.InventoryInbound) error {
|
||||
e.UpdateTime = time.Now()
|
||||
return s.repo.UpdateInventoryInbound(e)
|
||||
}
|
||||
func (s *deliveryService) DeleteInbound(id uint) error {
|
||||
return s.repo.DeleteInventoryInbound(id)
|
||||
}
|
||||
|
||||
func (s *deliveryService) ListOwnership(page, pageSize int, transferNumber, blNumber, status string) ([]model.OwnershipTransfer, int64, error) {
|
||||
return s.repo.ListOwnershipTransfer(page, pageSize, transferNumber, blNumber, status)
|
||||
}
|
||||
func (s *deliveryService) GetOwnershipByID(id uint) (*model.OwnershipTransfer, error) {
|
||||
return s.repo.GetOwnershipTransferByID(id)
|
||||
}
|
||||
func (s *deliveryService) CreateOwnership(e *model.OwnershipTransfer) error {
|
||||
e.CreateTime = time.Now()
|
||||
e.UpdateTime = time.Now()
|
||||
return s.repo.CreateOwnershipTransfer(e)
|
||||
}
|
||||
func (s *deliveryService) DeleteOwnership(id uint) error {
|
||||
return s.repo.DeleteOwnershipTransfer(id)
|
||||
}
|
||||
func (s *deliveryService) CreateOwnershipFiles(files []model.OwnershipTransferFile) error {
|
||||
return s.repo.CreateOwnershipTransferFiles(files)
|
||||
}
|
||||
127
backend/internal/service/user_service.go
Executable file
127
backend/internal/service/user_service.go
Executable file
@@ -0,0 +1,127 @@
|
||||
// internal/service/user_service.go
|
||||
package service
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/golang-jwt/jwt/v4"
|
||||
"github.com/rovina/zsp-backend/internal/dto"
|
||||
"github.com/rovina/zsp-backend/internal/model"
|
||||
"github.com/rovina/zsp-backend/internal/repository"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
type Claims struct {
|
||||
UserID uint `json:"user_id"`
|
||||
jwt.RegisteredClaims
|
||||
}
|
||||
|
||||
type UserService interface {
|
||||
Register(req *dto.CreateUserRequest) (*dto.UserResponse, error)
|
||||
Login(req *dto.LoginRequest) (*dto.UserResponse, string, error)
|
||||
GetUserByID(id uint) (*dto.UserResponse, error)
|
||||
}
|
||||
|
||||
type userService struct {
|
||||
userRepo repository.UserRepository
|
||||
jwtSecret []byte // 改为字节切片
|
||||
}
|
||||
|
||||
func NewUserService(userRepo repository.UserRepository, jwtSecretStr string) (*userService, error) {
|
||||
// 直接使用字符串作为密钥,不再解析 PEM
|
||||
return &userService{
|
||||
userRepo: userRepo,
|
||||
jwtSecret: []byte(jwtSecretStr),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *userService) Register(req *dto.CreateUserRequest) (*dto.UserResponse, error) {
|
||||
// 检查邮箱是否已存在
|
||||
existingUser, err := s.userRepo.FindByEmail(req.Email)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("database error: %v", err)
|
||||
}
|
||||
if existingUser != nil {
|
||||
return nil, errors.New("email already exists")
|
||||
}
|
||||
|
||||
existingUser, err = s.userRepo.FindByUsername(req.Username)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("database error: %v", err)
|
||||
}
|
||||
if existingUser != nil {
|
||||
return nil, errors.New("username already exists")
|
||||
}
|
||||
|
||||
// 加密密码
|
||||
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
user := &model.User{
|
||||
Username: req.Username,
|
||||
Email: req.Email,
|
||||
Password: string(hashedPassword),
|
||||
}
|
||||
|
||||
if err := s.userRepo.Create(user); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &dto.UserResponse{
|
||||
ID: user.ID,
|
||||
Username: user.Username,
|
||||
Email: user.Email,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *userService) Login(req *dto.LoginRequest) (*dto.UserResponse, string, error) {
|
||||
user, err := s.userRepo.FindByEmail(req.Email)
|
||||
if err != nil {
|
||||
return nil, "", errors.New("invalid credentials")
|
||||
}
|
||||
|
||||
if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(req.Password)); err != nil {
|
||||
return nil, "", errors.New("invalid credentials")
|
||||
}
|
||||
|
||||
expirationTime := time.Now().Add(24 * time.Hour)
|
||||
|
||||
claims := &Claims{
|
||||
UserID: user.ID,
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
ExpiresAt: jwt.NewNumericDate(expirationTime),
|
||||
IssuedAt: jwt.NewNumericDate(time.Now()),
|
||||
NotBefore: jwt.NewNumericDate(time.Now()),
|
||||
Issuer: "zsp-backend",
|
||||
},
|
||||
}
|
||||
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||
tokenString, err := token.SignedString(s.jwtSecret)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
return &dto.UserResponse{
|
||||
ID: user.ID,
|
||||
Username: user.Username,
|
||||
Email: user.Email,
|
||||
}, tokenString, nil
|
||||
}
|
||||
|
||||
func (s *userService) GetUserByID(id uint) (*dto.UserResponse, error) {
|
||||
user, err := s.userRepo.FindByID(id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &dto.UserResponse{
|
||||
ID: user.ID,
|
||||
Username: user.Username,
|
||||
Email: user.Email,
|
||||
}, nil
|
||||
}
|
||||
271
backend/internal/service/zsp_contract_service.go
Executable file
271
backend/internal/service/zsp_contract_service.go
Executable file
@@ -0,0 +1,271 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"mime/multipart"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/rovina/zsp-backend/internal/dto"
|
||||
"github.com/rovina/zsp-backend/internal/model"
|
||||
"github.com/rovina/zsp-backend/internal/repository"
|
||||
)
|
||||
|
||||
type ZSPContractService interface {
|
||||
CreateFolder(req *dto.ZSPFolderCreateRequest) error
|
||||
GetFolders() ([]model.ZSPContractFolder, error)
|
||||
GetFolderById(folderID string) (*model.ZSPContractFolder, error)
|
||||
DeleteFolderById(folderID string) error
|
||||
UpdateFolderById(folderID string, req *dto.ZSPFolderUpdateRequest) error
|
||||
|
||||
UploadContract(req *dto.ZSPContractCreateRequest, fileHeader *multipart.FileHeader) error
|
||||
GetContractById(contractID string) (*model.ZSPContractFile, error)
|
||||
GetContractWithDetails(contractID string) (*model.ZSPContractFile, error)
|
||||
ListContracts(folderID string) ([]model.ZSPContractFile, error)
|
||||
ListContractsWithDetails(folderID string) ([]model.ZSPContractFile, error)
|
||||
DeleteContractById(contractID string) error
|
||||
UpdateContract(contract *model.ZSPContractFile) error
|
||||
UpdateContractById(contractID string, req *dto.ZSPContractUpdateRequest) error
|
||||
|
||||
// 合同详情相关
|
||||
CreateContractDetail(req *dto.ZSPContractDetailCreateRequest) error
|
||||
UpdateContractDetail(req *dto.ZSPContractDetailUpdateRequest) error
|
||||
GetContractDetailById(detailID string) (*model.ZSPContractDetail, error)
|
||||
ListContractDetails(contractFileID string) ([]model.ZSPContractDetail, error)
|
||||
DeleteContractDetailById(detailID string) error
|
||||
BatchCreateContractDetails(req *dto.ZSPContractDetailBatchCreateRequest) error
|
||||
}
|
||||
|
||||
type zspContractService struct {
|
||||
repo repository.ZSPContractRepository
|
||||
}
|
||||
|
||||
func NewZSPContractService(repo repository.ZSPContractRepository) (ZSPContractService, error) {
|
||||
return &zspContractService{repo: repo}, nil
|
||||
}
|
||||
|
||||
func (s *zspContractService) CreateFolder(req *dto.ZSPFolderCreateRequest) error {
|
||||
var folder model.ZSPContractFolder
|
||||
folder.Name = req.Name
|
||||
folder.Description = req.Description
|
||||
folder.Count = 0
|
||||
folder.CreateTime = time.Now()
|
||||
|
||||
return s.repo.CreateFolder(&folder)
|
||||
}
|
||||
|
||||
func (s *zspContractService) GetFolders() ([]model.ZSPContractFolder, error) {
|
||||
folders, err := s.repo.GetFolders()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return folders, nil
|
||||
}
|
||||
|
||||
func (s *zspContractService) GetFolderById(folderID string) (*model.ZSPContractFolder, error) {
|
||||
return s.repo.GetFolderById(folderID)
|
||||
}
|
||||
|
||||
func (s *zspContractService) DeleteFolderById(folderID string) error {
|
||||
return s.repo.DeleteFolderById(folderID)
|
||||
}
|
||||
|
||||
func (s *zspContractService) UpdateFolderById(folderID string, req *dto.ZSPFolderUpdateRequest) error {
|
||||
folder, err := s.repo.GetFolderById(folderID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
folder.Name = req.Name
|
||||
folder.Description = req.Description
|
||||
return s.repo.UpdateFolder(folder)
|
||||
}
|
||||
|
||||
func (s *zspContractService) UploadContract(req *dto.ZSPContractCreateRequest, fileHeader *multipart.FileHeader) error {
|
||||
timeNow := time.Now()
|
||||
var contract model.ZSPContractFile
|
||||
contract.FolderID = req.FolderID
|
||||
contract.Commodity = req.Commodity
|
||||
contract.CompanyName = req.CompanyName
|
||||
contract.ContractNumber = req.ContractNumber
|
||||
contract.ContractType = req.ContractType
|
||||
contract.FileName = fileHeader.Filename
|
||||
// 提取扩展名便于前端展示(如 pdf、jpg)
|
||||
if ext := path.Ext(fileHeader.Filename); ext != "" {
|
||||
contract.FileType = strings.TrimPrefix(strings.ToLower(ext), ".")
|
||||
} else {
|
||||
contract.FileType = fileHeader.Header.Get("Content-Type")
|
||||
}
|
||||
if req.SignDate == "" {
|
||||
return fmt.Errorf("签订日期为必填项")
|
||||
}
|
||||
signDate, err := time.Parse("2006-01-02", req.SignDate)
|
||||
if err != nil {
|
||||
return fmt.Errorf("签订日期格式不正确,请使用 YYYY-MM-DD: %w", err)
|
||||
}
|
||||
contract.SignDate = signDate
|
||||
contract.UploadTime = timeNow
|
||||
|
||||
// 确保目录存在后保存文件
|
||||
saveDir := "./data/zsp-contract/files"
|
||||
if err := os.MkdirAll(saveDir, os.ModePerm); err != nil {
|
||||
return fmt.Errorf("create upload dir: %w", err)
|
||||
}
|
||||
storedName := fmt.Sprintf("%d_%s", timeNow.UnixNano(), fileHeader.Filename)
|
||||
savePath := path.Join(saveDir, storedName)
|
||||
|
||||
if err := saveUploadedFile(fileHeader, savePath); err != nil {
|
||||
return err
|
||||
}
|
||||
contract.FileURL = "files/" + storedName
|
||||
contract.FileSize = fileHeader.Size
|
||||
return s.repo.CreateContract(&contract)
|
||||
}
|
||||
|
||||
func (s *zspContractService) GetContractById(contractID string) (*model.ZSPContractFile, error) {
|
||||
return s.repo.GetContractById(contractID)
|
||||
}
|
||||
|
||||
func (s *zspContractService) ListContracts(folderID string) ([]model.ZSPContractFile, error) {
|
||||
return s.repo.ListContracts(folderID)
|
||||
}
|
||||
|
||||
func (s *zspContractService) DeleteContractById(contractID string) error {
|
||||
return s.repo.DeleteContractById(contractID)
|
||||
}
|
||||
|
||||
func (s *zspContractService) UpdateContract(contract *model.ZSPContractFile) error {
|
||||
return s.repo.UpdateContract(contract)
|
||||
}
|
||||
|
||||
func (s *zspContractService) UpdateContractById(contractID string, req *dto.ZSPContractUpdateRequest) error {
|
||||
contract, err := s.repo.GetContractById(contractID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if req.ContractNumber != "" {
|
||||
contract.ContractNumber = req.ContractNumber
|
||||
}
|
||||
if req.CompanyName != "" {
|
||||
contract.CompanyName = req.CompanyName
|
||||
}
|
||||
if req.Commodity != "" {
|
||||
contract.Commodity = req.Commodity
|
||||
}
|
||||
if req.ContractType != "" {
|
||||
contract.ContractType = req.ContractType
|
||||
}
|
||||
if req.FolderID > 0 {
|
||||
contract.FolderID = req.FolderID
|
||||
}
|
||||
if req.SignDate != "" {
|
||||
signDate, err := time.Parse("2006-01-02", req.SignDate)
|
||||
if err != nil {
|
||||
return fmt.Errorf("签订日期格式不正确: %w", err)
|
||||
}
|
||||
contract.SignDate = signDate
|
||||
}
|
||||
return s.repo.UpdateContract(contract)
|
||||
}
|
||||
|
||||
func (s *zspContractService) GetContractWithDetails(contractID string) (*model.ZSPContractFile, error) {
|
||||
contract, err := s.repo.GetContractById(contractID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// 获取详情列表
|
||||
details, err := s.repo.ListContractDetails(contractID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
contract.Details = details
|
||||
return contract, nil
|
||||
}
|
||||
|
||||
func (s *zspContractService) ListContractsWithDetails(folderID string) ([]model.ZSPContractFile, error) {
|
||||
return s.repo.ListContractsWithDetails(folderID)
|
||||
}
|
||||
|
||||
// ============ 合同详情相关方法 ============
|
||||
|
||||
func (s *zspContractService) CreateContractDetail(req *dto.ZSPContractDetailCreateRequest) error {
|
||||
var detail model.ZSPContractDetail
|
||||
detail.ContractFileID = req.ContractFileID
|
||||
detail.CommodityName = req.CommodityName
|
||||
detail.CommodityCode = req.CommodityCode
|
||||
detail.TotalQuantity = req.TotalQuantity
|
||||
detail.Unit = req.Unit
|
||||
detail.UnitPrice = req.UnitPrice
|
||||
detail.DeliveredQty = req.DeliveredQty
|
||||
detail.BillOfLading = req.BillOfLading
|
||||
detail.Remark = req.Remark
|
||||
|
||||
// PendingQty 会在 BeforeCreate 钩子中自动计算
|
||||
return s.repo.CreateContractDetail(&detail)
|
||||
}
|
||||
|
||||
func (s *zspContractService) UpdateContractDetail(req *dto.ZSPContractDetailUpdateRequest) error {
|
||||
detail, err := s.repo.GetContractDetailById(fmt.Sprintf("%d", req.ID))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if req.CommodityName != "" {
|
||||
detail.CommodityName = req.CommodityName
|
||||
}
|
||||
if req.CommodityCode != "" {
|
||||
detail.CommodityCode = req.CommodityCode
|
||||
}
|
||||
if req.TotalQuantity > 0 {
|
||||
detail.TotalQuantity = req.TotalQuantity
|
||||
}
|
||||
if req.Unit != "" {
|
||||
detail.Unit = req.Unit
|
||||
}
|
||||
if req.UnitPrice > 0 {
|
||||
detail.UnitPrice = req.UnitPrice
|
||||
}
|
||||
// DeliveredQty 可以为 0,所以需要特别处理
|
||||
detail.DeliveredQty = req.DeliveredQty
|
||||
if req.BillOfLading != "" {
|
||||
detail.BillOfLading = req.BillOfLading
|
||||
}
|
||||
detail.Remark = req.Remark
|
||||
|
||||
// PendingQty 会在 BeforeUpdate 钩子中自动计算
|
||||
return s.repo.UpdateContractDetail(detail)
|
||||
}
|
||||
|
||||
func (s *zspContractService) GetContractDetailById(detailID string) (*model.ZSPContractDetail, error) {
|
||||
return s.repo.GetContractDetailById(detailID)
|
||||
}
|
||||
|
||||
func (s *zspContractService) ListContractDetails(contractFileID string) ([]model.ZSPContractDetail, error) {
|
||||
return s.repo.ListContractDetails(contractFileID)
|
||||
}
|
||||
|
||||
func (s *zspContractService) DeleteContractDetailById(detailID string) error {
|
||||
return s.repo.DeleteContractDetailById(detailID)
|
||||
}
|
||||
|
||||
func (s *zspContractService) BatchCreateContractDetails(req *dto.ZSPContractDetailBatchCreateRequest) error {
|
||||
details := make([]model.ZSPContractDetail, 0, len(req.Details))
|
||||
for _, detailReq := range req.Details {
|
||||
detail := model.ZSPContractDetail{
|
||||
ContractFileID: req.ContractFileID,
|
||||
CommodityName: detailReq.CommodityName,
|
||||
CommodityCode: detailReq.CommodityCode,
|
||||
TotalQuantity: detailReq.TotalQuantity,
|
||||
Unit: detailReq.Unit,
|
||||
UnitPrice: detailReq.UnitPrice,
|
||||
DeliveredQty: detailReq.DeliveredQty,
|
||||
BillOfLading: detailReq.BillOfLading,
|
||||
Remark: detailReq.Remark,
|
||||
}
|
||||
// PendingQty 会在 BeforeCreate 钩子中自动计算
|
||||
details = append(details, detail)
|
||||
}
|
||||
return s.repo.BatchCreateContractDetails(details)
|
||||
}
|
||||
335
backend/internal/service/zsp_finances_service.go
Executable file
335
backend/internal/service/zsp_finances_service.go
Executable file
@@ -0,0 +1,335 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/rovina/zsp-backend/internal/dto"
|
||||
"github.com/rovina/zsp-backend/internal/model"
|
||||
"github.com/rovina/zsp-backend/internal/repository"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type ZSPFinancesService interface {
|
||||
CreateFreightCharge(req *dto.ZSPFreightChargesCreateRequest) error
|
||||
GetFreightCharge(id string) (*model.ZSPFreightCharges, error)
|
||||
ListFreightCharges() ([]model.ZSPFreightCharges, error)
|
||||
UpdateFreightCharge(id string, req *dto.ZSPFreightChargesUpdateRequest) error
|
||||
DeleteFreightCharge(id string) error
|
||||
|
||||
CreateMiscCharge(req *dto.ZSPMiscChargesCreateRequest) error
|
||||
GetMiscCharge(id string) (*model.ZSPMiscCharges, error)
|
||||
ListMiscCharges() ([]model.ZSPMiscCharges, error)
|
||||
UpdateMiscCharge(id string, req *dto.ZSPMiscChargesUpdateRequest) error
|
||||
DeleteMiscCharge(id string) error
|
||||
|
||||
CreateServiceFee(req *dto.ZSPServiceFeesCreateRequest) error
|
||||
GetServiceFee(id string) (*model.ZSPServiceFees, error)
|
||||
ListServiceFees() ([]model.ZSPServiceFees, error)
|
||||
UpdateServiceFee(id string, req *dto.ZSPServiceFeesUpdateRequest) error
|
||||
DeleteServiceFee(id string) error
|
||||
|
||||
CreateCostBreakdown(req *dto.ZSPCostBreakdownCreateRequest) error
|
||||
GetCostBreakdown(id string) (*model.ZSPCostBreakdown, error)
|
||||
ListCostBreakdowns(businessID string) ([]model.ZSPCostBreakdown, error)
|
||||
UpdateCostBreakdown(id string, req *dto.ZSPCostBreakdownUpdateRequest) error
|
||||
DeleteCostBreakdown(id string) error
|
||||
|
||||
CreateProfitRecord(req *dto.ZSPProfitRecordCreateRequest) error
|
||||
GetProfitRecord(id string) (*model.ZSPProfitRecord, error)
|
||||
ListProfitRecords(filters map[string]string) ([]model.ZSPProfitRecord, error)
|
||||
UpdateProfitRecord(id string, req *dto.ZSPProfitRecordUpdateRequest) error
|
||||
DeleteProfitRecord(id string) error
|
||||
}
|
||||
|
||||
type zspFinancesService struct {
|
||||
repo repository.ZSPFinancesRepository
|
||||
}
|
||||
|
||||
func NewZSPFinancesService(db *gorm.DB) ZSPFinancesService {
|
||||
return &zspFinancesService{repo: repository.NewZSPFinancesRepository(db)}
|
||||
}
|
||||
|
||||
// ============ 货代费用 ============
|
||||
func (s *zspFinancesService) CreateFreightCharge(req *dto.ZSPFreightChargesCreateRequest) error {
|
||||
currency := req.Currency
|
||||
if currency == "" {
|
||||
currency = "CNY"
|
||||
}
|
||||
charge := model.ZSPFreightCharges{
|
||||
BillOfLading: req.BillOfLading,
|
||||
ContractNumber: req.ContractNumber,
|
||||
ExpenseItem: req.ExpenseItem,
|
||||
Amount: req.Amount,
|
||||
Currency: currency,
|
||||
PaymentDate: parseDate(req.PaymentDate),
|
||||
FreightCompanyName: req.FreightCompanyName,
|
||||
Remark: req.Remark,
|
||||
CreateTime: time.Now(),
|
||||
UpdateTime: time.Now(),
|
||||
}
|
||||
return s.repo.CreateFreightCharge(&charge)
|
||||
}
|
||||
|
||||
func (s *zspFinancesService) GetFreightCharge(id string) (*model.ZSPFreightCharges, error) {
|
||||
return s.repo.GetFreightCharge(id)
|
||||
}
|
||||
|
||||
func (s *zspFinancesService) ListFreightCharges() ([]model.ZSPFreightCharges, error) {
|
||||
return s.repo.ListFreightCharges()
|
||||
}
|
||||
|
||||
func (s *zspFinancesService) UpdateFreightCharge(id string, req *dto.ZSPFreightChargesUpdateRequest) error {
|
||||
charge, err := s.repo.GetFreightCharge(id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if req.BillOfLading != "" {
|
||||
charge.BillOfLading = req.BillOfLading
|
||||
}
|
||||
if req.ContractNumber != "" {
|
||||
charge.ContractNumber = req.ContractNumber
|
||||
}
|
||||
if req.ExpenseItem != "" {
|
||||
charge.ExpenseItem = req.ExpenseItem
|
||||
}
|
||||
if req.Amount != 0 {
|
||||
charge.Amount = req.Amount
|
||||
}
|
||||
if req.Currency != "" {
|
||||
charge.Currency = req.Currency
|
||||
}
|
||||
if req.PaymentDate != "" {
|
||||
charge.PaymentDate = parseDate(req.PaymentDate)
|
||||
}
|
||||
if req.FreightCompanyName != "" {
|
||||
charge.FreightCompanyName = req.FreightCompanyName
|
||||
}
|
||||
charge.Remark = req.Remark
|
||||
charge.UpdateTime = time.Now()
|
||||
return s.repo.UpdateFreightCharge(charge)
|
||||
}
|
||||
|
||||
func (s *zspFinancesService) DeleteFreightCharge(id string) error {
|
||||
return s.repo.DeleteFreightCharge(id)
|
||||
}
|
||||
|
||||
// ============ 杂费 ============
|
||||
func (s *zspFinancesService) CreateMiscCharge(req *dto.ZSPMiscChargesCreateRequest) error {
|
||||
currency := req.Currency
|
||||
if currency == "" {
|
||||
currency = "CNY"
|
||||
}
|
||||
charge := model.ZSPMiscCharges{
|
||||
BillOfLading: req.BillOfLading,
|
||||
ContractNumber: req.ContractNumber,
|
||||
ExpenseItem: req.ExpenseItem,
|
||||
Amount: req.Amount,
|
||||
Currency: currency,
|
||||
PaymentDate: parseDate(req.PaymentDate),
|
||||
CompanyName: req.CompanyName,
|
||||
Remark: req.Remark,
|
||||
CreateTime: time.Now(),
|
||||
UpdateTime: time.Now(),
|
||||
}
|
||||
return s.repo.CreateMiscCharge(&charge)
|
||||
}
|
||||
|
||||
func (s *zspFinancesService) GetMiscCharge(id string) (*model.ZSPMiscCharges, error) {
|
||||
return s.repo.GetMiscCharge(id)
|
||||
}
|
||||
|
||||
func (s *zspFinancesService) ListMiscCharges() ([]model.ZSPMiscCharges, error) {
|
||||
return s.repo.ListMiscCharges()
|
||||
}
|
||||
|
||||
func (s *zspFinancesService) UpdateMiscCharge(id string, req *dto.ZSPMiscChargesUpdateRequest) error {
|
||||
charge, err := s.repo.GetMiscCharge(id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if req.BillOfLading != "" {
|
||||
charge.BillOfLading = req.BillOfLading
|
||||
}
|
||||
if req.ContractNumber != "" {
|
||||
charge.ContractNumber = req.ContractNumber
|
||||
}
|
||||
if req.ExpenseItem != "" {
|
||||
charge.ExpenseItem = req.ExpenseItem
|
||||
}
|
||||
if req.Amount != 0 {
|
||||
charge.Amount = req.Amount
|
||||
}
|
||||
if req.Currency != "" {
|
||||
charge.Currency = req.Currency
|
||||
}
|
||||
if req.PaymentDate != "" {
|
||||
charge.PaymentDate = parseDate(req.PaymentDate)
|
||||
}
|
||||
if req.CompanyName != "" {
|
||||
charge.CompanyName = req.CompanyName
|
||||
}
|
||||
charge.Remark = req.Remark
|
||||
charge.UpdateTime = time.Now()
|
||||
return s.repo.UpdateMiscCharge(charge)
|
||||
}
|
||||
|
||||
func (s *zspFinancesService) DeleteMiscCharge(id string) error {
|
||||
return s.repo.DeleteMiscCharge(id)
|
||||
}
|
||||
|
||||
// ============ 服务费 ============
|
||||
func (s *zspFinancesService) CreateServiceFee(req *dto.ZSPServiceFeesCreateRequest) error {
|
||||
currency := req.Currency
|
||||
if currency == "" {
|
||||
currency = "CNY"
|
||||
}
|
||||
fee := model.ZSPServiceFees{
|
||||
BillOfLading: req.BillOfLading,
|
||||
ContractNumber: req.ContractNumber,
|
||||
Name: req.Name,
|
||||
Amount: req.Amount,
|
||||
Currency: currency,
|
||||
Date: parseDate(req.Date),
|
||||
Remark: req.Remark,
|
||||
CreateTime: time.Now(),
|
||||
UpdateTime: time.Now(),
|
||||
}
|
||||
return s.repo.CreateServiceFee(&fee)
|
||||
}
|
||||
|
||||
func (s *zspFinancesService) GetServiceFee(id string) (*model.ZSPServiceFees, error) {
|
||||
return s.repo.GetServiceFee(id)
|
||||
}
|
||||
|
||||
func (s *zspFinancesService) ListServiceFees() ([]model.ZSPServiceFees, error) {
|
||||
return s.repo.ListServiceFees()
|
||||
}
|
||||
|
||||
func (s *zspFinancesService) UpdateServiceFee(id string, req *dto.ZSPServiceFeesUpdateRequest) error {
|
||||
fee, err := s.repo.GetServiceFee(id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if req.BillOfLading != "" {
|
||||
fee.BillOfLading = req.BillOfLading
|
||||
}
|
||||
if req.ContractNumber != "" {
|
||||
fee.ContractNumber = req.ContractNumber
|
||||
}
|
||||
if req.Name != "" {
|
||||
fee.Name = req.Name
|
||||
}
|
||||
if req.Amount != 0 {
|
||||
fee.Amount = req.Amount
|
||||
}
|
||||
if req.Currency != "" {
|
||||
fee.Currency = req.Currency
|
||||
}
|
||||
if req.Date != "" {
|
||||
fee.Date = parseDate(req.Date)
|
||||
}
|
||||
fee.Remark = req.Remark
|
||||
fee.UpdateTime = time.Now()
|
||||
return s.repo.UpdateServiceFee(fee)
|
||||
}
|
||||
|
||||
func (s *zspFinancesService) DeleteServiceFee(id string) error {
|
||||
return s.repo.DeleteServiceFee(id)
|
||||
}
|
||||
|
||||
// ============ 成本构成 ============
|
||||
func (s *zspFinancesService) CreateCostBreakdown(req *dto.ZSPCostBreakdownCreateRequest) error {
|
||||
breakdown := model.ZSPCostBreakdown{
|
||||
BusinessID: req.BusinessID,
|
||||
BusinessType: req.BusinessType,
|
||||
TotalCost: 0,
|
||||
CreateTime: time.Now(),
|
||||
UpdateTime: time.Now(),
|
||||
}
|
||||
return s.repo.CreateCostBreakdown(&breakdown)
|
||||
}
|
||||
|
||||
func (s *zspFinancesService) GetCostBreakdown(id string) (*model.ZSPCostBreakdown, error) {
|
||||
return s.repo.GetCostBreakdown(id)
|
||||
}
|
||||
|
||||
func (s *zspFinancesService) ListCostBreakdowns(businessID string) ([]model.ZSPCostBreakdown, error) {
|
||||
return s.repo.ListCostBreakdowns(businessID)
|
||||
}
|
||||
|
||||
func (s *zspFinancesService) UpdateCostBreakdown(id string, req *dto.ZSPCostBreakdownUpdateRequest) error {
|
||||
breakdown, err := s.repo.GetCostBreakdown(id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
breakdown.FreightChargeTotal = req.FreightChargeTotal
|
||||
breakdown.MiscChargeTotal = req.MiscChargeTotal
|
||||
breakdown.ServiceFeeTotal = req.ServiceFeeTotal
|
||||
breakdown.TotalCost = req.FreightChargeTotal + req.MiscChargeTotal + req.ServiceFeeTotal
|
||||
breakdown.UpdateTime = time.Now()
|
||||
return s.repo.UpdateCostBreakdown(breakdown)
|
||||
}
|
||||
|
||||
func (s *zspFinancesService) DeleteCostBreakdown(id string) error {
|
||||
return s.repo.DeleteCostBreakdown(id)
|
||||
}
|
||||
|
||||
// ============ 利润记录 ============
|
||||
func (s *zspFinancesService) CreateProfitRecord(req *dto.ZSPProfitRecordCreateRequest) error {
|
||||
record := model.ZSPProfitRecord{
|
||||
BusinessID: req.BusinessID,
|
||||
BusinessType: req.BusinessType,
|
||||
Revenue: req.Revenue,
|
||||
TotalCost: req.TotalCost,
|
||||
Profit: req.Revenue - req.TotalCost,
|
||||
ProfitRate: calculateProfitRate(req.Revenue, req.TotalCost),
|
||||
RecordDate: parseDate(req.RecordDate),
|
||||
Remark: req.Remark,
|
||||
CreateTime: time.Now(),
|
||||
UpdateTime: time.Now(),
|
||||
}
|
||||
return s.repo.CreateProfitRecord(&record)
|
||||
}
|
||||
|
||||
func (s *zspFinancesService) GetProfitRecord(id string) (*model.ZSPProfitRecord, error) {
|
||||
return s.repo.GetProfitRecord(id)
|
||||
}
|
||||
|
||||
func (s *zspFinancesService) ListProfitRecords(filters map[string]string) ([]model.ZSPProfitRecord, error) {
|
||||
return s.repo.ListProfitRecords(filters)
|
||||
}
|
||||
|
||||
func (s *zspFinancesService) UpdateProfitRecord(id string, req *dto.ZSPProfitRecordUpdateRequest) error {
|
||||
record, err := s.repo.GetProfitRecord(id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
record.Revenue = req.Revenue
|
||||
record.TotalCost = req.TotalCost
|
||||
record.Profit = req.Revenue - req.TotalCost
|
||||
record.ProfitRate = calculateProfitRate(req.Revenue, req.TotalCost)
|
||||
record.Remark = req.Remark
|
||||
record.UpdateTime = time.Now()
|
||||
return s.repo.UpdateProfitRecord(id, record)
|
||||
}
|
||||
|
||||
func (s *zspFinancesService) DeleteProfitRecord(id string) error {
|
||||
return s.repo.DeleteProfitRecord(id)
|
||||
}
|
||||
|
||||
// ============ 辅助函数 ============
|
||||
|
||||
func parseDate(dateStr string) time.Time {
|
||||
t, err := time.Parse("2006-01-02", dateStr)
|
||||
if err != nil {
|
||||
return time.Time{}
|
||||
}
|
||||
return t
|
||||
}
|
||||
|
||||
func calculateProfitRate(revenue, totalCost float64) float64 {
|
||||
if revenue == 0 {
|
||||
return 0
|
||||
}
|
||||
return (revenue - totalCost) / revenue * 100
|
||||
}
|
||||
51
backend/pkg/config/config.go
Executable file
51
backend/pkg/config/config.go
Executable file
@@ -0,0 +1,51 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"log"
|
||||
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Server ServerConfig `mapstructure:"server"`
|
||||
Database DatabaseConfig `mapstructure:"database"`
|
||||
JWT JWTConfig `mapstructure:"jwt"`
|
||||
}
|
||||
|
||||
type ServerConfig struct {
|
||||
Port int `mapstructure:"port"`
|
||||
Mode string `mapstructure:"mode"`
|
||||
ReadTimeout int `mapstructure:"read_timeout"`
|
||||
WriteTimeout int `mapstructure:"write_timeout"`
|
||||
}
|
||||
|
||||
type DatabaseConfig struct {
|
||||
Host string `mapstructure:"host"`
|
||||
Port int `mapstructure:"port"`
|
||||
User string `mapstructure:"user"`
|
||||
Password string `mapstructure:"password"`
|
||||
DBName string `mapstructure:"dbname"`
|
||||
MaxOpenConns int `mapstructure:"max_open_conns"`
|
||||
MaxIdleConns int `mapstructure:"max_idle_conns"`
|
||||
ResetDatabase bool `mapstructure:"reset_database"`
|
||||
}
|
||||
|
||||
type JWTConfig struct {
|
||||
Secret string `mapstructure:"secret"`
|
||||
ExpireHours int `mapstructure:"expire_hours"`
|
||||
}
|
||||
|
||||
func Load(configPath string) (*Config, error) {
|
||||
viper.SetConfigFile(configPath)
|
||||
viper.AutomaticEnv()
|
||||
if err := viper.ReadInConfig(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var config Config
|
||||
if err := viper.Unmarshal(&config); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
log.Printf("Config loaded successfully from %s", configPath)
|
||||
return &config, nil
|
||||
}
|
||||
30
backend/pkg/database/gorm.go
Executable file
30
backend/pkg/database/gorm.go
Executable file
@@ -0,0 +1,30 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/rovina/zsp-backend/pkg/config"
|
||||
"gorm.io/driver/mysql"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/logger"
|
||||
)
|
||||
|
||||
func NewDB(cfg *config.DatabaseConfig) (*gorm.DB, error) {
|
||||
dsn := fmt.Sprintf(
|
||||
"%s:%s@tcp(%s:%d)/%s?charset=utf8mb4&parseTime=True&loc=Local",
|
||||
cfg.User, cfg.Password, cfg.Host, cfg.Port, cfg.DBName,
|
||||
)
|
||||
|
||||
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
|
||||
Logger: logger.Default.LogMode(logger.Info),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sqlDB, _ := db.DB()
|
||||
sqlDB.SetMaxOpenConns(cfg.MaxOpenConns)
|
||||
sqlDB.SetMaxIdleConns(cfg.MaxIdleConns)
|
||||
|
||||
return db, nil
|
||||
}
|
||||
57
backend/pkg/database/init.go
Executable file
57
backend/pkg/database/init.go
Executable file
@@ -0,0 +1,57 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"github.com/rovina/zsp-backend/pkg/config"
|
||||
)
|
||||
|
||||
func CheckDatabaseConnection(cfg *config.DatabaseConfig) error {
|
||||
dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8mb4&parseTime=True&loc=Local",
|
||||
cfg.User, cfg.Password, cfg.Host, cfg.Port, cfg.DBName)
|
||||
log.Println("start conn db:", dsn)
|
||||
db, err := sql.Open("mysql", dsn)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
return db.Ping()
|
||||
}
|
||||
|
||||
func EnsureDatabase(cfg *config.DatabaseConfig) error {
|
||||
// 不指定数据库连接
|
||||
dsn := fmt.Sprintf(
|
||||
"%s:%s@tcp(%s:%d)/?charset=utf8mb4&parseTime=True&loc=Local",
|
||||
cfg.User, cfg.Password, cfg.Host, cfg.Port,
|
||||
)
|
||||
|
||||
db, err := sql.Open("mysql", dsn)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
// 创建数据库
|
||||
if cfg.ResetDatabase {
|
||||
sql := fmt.Sprintf("DROP DATABASE IF EXISTS `%s`;", cfg.DBName)
|
||||
_, err = db.Exec(sql)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
sql := fmt.Sprintf(
|
||||
"CREATE DATABASE IF NOT EXISTS `%s` DEFAULT CHARSET utf8mb4;",
|
||||
cfg.DBName,
|
||||
)
|
||||
|
||||
_, err = db.Exec(sql)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
log.Println("Database ensured:", cfg.DBName)
|
||||
return nil
|
||||
}
|
||||
39
backend/pkg/database/migrate.go
Executable file
39
backend/pkg/database/migrate.go
Executable file
@@ -0,0 +1,39 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"github.com/rovina/zsp-backend/internal/model"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func AutoMigrate(db *gorm.DB) error {
|
||||
return db.AutoMigrate(
|
||||
&model.User{},
|
||||
&model.Contract{},
|
||||
&model.ContractFile{},
|
||||
&model.ZSPContractFolder{},
|
||||
&model.ZSPContractFile{},
|
||||
&model.ZSPContractDetail{},
|
||||
&model.ZSPFreightCharges{},
|
||||
&model.ZSPMiscCharges{},
|
||||
&model.ZSPServiceFees{},
|
||||
&model.ZSPCostBreakdown{},
|
||||
&model.ZSPProfitRecord{},
|
||||
&model.ZSPUpstreamInvoice{},
|
||||
&model.ZSPDownstreamInvoice{},
|
||||
&model.ZSPFreightMiscInvoice{},
|
||||
&model.ZSPSettlement{},
|
||||
&model.ZSPReconciliation{},
|
||||
&model.ContractFolder{},
|
||||
&model.ContractBatch{},
|
||||
&model.ContractBatchFile{},
|
||||
&model.CompanyFolder{},
|
||||
&model.CompanyFile{},
|
||||
&model.DeliveryApply{},
|
||||
&model.DeliveryOutbound{},
|
||||
&model.Warehouse{},
|
||||
&model.InventoryInbound{},
|
||||
&model.OwnershipTransfer{},
|
||||
&model.OwnershipTransferBL{},
|
||||
&model.OwnershipTransferFile{},
|
||||
)
|
||||
}
|
||||
45
backend/pkg/utils/file.go
Executable file
45
backend/pkg/utils/file.go
Executable file
@@ -0,0 +1,45 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"mime/multipart"
|
||||
"os"
|
||||
)
|
||||
|
||||
func FormatFileSize(i int64) string {
|
||||
const (
|
||||
KB = 1024
|
||||
MB = KB * 1024
|
||||
GB = MB * 1024
|
||||
TB = GB * 1024
|
||||
)
|
||||
switch {
|
||||
case i >= TB:
|
||||
return fmt.Sprintf("%.2fTB", float64(i)/float64(TB))
|
||||
case i >= GB:
|
||||
return fmt.Sprintf("%.2fGB", float64(i)/float64(GB))
|
||||
case i >= MB:
|
||||
return fmt.Sprintf("%.2fMB", float64(i)/float64(MB))
|
||||
case i >= KB:
|
||||
return fmt.Sprintf("%.2fKB", float64(i)/float64(KB))
|
||||
default:
|
||||
return fmt.Sprintf("%dB", i)
|
||||
}
|
||||
}
|
||||
|
||||
// SaveUploadedFile 保存 multipart 上传的文件到指定路径
|
||||
func SaveUploadedFile(fh *multipart.FileHeader, dst string) error {
|
||||
src, err := fh.Open()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer src.Close()
|
||||
out, err := os.Create(dst)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer out.Close()
|
||||
_, err = io.Copy(out, src)
|
||||
return err
|
||||
}
|
||||
366
backend/readme.md
Executable file
366
backend/readme.md
Executable file
@@ -0,0 +1,366 @@
|
||||
# ZSP 后端管理系统
|
||||
|
||||
基于 Go + Gin + GORM + MySQL 构建的现代化合同管理与用户认证系统后端API。
|
||||
|
||||
## 📋 项目概述
|
||||
|
||||
ZSP 后端是一个用于供应链合同管理的企业级应用系统,提供用户认证、合同管理、文件存储等核心功能。系统采用微服务架构思想,具备高可扩展性和良好的开发体验。
|
||||
|
||||
### 主要特性
|
||||
- 🔐 **JWT 用户认证与授权**
|
||||
- 📄 **合同全生命周期管理**
|
||||
- 🐳 **Docker 容器化开发环境**
|
||||
- 🔄 **热重载开发体验** (Air)
|
||||
- 📊 **MySQL 数据库支持**
|
||||
- 🧪 **自动化数据库迁移**
|
||||
- 🔧 **配置驱动架构**
|
||||
|
||||
## 🏗️ 技术栈
|
||||
|
||||
### 后端框架
|
||||
- **Go 1.25.4** - 高性能编程语言
|
||||
- **Gin** - 轻量级 Web 框架
|
||||
- **GORM** - ORM 数据库工具
|
||||
- **JWT** - 用户认证令牌
|
||||
|
||||
### 开发工具
|
||||
- **Docker & Docker Compose** - 容器化开发环境
|
||||
- **Air** - Go 应用热重载工具
|
||||
- **MySQL 8.0** - 关系型数据库
|
||||
|
||||
### 配置管理
|
||||
- **Viper** - 配置管理库
|
||||
- **YAML 配置文件** - 结构化配置
|
||||
|
||||
## 📁 项目结构
|
||||
|
||||
```
|
||||
zsp-backend/
|
||||
├── cmd/api/ # 应用入口
|
||||
│ └── main.go # 主程序入口
|
||||
├── configs/ # 配置文件
|
||||
│ └── config.dev.yaml # 开发环境配置
|
||||
├── internal/ # 内部包
|
||||
│ ├── handler/ # HTTP 处理器
|
||||
│ │ ├── user_handler.go # 用户相关接口
|
||||
│ │ └── contract_handler.go # 合同相关接口
|
||||
│ ├── middleware/ # 中间件
|
||||
│ │ └── auth.go # 认证中间件
|
||||
│ ├── model/ # 数据模型
|
||||
│ │ ├── user.go # 用户模型
|
||||
│ │ ├── contract.go # 合同模型
|
||||
│ │ └── error.go # 错误响应模型
|
||||
│ ├── repository/ # 数据访问层
|
||||
│ │ ├── user_repository.go
|
||||
│ │ └── contract_repository.go
|
||||
│ ├── service/ # 业务逻辑层
|
||||
│ │ ├── user_service.go
|
||||
│ │ └── contract_service.go
|
||||
│ └── server/ # 服务器配置
|
||||
│ └── router.go # 路由配置
|
||||
├── pkg/ # 公共包
|
||||
│ ├── config/ # 配置加载
|
||||
│ └── database/ # 数据库连接
|
||||
├── scripts/ # 脚本文件
|
||||
│ └── init_database.sql # 数据库初始化脚本
|
||||
├── migrations/ # 数据库迁移文件
|
||||
├── test/ # 测试文件
|
||||
├── web/ # 前端文件(可选)
|
||||
├── deployments/ # 部署配置
|
||||
├── docs/ # 文档
|
||||
├── tmp/ # 临时文件
|
||||
└── bin/ # 构建输出
|
||||
```
|
||||
|
||||
## 🚀 快速开始
|
||||
|
||||
### 前提条件
|
||||
- Docker & Docker Compose
|
||||
- Go 1.25.4+ (可选,用于本地开发)
|
||||
|
||||
### 使用 Docker 开发环境(推荐)
|
||||
|
||||
1. **克隆项目**
|
||||
```bash
|
||||
git clone <repository-url>
|
||||
cd zsp-backend
|
||||
```
|
||||
|
||||
2. **启动开发环境**
|
||||
```bash
|
||||
docker-compose -f docker-compose.dev.yml up --build
|
||||
```
|
||||
|
||||
3. **访问应用**
|
||||
- API 服务: http://localhost:8080
|
||||
- MySQL 数据库: localhost:3306
|
||||
|
||||
### 本地开发(不使用 Docker)
|
||||
|
||||
1. **安装依赖**
|
||||
```bash
|
||||
go mod download
|
||||
```
|
||||
|
||||
2. **启动 MySQL 数据库**
|
||||
```bash
|
||||
# 使用 Docker 启动 MySQL
|
||||
docker run --name mysql-dev -e MYSQL_ROOT_PASSWORD=password \
|
||||
-e MYSQL_DATABASE=myapp -p 3306:3306 -d mysql:8.0
|
||||
```
|
||||
|
||||
3. **运行应用**
|
||||
```bash
|
||||
# 使用 Air 热重载
|
||||
air -c .air.toml
|
||||
|
||||
# 或直接运行
|
||||
go run cmd/api/main.go
|
||||
```
|
||||
|
||||
## 🔧 配置说明
|
||||
|
||||
### 环境变量
|
||||
创建 `.env.dev` 文件(参考 `.env.dev.example`):
|
||||
```env
|
||||
# 数据库配置
|
||||
DB_HOST=localhost
|
||||
DB_PORT=3306
|
||||
DB_USER=root
|
||||
DB_PASSWORD=password
|
||||
DB_NAME=myapp
|
||||
|
||||
# JWT 配置
|
||||
JWT_SECRET=your-secret-key
|
||||
JWT_EXPIRE_HOURS=24
|
||||
```
|
||||
|
||||
### 配置文件 (`configs/config.dev.yaml`)
|
||||
```yaml
|
||||
server:
|
||||
port: 8080
|
||||
mode: "debug"
|
||||
read_timeout: 30
|
||||
write_timeout: 30
|
||||
|
||||
database:
|
||||
host: "mysql"
|
||||
port: 3306
|
||||
user: "root"
|
||||
password: "password"
|
||||
dbname: "myapp"
|
||||
max_open_conns: 100
|
||||
max_idle_conns: 10
|
||||
reset_database: true
|
||||
|
||||
jwt:
|
||||
secret: "your-jwt-secret-key"
|
||||
expire_hours: 24
|
||||
```
|
||||
|
||||
## 📖 API 文档
|
||||
|
||||
### 用户认证
|
||||
|
||||
#### 用户注册
|
||||
```http
|
||||
POST /api/v1/users/register
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"username": "testuser",
|
||||
"email": "test@example.com",
|
||||
"password": "password123"
|
||||
}
|
||||
```
|
||||
|
||||
#### 用户登录
|
||||
```http
|
||||
POST /api/v1/users/login
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"email": "test@example.com",
|
||||
"password": "password123"
|
||||
}
|
||||
```
|
||||
|
||||
响应:
|
||||
```json
|
||||
{
|
||||
"message": "Login successful",
|
||||
"user": {
|
||||
"id": 1,
|
||||
"username": "testuser",
|
||||
"email": "test@example.com"
|
||||
},
|
||||
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
|
||||
}
|
||||
```
|
||||
|
||||
### 合同管理
|
||||
|
||||
#### 创建合同
|
||||
```http
|
||||
POST /api/v1/contracts
|
||||
Authorization: Bearer <jwt-token>
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"name": "采购合同",
|
||||
"contract_type": "采购",
|
||||
"contract_code": "CG2024001",
|
||||
"buy_party": "买方公司",
|
||||
"sell_party": "卖方公司",
|
||||
"count_party": "计数方",
|
||||
"unit_price": "1000.00",
|
||||
"bl_number": "BL123456",
|
||||
"delivery_time": "2024-12-31",
|
||||
"pickup_time": "2024-12-30",
|
||||
"packaging": "标准包装",
|
||||
"remarks": "备注信息",
|
||||
"file_path": "/uploads/contract.pdf"
|
||||
}
|
||||
```
|
||||
|
||||
#### 获取合同列表
|
||||
```http
|
||||
GET /api/v1/contracts
|
||||
Authorization: Bearer <jwt-token>
|
||||
```
|
||||
|
||||
## 🗄️ 数据模型
|
||||
|
||||
### 用户表 (users)
|
||||
| 字段 | 类型 | 描述 |
|
||||
|------|------|------|
|
||||
| id | uint | 主键 |
|
||||
| username | string(50) | 用户名,唯一 |
|
||||
| email | string(100) | 邮箱,唯一 |
|
||||
| password | string(255) | 密码哈希 |
|
||||
| created_at | datetime | 创建时间 |
|
||||
| updated_at | datetime | 更新时间 |
|
||||
|
||||
### 合同表 (contracts)
|
||||
| 字段 | 类型 | 描述 |
|
||||
|------|------|------|
|
||||
| id | uint | 主键 |
|
||||
| name | string(100) | 合同名称 |
|
||||
| contract_type | string(50) | 合同类型 |
|
||||
| contract_code | string(50) | 合同编码 |
|
||||
| buy_party | string(100) | 买方 |
|
||||
| sell_party | string(100) | 卖方 |
|
||||
| count_party | string(100) | 计数方 |
|
||||
| unit_price | string(50) | 单价 |
|
||||
| bl_number | string(50) | 提单号 |
|
||||
| delivery_time | string(50) | 交付时间 |
|
||||
| pickup_time | string(50) | 提货时间 |
|
||||
| packaging | string(50) | 包装方式 |
|
||||
| remarks | string(255) | 备注 |
|
||||
| file_path | string(255) | 文件路径 |
|
||||
|
||||
## 🐳 Docker 开发环境
|
||||
|
||||
### 开发环境配置 (`docker-compose.dev.yml`)
|
||||
- **应用服务**: Go + Air 热重载
|
||||
- **数据库服务**: MySQL 8.0
|
||||
- **网络配置**: 自定义网络 `app-network`
|
||||
- **卷挂载**: 代码实时同步,Go 模块缓存
|
||||
|
||||
### 构建自定义镜像
|
||||
```bash
|
||||
# 构建开发镜像
|
||||
docker build -f Dockerfile.dev -t zsp-backend-dev .
|
||||
|
||||
# 运行容器
|
||||
docker run -p 8080:8080 -v $(pwd):/workspace zsp-backend-dev
|
||||
```
|
||||
|
||||
## 🧪 测试
|
||||
|
||||
### 运行测试
|
||||
```bash
|
||||
# 运行所有测试
|
||||
go test ./...
|
||||
|
||||
# 运行特定包测试
|
||||
go test ./internal/handler
|
||||
|
||||
# 运行测试并显示覆盖率
|
||||
go test -cover ./...
|
||||
```
|
||||
|
||||
### 测试数据库
|
||||
测试环境使用独立的数据库 `myapp_test`,通过初始化脚本自动创建。
|
||||
|
||||
## 🔄 数据库迁移
|
||||
|
||||
### 自动迁移
|
||||
在开发环境中,设置 `reset_database: true` 会自动执行数据库迁移:
|
||||
```go
|
||||
if cfg.Database.ResetDatabase {
|
||||
err = database.AutoMigrate(db)
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
### 手动迁移
|
||||
1. 创建迁移文件在 `migrations/` 目录
|
||||
2. 使用 GORM 的 `AutoMigrate` 或手动执行 SQL
|
||||
|
||||
## 📊 部署
|
||||
|
||||
### 生产环境构建
|
||||
```bash
|
||||
# 构建生产镜像
|
||||
docker build -t zsp-backend-prod .
|
||||
|
||||
# 使用生产配置
|
||||
docker run -p 8080:8080 \
|
||||
-v /path/to/config.yaml:/app/config.yaml \
|
||||
zsp-backend-prod
|
||||
```
|
||||
|
||||
### 环境变量配置
|
||||
生产环境建议使用环境变量覆盖配置文件:
|
||||
```bash
|
||||
export DB_HOST=production-db
|
||||
export DB_PASSWORD=secure-password
|
||||
export JWT_SECRET=production-secret
|
||||
```
|
||||
|
||||
## 🛠️ 开发工具
|
||||
|
||||
### Air 热重载配置 (`.air.toml`)
|
||||
- 自动检测文件变化并重新编译
|
||||
- 排除测试文件和构建目录
|
||||
- 自定义构建命令和输出目录
|
||||
|
||||
### 代码规范
|
||||
- 使用 `go fmt` 格式化代码
|
||||
- 遵循 Go 官方代码规范
|
||||
- 使用有意义的包和函数命名
|
||||
|
||||
## 🤝 贡献指南
|
||||
|
||||
1. Fork 项目
|
||||
2. 创建功能分支 (`git checkout -b feature/AmazingFeature`)
|
||||
3. 提交更改 (`git commit -m 'Add some AmazingFeature'`)
|
||||
4. 推送到分支 (`git push origin feature/AmazingFeature`)
|
||||
5. 创建 Pull Request
|
||||
|
||||
## 📄 许可证
|
||||
|
||||
本项目采用 MIT 许可证 - 查看 [LICENSE](LICENSE) 文件了解详情。
|
||||
|
||||
## 📞 支持
|
||||
|
||||
如有问题或建议,请:
|
||||
1. 查看 [Issues](https://github.com/rovina/zsp-backend/issues)
|
||||
2. 提交新的 Issue
|
||||
3. 联系项目维护者
|
||||
|
||||
---
|
||||
|
||||
**最后更新**: 2024年1月
|
||||
**版本**: 1.0.0-dev
|
||||
11
backend/scripts/init_database.sql
Executable file
11
backend/scripts/init_database.sql
Executable file
@@ -0,0 +1,11 @@
|
||||
CREATE DATABASE IF NOT EXISTS `myapp_dev` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
CREATE DATABASE IF NOT EXISTS `myapp_test` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
--- 创建专用用户
|
||||
|
||||
CREATE USER IF NOT EXISTS 'myapp_user'@'%' IDENTIFIED BY 'devpassword';
|
||||
GRANT ALL PRIVILEGES ON `myapp_dev`.* TO 'myapp_user'@'%';
|
||||
GRANT ALL PRIVILEGES ON `myapp_test`.* TO 'myapp_user'@'%';
|
||||
FLUSH PRIVILEGES;
|
||||
|
||||
USE `myapp_dev`;
|
||||
496
backend/test/company_handler_test.go
Normal file
496
backend/test/company_handler_test.go
Normal file
@@ -0,0 +1,496 @@
|
||||
package test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/rovina/zsp-backend/internal/handler"
|
||||
"github.com/rovina/zsp-backend/internal/model"
|
||||
"github.com/rovina/zsp-backend/internal/service"
|
||||
)
|
||||
|
||||
// mockCompanyService implements service.CompanyService for testing
|
||||
type mockCompanyService struct {
|
||||
createFolderFn func(name, description, category string) (*model.CompanyFolder, error)
|
||||
listFoldersFn func() ([]model.CompanyFolder, error)
|
||||
getFolderByIDFn func(id uint) (*model.CompanyFolder, error)
|
||||
updateFolderFn func(id uint, name, description, category string) error
|
||||
deleteFolderFn func(id uint) error
|
||||
|
||||
uploadFilesFn func(folderID *uint, companyName, fileCategory, expireDate, description string, files []*multipart.FileHeader) (int, error)
|
||||
listFilesFn func(folderID *uint) ([]model.CompanyFile, error)
|
||||
getFileByIDFn func(id uint) (*model.CompanyFile, error)
|
||||
updateFileFn func(id uint, companyName, fileCategory, expireDate, description string, folderID *uint) error
|
||||
deleteFileFn func(id uint) error
|
||||
}
|
||||
|
||||
func (m *mockCompanyService) CreateFolder(name, description, category string) (*model.CompanyFolder, error) {
|
||||
return m.createFolderFn(name, description, category)
|
||||
}
|
||||
func (m *mockCompanyService) ListFolders() ([]model.CompanyFolder, error) {
|
||||
return m.listFoldersFn()
|
||||
}
|
||||
func (m *mockCompanyService) GetFolderByID(id uint) (*model.CompanyFolder, error) {
|
||||
return m.getFolderByIDFn(id)
|
||||
}
|
||||
func (m *mockCompanyService) UpdateFolder(id uint, name, description, category string) error {
|
||||
return m.updateFolderFn(id, name, description, category)
|
||||
}
|
||||
func (m *mockCompanyService) DeleteFolder(id uint) error {
|
||||
return m.deleteFolderFn(id)
|
||||
}
|
||||
|
||||
func (m *mockCompanyService) UploadFiles(folderID *uint, companyName, fileCategory, expireDate, description string, files []*multipart.FileHeader) (int, error) {
|
||||
return m.uploadFilesFn(folderID, companyName, fileCategory, expireDate, description, files)
|
||||
}
|
||||
func (m *mockCompanyService) ListFiles(folderID *uint) ([]model.CompanyFile, error) {
|
||||
return m.listFilesFn(folderID)
|
||||
}
|
||||
func (m *mockCompanyService) GetFileByID(id uint) (*model.CompanyFile, error) {
|
||||
return m.getFileByIDFn(id)
|
||||
}
|
||||
func (m *mockCompanyService) UpdateFile(id uint, companyName, fileCategory, expireDate, description string, folderID *uint) error {
|
||||
return m.updateFileFn(id, companyName, fileCategory, expireDate, description, folderID)
|
||||
}
|
||||
func (m *mockCompanyService) DeleteFile(id uint) error {
|
||||
return m.deleteFileFn(id)
|
||||
}
|
||||
|
||||
var _ service.CompanyService = (*mockCompanyService)(nil)
|
||||
|
||||
func setupCompanyRouter(h *handler.CompanyHandler) *gin.Engine {
|
||||
gin.SetMode(gin.TestMode)
|
||||
r := gin.New()
|
||||
api := r.Group("/api")
|
||||
{
|
||||
company := api.Group("/company")
|
||||
{
|
||||
company.GET("/folders", h.ListFolders)
|
||||
company.POST("/folders", h.CreateFolder)
|
||||
company.PUT("/folders/:id", h.UpdateFolder)
|
||||
company.DELETE("/folders/:id", h.DeleteFolder)
|
||||
|
||||
company.GET("/files", h.ListFiles)
|
||||
company.POST("/files/upload", h.UploadFiles)
|
||||
company.PUT("/files/:id", h.UpdateFile)
|
||||
company.DELETE("/files/:id", h.DeleteFile)
|
||||
}
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
// ========== Folder Tests ==========
|
||||
|
||||
func TestCompanyListFolders_Success(t *testing.T) {
|
||||
mock := &mockCompanyService{
|
||||
listFoldersFn: func() ([]model.CompanyFolder, error) {
|
||||
return []model.CompanyFolder{
|
||||
{ID: 1, Name: "营业执照文件夹", Category: "license"},
|
||||
{ID: 2, Name: "开票信息文件夹", Category: "invoice"},
|
||||
}, nil
|
||||
},
|
||||
}
|
||||
h := handler.NewCompanyHandler(mock)
|
||||
router := setupCompanyRouter(h)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("GET", "/api/company/folders", nil)
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("expected status 200, got %d", w.Code)
|
||||
}
|
||||
|
||||
var resp map[string]interface{}
|
||||
json.Unmarshal(w.Body.Bytes(), &resp)
|
||||
if resp["success"] != true {
|
||||
t.Errorf("expected success true, got %v", resp["success"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestCompanyListFolders_Error(t *testing.T) {
|
||||
mock := &mockCompanyService{
|
||||
listFoldersFn: func() ([]model.CompanyFolder, error) {
|
||||
return nil, errors.New("database error")
|
||||
},
|
||||
}
|
||||
h := handler.NewCompanyHandler(mock)
|
||||
router := setupCompanyRouter(h)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("GET", "/api/company/folders", nil)
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusInternalServerError {
|
||||
t.Errorf("expected status 500, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCompanyCreateFolder_Success(t *testing.T) {
|
||||
mock := &mockCompanyService{
|
||||
createFolderFn: func(name, description, category string) (*model.CompanyFolder, error) {
|
||||
return &model.CompanyFolder{ID: 1, Name: name, Description: description, Category: category}, nil
|
||||
},
|
||||
}
|
||||
h := handler.NewCompanyHandler(mock)
|
||||
router := setupCompanyRouter(h)
|
||||
|
||||
body := `{"name":"测试文件夹","description":"测试描述","category":"license"}`
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("POST", "/api/company/folders", bytes.NewReader([]byte(body)))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("expected status 200, got %d", w.Code)
|
||||
}
|
||||
|
||||
var resp map[string]interface{}
|
||||
json.Unmarshal(w.Body.Bytes(), &resp)
|
||||
if resp["success"] != true {
|
||||
t.Errorf("expected success true, got %v", resp["success"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestCompanyCreateFolder_BadRequest(t *testing.T) {
|
||||
mock := &mockCompanyService{
|
||||
createFolderFn: func(name, description, category string) (*model.CompanyFolder, error) {
|
||||
return nil, nil
|
||||
},
|
||||
}
|
||||
h := handler.NewCompanyHandler(mock)
|
||||
router := setupCompanyRouter(h)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("POST", "/api/company/folders", bytes.NewReader([]byte("{}")))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusBadRequest {
|
||||
t.Errorf("expected status 400 for missing required field, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCompanyCreateFolder_Error(t *testing.T) {
|
||||
mock := &mockCompanyService{
|
||||
createFolderFn: func(name, description, category string) (*model.CompanyFolder, error) {
|
||||
return nil, errors.New("folder already exists")
|
||||
},
|
||||
}
|
||||
h := handler.NewCompanyHandler(mock)
|
||||
router := setupCompanyRouter(h)
|
||||
|
||||
body := `{"name":"测试文件夹","description":"测试描述","category":"license"}`
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("POST", "/api/company/folders", bytes.NewReader([]byte(body)))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusInternalServerError {
|
||||
t.Errorf("expected status 500, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCompanyUpdateFolder_Success(t *testing.T) {
|
||||
mock := &mockCompanyService{
|
||||
updateFolderFn: func(id uint, name, description, category string) error {
|
||||
return nil
|
||||
},
|
||||
}
|
||||
h := handler.NewCompanyHandler(mock)
|
||||
router := setupCompanyRouter(h)
|
||||
|
||||
body := `{"name":"更新文件夹","description":"更新描述","category":"invoice"}`
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("PUT", "/api/company/folders/1", bytes.NewReader([]byte(body)))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("expected status 200, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCompanyUpdateFolder_InvalidID(t *testing.T) {
|
||||
mock := &mockCompanyService{
|
||||
updateFolderFn: func(id uint, name, description, category string) error {
|
||||
return nil
|
||||
},
|
||||
}
|
||||
h := handler.NewCompanyHandler(mock)
|
||||
router := setupCompanyRouter(h)
|
||||
|
||||
body := `{"name":"更新文件夹","description":"更新描述","category":"invoice"}`
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("PUT", "/api/company/folders/invalid", bytes.NewReader([]byte(body)))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusBadRequest {
|
||||
t.Errorf("expected status 400 for invalid id, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCompanyUpdateFolder_Error(t *testing.T) {
|
||||
mock := &mockCompanyService{
|
||||
updateFolderFn: func(id uint, name, description, category string) error {
|
||||
return errors.New("folder not found")
|
||||
},
|
||||
}
|
||||
h := handler.NewCompanyHandler(mock)
|
||||
router := setupCompanyRouter(h)
|
||||
|
||||
body := `{"name":"更新文件夹","description":"更新描述","category":"invoice"}`
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("PUT", "/api/company/folders/999", bytes.NewReader([]byte(body)))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusInternalServerError {
|
||||
t.Errorf("expected status 500, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCompanyDeleteFolder_Success(t *testing.T) {
|
||||
mock := &mockCompanyService{
|
||||
deleteFolderFn: func(id uint) error {
|
||||
return nil
|
||||
},
|
||||
}
|
||||
h := handler.NewCompanyHandler(mock)
|
||||
router := setupCompanyRouter(h)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("DELETE", "/api/company/folders/1", nil)
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("expected status 200, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCompanyDeleteFolder_InvalidID(t *testing.T) {
|
||||
mock := &mockCompanyService{
|
||||
deleteFolderFn: func(id uint) error {
|
||||
return nil
|
||||
},
|
||||
}
|
||||
h := handler.NewCompanyHandler(mock)
|
||||
router := setupCompanyRouter(h)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("DELETE", "/api/company/folders/invalid", nil)
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusBadRequest {
|
||||
t.Errorf("expected status 400 for invalid id, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCompanyDeleteFolder_Error(t *testing.T) {
|
||||
mock := &mockCompanyService{
|
||||
deleteFolderFn: func(id uint) error {
|
||||
return errors.New("folder not found")
|
||||
},
|
||||
}
|
||||
h := handler.NewCompanyHandler(mock)
|
||||
router := setupCompanyRouter(h)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("DELETE", "/api/company/folders/999", nil)
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusInternalServerError {
|
||||
t.Errorf("expected status 500, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
// ========== File Tests ==========
|
||||
|
||||
func TestCompanyListFiles_Success(t *testing.T) {
|
||||
mock := &mockCompanyService{
|
||||
listFilesFn: func(folderID *uint) ([]model.CompanyFile, error) {
|
||||
return []model.CompanyFile{
|
||||
{ID: 1, FileName: "营业执照.pdf", CompanyName: "公司A", FileCategory: "license"},
|
||||
{ID: 2, FileName: "开票信息.pdf", CompanyName: "公司A", FileCategory: "invoice"},
|
||||
}, nil
|
||||
},
|
||||
}
|
||||
h := handler.NewCompanyHandler(mock)
|
||||
router := setupCompanyRouter(h)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("GET", "/api/company/files", nil)
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("expected status 200, got %d", w.Code)
|
||||
}
|
||||
|
||||
var resp map[string]interface{}
|
||||
json.Unmarshal(w.Body.Bytes(), &resp)
|
||||
if resp["success"] != true {
|
||||
t.Errorf("expected success true, got %v", resp["success"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestCompanyListFiles_WithFolderID(t *testing.T) {
|
||||
mock := &mockCompanyService{
|
||||
listFilesFn: func(folderID *uint) ([]model.CompanyFile, error) {
|
||||
if folderID != nil && *folderID != 1 {
|
||||
t.Errorf("expected folderID 1, got %v", folderID)
|
||||
}
|
||||
return []model.CompanyFile{
|
||||
{ID: 1, FileName: "营业执照.pdf", FolderID: 1},
|
||||
}, nil
|
||||
},
|
||||
}
|
||||
h := handler.NewCompanyHandler(mock)
|
||||
router := setupCompanyRouter(h)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("GET", "/api/company/files?folderId=1", nil)
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("expected status 200, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCompanyListFiles_Error(t *testing.T) {
|
||||
mock := &mockCompanyService{
|
||||
listFilesFn: func(folderID *uint) ([]model.CompanyFile, error) {
|
||||
return nil, errors.New("database error")
|
||||
},
|
||||
}
|
||||
h := handler.NewCompanyHandler(mock)
|
||||
router := setupCompanyRouter(h)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("GET", "/api/company/files", nil)
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusInternalServerError {
|
||||
t.Errorf("expected status 500, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCompanyUpdateFile_Success(t *testing.T) {
|
||||
mock := &mockCompanyService{
|
||||
updateFileFn: func(id uint, companyName, fileCategory, expireDate, description string, folderID *uint) error {
|
||||
return nil
|
||||
},
|
||||
}
|
||||
h := handler.NewCompanyHandler(mock)
|
||||
router := setupCompanyRouter(h)
|
||||
|
||||
body := `{"companyName":"公司B","fileCategory":"license","expireDate":"2025-12-31","description":"更新描述"}`
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("PUT", "/api/company/files/1", bytes.NewReader([]byte(body)))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("expected status 200, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCompanyUpdateFile_InvalidID(t *testing.T) {
|
||||
mock := &mockCompanyService{
|
||||
updateFileFn: func(id uint, companyName, fileCategory, expireDate, description string, folderID *uint) error {
|
||||
return nil
|
||||
},
|
||||
}
|
||||
h := handler.NewCompanyHandler(mock)
|
||||
router := setupCompanyRouter(h)
|
||||
|
||||
body := `{"companyName":"公司B","fileCategory":"license"}`
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("PUT", "/api/company/files/invalid", bytes.NewReader([]byte(body)))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusBadRequest {
|
||||
t.Errorf("expected status 400 for invalid id, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCompanyUpdateFile_Error(t *testing.T) {
|
||||
mock := &mockCompanyService{
|
||||
updateFileFn: func(id uint, companyName, fileCategory, expireDate, description string, folderID *uint) error {
|
||||
return errors.New("file not found")
|
||||
},
|
||||
}
|
||||
h := handler.NewCompanyHandler(mock)
|
||||
router := setupCompanyRouter(h)
|
||||
|
||||
body := `{"companyName":"公司B","fileCategory":"license"}`
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("PUT", "/api/company/files/999", bytes.NewReader([]byte(body)))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusInternalServerError {
|
||||
t.Errorf("expected status 500, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCompanyDeleteFile_Success(t *testing.T) {
|
||||
mock := &mockCompanyService{
|
||||
deleteFileFn: func(id uint) error {
|
||||
return nil
|
||||
},
|
||||
}
|
||||
h := handler.NewCompanyHandler(mock)
|
||||
router := setupCompanyRouter(h)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("DELETE", "/api/company/files/1", nil)
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("expected status 200, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCompanyDeleteFile_InvalidID(t *testing.T) {
|
||||
mock := &mockCompanyService{
|
||||
deleteFileFn: func(id uint) error {
|
||||
return nil
|
||||
},
|
||||
}
|
||||
h := handler.NewCompanyHandler(mock)
|
||||
router := setupCompanyRouter(h)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("DELETE", "/api/company/files/invalid", nil)
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusBadRequest {
|
||||
t.Errorf("expected status 400 for invalid id, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCompanyDeleteFile_Error(t *testing.T) {
|
||||
mock := &mockCompanyService{
|
||||
deleteFileFn: func(id uint) error {
|
||||
return errors.New("file not found")
|
||||
},
|
||||
}
|
||||
h := handler.NewCompanyHandler(mock)
|
||||
router := setupCompanyRouter(h)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("DELETE", "/api/company/files/999", nil)
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusInternalServerError {
|
||||
t.Errorf("expected status 500, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
216
backend/test/contract_handler_test.go
Executable file
216
backend/test/contract_handler_test.go
Executable file
@@ -0,0 +1,216 @@
|
||||
package test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/rovina/zsp-backend/internal/handler"
|
||||
"github.com/rovina/zsp-backend/internal/model"
|
||||
)
|
||||
|
||||
// ----- mock contract services -----
|
||||
type mockContractService struct {
|
||||
listFn func(contractType string) ([]model.Contract, error)
|
||||
getByIDFn func(id uint) (*model.Contract, error)
|
||||
}
|
||||
|
||||
func (m *mockContractService) CreateContract(contract *model.Contract, files []model.ContractFile) error { return nil }
|
||||
func (m *mockContractService) SaveFiles(files []model.ContractFile) error { return nil }
|
||||
func (m *mockContractService) BatchSaveFiles(basePath string, files []*multipart.FileHeader) (int, error) { return 0, nil }
|
||||
func (m *mockContractService) List(contractType string) ([]model.Contract, error) { return m.listFn(contractType) }
|
||||
func (m *mockContractService) GetByID(id uint) (*model.Contract, error) { return m.getByIDFn(id) }
|
||||
func (m *mockContractService) Update(contract *model.Contract) error { return nil }
|
||||
func (m *mockContractService) UpdateWithFiles(id uint, keepFileIDs []uint, newFileModels []model.ContractFile, form map[string]string) error {
|
||||
return nil
|
||||
}
|
||||
func (m *mockContractService) Delete(id uint) error { return nil }
|
||||
|
||||
type mockContractFolderService struct {
|
||||
listFn func() ([]model.ContractFolder, error)
|
||||
createFn func(name, description string) (*model.ContractFolder, error)
|
||||
getByIDFn func(id uint) (*model.ContractFolder, error)
|
||||
updateFn func(id uint, name, description string) error
|
||||
deleteFn func(id uint) error
|
||||
}
|
||||
|
||||
func (m *mockContractFolderService) List() ([]model.ContractFolder, error) { return m.listFn() }
|
||||
func (m *mockContractFolderService) Create(name, description string) (*model.ContractFolder, error) { return m.createFn(name, description) }
|
||||
func (m *mockContractFolderService) GetByID(id uint) (*model.ContractFolder, error) { return m.getByIDFn(id) }
|
||||
func (m *mockContractFolderService) Update(id uint, name, description string) error { return m.updateFn(id, name, description) }
|
||||
func (m *mockContractFolderService) Delete(id uint) error { return m.deleteFn(id) }
|
||||
|
||||
type mockContractBatchService struct {
|
||||
listFn func(folderID *uint) ([]model.ContractBatch, error)
|
||||
getByIDFn func(id uint) (*model.ContractBatch, error)
|
||||
deleteFn func(id uint) error
|
||||
}
|
||||
|
||||
func (m *mockContractBatchService) List(folderID *uint) ([]model.ContractBatch, error) { return m.listFn(folderID) }
|
||||
func (m *mockContractBatchService) GetByID(id uint) (*model.ContractBatch, error) { return m.getByIDFn(id) }
|
||||
func (m *mockContractBatchService) Create(folderID uint, batchName, remark string, files []model.ContractBatchFile) (*model.ContractBatch, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (m *mockContractBatchService) Delete(id uint) error { return m.deleteFn(id) }
|
||||
|
||||
func setupContractRouter(h *handler.ContractHandler) *gin.Engine {
|
||||
gin.SetMode(gin.TestMode)
|
||||
r := gin.New()
|
||||
api := r.Group("/api")
|
||||
{
|
||||
contracts := api.Group("/contract")
|
||||
{
|
||||
contracts.GET("", h.List)
|
||||
contracts.GET("/folders", h.ListFolders)
|
||||
contracts.POST("/folders", h.CreateFolder)
|
||||
contracts.GET("/folders/:id", h.GetFolder)
|
||||
contracts.PUT("/folders/:id", h.UpdateFolder)
|
||||
contracts.DELETE("/folders/:id", h.DeleteFolder)
|
||||
contracts.GET("/:id", h.Get)
|
||||
}
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
func TestContractList_Success(t *testing.T) {
|
||||
mockContract := &mockContractService{
|
||||
listFn: func(contractType string) ([]model.Contract, error) {
|
||||
return []model.Contract{
|
||||
{ID: 1, Name: "合同1", ContractCode: "C001"},
|
||||
{ID: 2, Name: "合同2", ContractCode: "C002"},
|
||||
}, nil
|
||||
},
|
||||
}
|
||||
mockFolder := &mockContractFolderService{
|
||||
listFn: func() ([]model.ContractFolder, error) { return nil, nil },
|
||||
}
|
||||
mockBatch := &mockContractBatchService{
|
||||
listFn: func(folderID *uint) ([]model.ContractBatch, error) { return nil, nil },
|
||||
}
|
||||
|
||||
h := handler.NewContractHandler(mockContract, mockFolder, mockBatch)
|
||||
router := setupContractRouter(h)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("GET", "/api/contract", nil)
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("expected status 200, got %d", w.Code)
|
||||
}
|
||||
|
||||
var resp map[string]interface{}
|
||||
json.Unmarshal(w.Body.Bytes(), &resp)
|
||||
if resp["success"] != true {
|
||||
t.Errorf("expected success true, got %v", resp["success"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestContractGet_Success(t *testing.T) {
|
||||
mockContract := &mockContractService{
|
||||
getByIDFn: func(id uint) (*model.Contract, error) {
|
||||
return &model.Contract{ID: 1, Name: "合同1", ContractCode: "C001"}, nil
|
||||
},
|
||||
}
|
||||
mockFolder := &mockContractFolderService{
|
||||
listFn: func() ([]model.ContractFolder, error) { return nil, nil },
|
||||
}
|
||||
mockBatch := &mockContractBatchService{
|
||||
listFn: func(folderID *uint) ([]model.ContractBatch, error) { return nil, nil },
|
||||
}
|
||||
|
||||
h := handler.NewContractHandler(mockContract, mockFolder, mockBatch)
|
||||
router := setupContractRouter(h)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("GET", "/api/contract/1", nil)
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("expected status 200, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestContractGet_NotFound(t *testing.T) {
|
||||
mockContract := &mockContractService{
|
||||
getByIDFn: func(id uint) (*model.Contract, error) {
|
||||
return nil, errors.New("contract not found")
|
||||
},
|
||||
}
|
||||
mockFolder := &mockContractFolderService{
|
||||
listFn: func() ([]model.ContractFolder, error) { return nil, nil },
|
||||
}
|
||||
mockBatch := &mockContractBatchService{
|
||||
listFn: func(folderID *uint) ([]model.ContractBatch, error) { return nil, nil },
|
||||
}
|
||||
|
||||
h := handler.NewContractHandler(mockContract, mockFolder, mockBatch)
|
||||
router := setupContractRouter(h)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("GET", "/api/contract/999", nil)
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusNotFound {
|
||||
t.Errorf("expected status 404, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestContractCreateFolder_Success(t *testing.T) {
|
||||
mockContract := &mockContractService{}
|
||||
mockFolder := &mockContractFolderService{
|
||||
createFn: func(name, description string) (*model.ContractFolder, error) {
|
||||
return &model.ContractFolder{ID: 1, Name: name, Description: description}, nil
|
||||
},
|
||||
}
|
||||
mockBatch := &mockContractBatchService{}
|
||||
|
||||
h := handler.NewContractHandler(mockContract, mockFolder, mockBatch)
|
||||
router := setupContractRouter(h)
|
||||
|
||||
body := `{"name":"测试文件夹","description":"测试描述"}`
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("POST", "/api/contract/folders", nil)
|
||||
req.Body = http.NoBody
|
||||
_ = body // body is unused in this simple test; the handler uses ShouldBindJSON
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
// Without valid JSON body, this should return 400
|
||||
if w.Code != http.StatusBadRequest {
|
||||
t.Errorf("expected status 400 for missing body, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestContractListFolders_Success(t *testing.T) {
|
||||
mockContract := &mockContractService{}
|
||||
mockFolder := &mockContractFolderService{
|
||||
listFn: func() ([]model.ContractFolder, error) {
|
||||
return []model.ContractFolder{
|
||||
{ID: 1, Name: "文件夹1"},
|
||||
{ID: 2, Name: "文件夹2"},
|
||||
}, nil
|
||||
},
|
||||
}
|
||||
mockBatch := &mockContractBatchService{}
|
||||
|
||||
h := handler.NewContractHandler(mockContract, mockFolder, mockBatch)
|
||||
router := setupContractRouter(h)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("GET", "/api/contract/folders", nil)
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("expected status 200, got %d", w.Code)
|
||||
}
|
||||
|
||||
var resp map[string]interface{}
|
||||
json.Unmarshal(w.Body.Bytes(), &resp)
|
||||
if resp["success"] != true {
|
||||
t.Errorf("expected success true, got %v", resp["success"])
|
||||
}
|
||||
}
|
||||
375
backend/test/delivery_handler_test.go
Executable file
375
backend/test/delivery_handler_test.go
Executable file
@@ -0,0 +1,375 @@
|
||||
package test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/rovina/zsp-backend/internal/handler"
|
||||
"github.com/rovina/zsp-backend/internal/model"
|
||||
"github.com/rovina/zsp-backend/internal/service"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// mockDeliveryService implements service.DeliveryService for testing
|
||||
type mockDeliveryService struct {
|
||||
listApplyFn func(page, pageSize int, orderNumber, blNumber, status string) ([]model.DeliveryApply, int64, error)
|
||||
getApplyByIDFn func(id uint) (*model.DeliveryApply, error)
|
||||
createApplyFn func(e *model.DeliveryApply) error
|
||||
deleteApplyFn func(id uint) error
|
||||
listOutboundFn func(page, pageSize int, outboundNumber, blNumber, status string) ([]model.DeliveryOutbound, int64, error)
|
||||
getOutboundByIDFn func(id uint) (*model.DeliveryOutbound, error)
|
||||
createOutboundFn func(e *model.DeliveryOutbound) error
|
||||
deleteOutboundFn func(id uint) error
|
||||
listWarehousesFn func() ([]model.Warehouse, error)
|
||||
createWarehouseFn func(e *model.Warehouse) error
|
||||
deleteWarehouseFn func(id uint) error
|
||||
listInboundFn func(page, pageSize int, warehouse, commodity string) ([]model.InventoryInbound, int64, error)
|
||||
createInboundFn func(e *model.InventoryInbound) error
|
||||
deleteInboundFn func(id uint) error
|
||||
listOwnershipFn func(page, pageSize int, transferNumber, blNumber, status string) ([]model.OwnershipTransfer, int64, error)
|
||||
deleteOwnershipFn func(id uint) error
|
||||
dbFn func() *gorm.DB
|
||||
}
|
||||
|
||||
func (m *mockDeliveryService) ListApply(page, pageSize int, orderNumber, blNumber, status string) ([]model.DeliveryApply, int64, error) {
|
||||
return m.listApplyFn(page, pageSize, orderNumber, blNumber, status)
|
||||
}
|
||||
func (m *mockDeliveryService) GetApplyByID(id uint) (*model.DeliveryApply, error) { return m.getApplyByIDFn(id) }
|
||||
func (m *mockDeliveryService) CreateApply(e *model.DeliveryApply) error { return m.createApplyFn(e) }
|
||||
func (m *mockDeliveryService) UpdateApply(e *model.DeliveryApply) error { return nil }
|
||||
func (m *mockDeliveryService) DeleteApply(id uint) error { return m.deleteApplyFn(id) }
|
||||
|
||||
func (m *mockDeliveryService) ListOutbound(page, pageSize int, outboundNumber, blNumber, status string) ([]model.DeliveryOutbound, int64, error) {
|
||||
return m.listOutboundFn(page, pageSize, outboundNumber, blNumber, status)
|
||||
}
|
||||
func (m *mockDeliveryService) GetOutboundByID(id uint) (*model.DeliveryOutbound, error) { return m.getOutboundByIDFn(id) }
|
||||
func (m *mockDeliveryService) CreateOutbound(e *model.DeliveryOutbound) error { return m.createOutboundFn(e) }
|
||||
func (m *mockDeliveryService) UpdateOutbound(e *model.DeliveryOutbound) error { return nil }
|
||||
func (m *mockDeliveryService) DeleteOutbound(id uint) error { return m.deleteOutboundFn(id) }
|
||||
|
||||
func (m *mockDeliveryService) ListWarehouses() ([]model.Warehouse, error) { return m.listWarehousesFn() }
|
||||
func (m *mockDeliveryService) GetWarehouseByID(id uint) (*model.Warehouse, error) { return nil, nil }
|
||||
func (m *mockDeliveryService) CreateWarehouse(e *model.Warehouse) error { return m.createWarehouseFn(e) }
|
||||
func (m *mockDeliveryService) UpdateWarehouse(e *model.Warehouse) error { return nil }
|
||||
func (m *mockDeliveryService) DeleteWarehouse(id uint) error { return m.deleteWarehouseFn(id) }
|
||||
|
||||
func (m *mockDeliveryService) ListInbound(page, pageSize int, warehouse, commodity string) ([]model.InventoryInbound, int64, error) {
|
||||
return m.listInboundFn(page, pageSize, warehouse, commodity)
|
||||
}
|
||||
func (m *mockDeliveryService) GetInboundByID(id uint) (*model.InventoryInbound, error) { return nil, nil }
|
||||
func (m *mockDeliveryService) CreateInbound(e *model.InventoryInbound) error { return m.createInboundFn(e) }
|
||||
func (m *mockDeliveryService) UpdateInbound(e *model.InventoryInbound) error { return nil }
|
||||
func (m *mockDeliveryService) DeleteInbound(id uint) error { return m.deleteInboundFn(id) }
|
||||
|
||||
func (m *mockDeliveryService) ListOwnership(page, pageSize int, transferNumber, blNumber, status string) ([]model.OwnershipTransfer, int64, error) {
|
||||
return m.listOwnershipFn(page, pageSize, transferNumber, blNumber, status)
|
||||
}
|
||||
func (m *mockDeliveryService) GetOwnershipByID(id uint) (*model.OwnershipTransfer, error) { return nil, nil }
|
||||
func (m *mockDeliveryService) CreateOwnership(e *model.OwnershipTransfer) error { return nil }
|
||||
func (m *mockDeliveryService) DeleteOwnership(id uint) error { return m.deleteOwnershipFn(id) }
|
||||
func (m *mockDeliveryService) CreateOwnershipFiles(files []model.OwnershipTransferFile) error { return nil }
|
||||
func (m *mockDeliveryService) DB() *gorm.DB { return nil }
|
||||
|
||||
var _ service.DeliveryService = (*mockDeliveryService)(nil)
|
||||
|
||||
func setupDeliveryRouter(h *handler.DeliveryHandler) *gin.Engine {
|
||||
gin.SetMode(gin.TestMode)
|
||||
r := gin.New()
|
||||
api := r.Group("/api")
|
||||
{
|
||||
applyDelivery := api.Group("/apply-delivery")
|
||||
{
|
||||
applyDelivery.GET("", h.ListApply)
|
||||
applyDelivery.POST("", h.CreateApply)
|
||||
applyDelivery.DELETE("/:id", h.DeleteApply)
|
||||
}
|
||||
deliveryDetails := api.Group("/delivery-details")
|
||||
{
|
||||
deliveryDetails.GET("", h.ListOutbound)
|
||||
deliveryDetails.POST("", h.CreateOutbound)
|
||||
deliveryDetails.DELETE("/:id", h.DeleteOutbound)
|
||||
}
|
||||
inventory := api.Group("/inventory")
|
||||
{
|
||||
inventory.GET("/warehouses", h.ListWarehouses)
|
||||
inventory.POST("/warehouses", h.CreateWarehouse)
|
||||
inventory.DELETE("/warehouses/:id", h.DeleteWarehouse)
|
||||
inventory.POST("/inbound", h.CreateInbound)
|
||||
inventory.DELETE("/inbound/:id", h.DeleteInbound)
|
||||
}
|
||||
ownershipTransfer := api.Group("/ownership-transfer")
|
||||
{
|
||||
ownershipTransfer.GET("", h.ListOwnership)
|
||||
ownershipTransfer.DELETE("/:id", h.DeleteOwnership)
|
||||
}
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
// ========== 我要提货 /api/apply-delivery ==========
|
||||
|
||||
func TestDeliveryListApply_Success(t *testing.T) {
|
||||
mock := &mockDeliveryService{
|
||||
listApplyFn: func(page, pageSize int, orderNumber, blNumber, status string) ([]model.DeliveryApply, int64, error) {
|
||||
return []model.DeliveryApply{
|
||||
{ID: 1, OrderNumber: "DA001", Status: "pending"},
|
||||
{ID: 2, OrderNumber: "DA002", Status: "approved"},
|
||||
}, 2, nil
|
||||
},
|
||||
}
|
||||
h := handler.NewDeliveryHandler(mock)
|
||||
router := setupDeliveryRouter(h)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("GET", "/api/apply-delivery?page=1&pageSize=10", nil)
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("expected status 200, got %d", w.Code)
|
||||
}
|
||||
|
||||
var resp map[string]interface{}
|
||||
json.Unmarshal(w.Body.Bytes(), &resp)
|
||||
if resp["success"] != true {
|
||||
t.Errorf("expected success true, got %v", resp["success"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeliveryCreateApply_Success(t *testing.T) {
|
||||
mock := &mockDeliveryService{
|
||||
createApplyFn: func(e *model.DeliveryApply) error {
|
||||
e.ID = 1
|
||||
e.OrderNumber = "DA1234567890"
|
||||
return nil
|
||||
},
|
||||
}
|
||||
h := handler.NewDeliveryHandler(mock)
|
||||
router := setupDeliveryRouter(h)
|
||||
|
||||
body := `{"blNumber":"BL001","commodity":"燕麦","quantity":100}`
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("POST", "/api/apply-delivery", bytes.NewReader([]byte(body)))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("expected status 200, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeliveryDeleteApply_Success(t *testing.T) {
|
||||
mock := &mockDeliveryService{
|
||||
deleteApplyFn: func(id uint) error { return nil },
|
||||
}
|
||||
h := handler.NewDeliveryHandler(mock)
|
||||
router := setupDeliveryRouter(h)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("DELETE", "/api/apply-delivery/1", nil)
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("expected status 200, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeliveryDeleteApply_NotFound(t *testing.T) {
|
||||
mock := &mockDeliveryService{
|
||||
deleteApplyFn: func(id uint) error { return errors.New("not found") },
|
||||
}
|
||||
h := handler.NewDeliveryHandler(mock)
|
||||
router := setupDeliveryRouter(h)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("DELETE", "/api/apply-delivery/999", nil)
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusInternalServerError {
|
||||
t.Errorf("expected status 500, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
// ========== 提货明细 /api/delivery-details ==========
|
||||
|
||||
func TestDeliveryListOutbound_Success(t *testing.T) {
|
||||
mock := &mockDeliveryService{
|
||||
listOutboundFn: func(page, pageSize int, outboundNumber, blNumber, status string) ([]model.DeliveryOutbound, int64, error) {
|
||||
return []model.DeliveryOutbound{
|
||||
{ID: 1, OutboundNumber: "OB001", OutboundStatus: "completed"},
|
||||
}, 1, nil
|
||||
},
|
||||
}
|
||||
h := handler.NewDeliveryHandler(mock)
|
||||
router := setupDeliveryRouter(h)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("GET", "/api/delivery-details?page=1&pageSize=10", nil)
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("expected status 200, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeliveryCreateOutbound_Success(t *testing.T) {
|
||||
mock := &mockDeliveryService{
|
||||
createOutboundFn: func(e *model.DeliveryOutbound) error {
|
||||
e.ID = 1
|
||||
e.OutboundNumber = "OB1234567890"
|
||||
return nil
|
||||
},
|
||||
}
|
||||
h := handler.NewDeliveryHandler(mock)
|
||||
router := setupDeliveryRouter(h)
|
||||
|
||||
body := `{"blNumber":"BL001","warehouse":"A库","quantity":50}`
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("POST", "/api/delivery-details", bytes.NewReader([]byte(body)))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("expected status 200, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeliveryDeleteOutbound_Success(t *testing.T) {
|
||||
mock := &mockDeliveryService{
|
||||
deleteOutboundFn: func(id uint) error { return nil },
|
||||
}
|
||||
h := handler.NewDeliveryHandler(mock)
|
||||
router := setupDeliveryRouter(h)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("DELETE", "/api/delivery-details/1", nil)
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("expected status 200, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
// ========== 库存 /api/inventory ==========
|
||||
|
||||
func TestDeliveryListWarehouses_Success(t *testing.T) {
|
||||
mock := &mockDeliveryService{
|
||||
listWarehousesFn: func() ([]model.Warehouse, error) {
|
||||
return []model.Warehouse{
|
||||
{ID: 1, Name: "A库", Status: "active"},
|
||||
{ID: 2, Name: "B库", Status: "active"},
|
||||
}, nil
|
||||
},
|
||||
}
|
||||
h := handler.NewDeliveryHandler(mock)
|
||||
router := setupDeliveryRouter(h)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("GET", "/api/inventory/warehouses", nil)
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("expected status 200, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeliveryCreateWarehouse_Success(t *testing.T) {
|
||||
mock := &mockDeliveryService{
|
||||
createWarehouseFn: func(e *model.Warehouse) error {
|
||||
e.ID = 1
|
||||
return nil
|
||||
},
|
||||
}
|
||||
h := handler.NewDeliveryHandler(mock)
|
||||
router := setupDeliveryRouter(h)
|
||||
|
||||
body := `{"name":"C库","location":"广州"}`
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("POST", "/api/inventory/warehouses", bytes.NewReader([]byte(body)))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("expected status 200, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeliveryDeleteWarehouse_Success(t *testing.T) {
|
||||
mock := &mockDeliveryService{
|
||||
deleteWarehouseFn: func(id uint) error { return nil },
|
||||
}
|
||||
h := handler.NewDeliveryHandler(mock)
|
||||
router := setupDeliveryRouter(h)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("DELETE", "/api/inventory/warehouses/1", nil)
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("expected status 200, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeliveryCreateInbound_Success(t *testing.T) {
|
||||
mock := &mockDeliveryService{
|
||||
createInboundFn: func(e *model.InventoryInbound) error {
|
||||
e.ID = 1
|
||||
e.InboundNumber = "IN1234567890"
|
||||
return nil
|
||||
},
|
||||
}
|
||||
h := handler.NewDeliveryHandler(mock)
|
||||
router := setupDeliveryRouter(h)
|
||||
|
||||
body := `{"warehouse":"A库","commodity":"燕麦","inboundWeight":1000}`
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("POST", "/api/inventory/inbound", bytes.NewReader([]byte(body)))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("expected status 200, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
// ========== 货权转移 /api/ownership-transfer ==========
|
||||
|
||||
func TestDeliveryListOwnership_Success(t *testing.T) {
|
||||
mock := &mockDeliveryService{
|
||||
listOwnershipFn: func(page, pageSize int, transferNumber, blNumber, status string) ([]model.OwnershipTransfer, int64, error) {
|
||||
return []model.OwnershipTransfer{
|
||||
{ID: 1, TransferNumber: "OT001", Status: "draft"},
|
||||
}, 1, nil
|
||||
},
|
||||
}
|
||||
h := handler.NewDeliveryHandler(mock)
|
||||
router := setupDeliveryRouter(h)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("GET", "/api/ownership-transfer?page=1&pageSize=10", nil)
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("expected status 200, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeliveryDeleteOwnership_Success(t *testing.T) {
|
||||
mock := &mockDeliveryService{
|
||||
deleteOwnershipFn: func(id uint) error { return nil },
|
||||
}
|
||||
h := handler.NewDeliveryHandler(mock)
|
||||
router := setupDeliveryRouter(h)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("DELETE", "/api/ownership-transfer/1", nil)
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("expected status 200, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
177
backend/test/user_handler_test.go
Executable file
177
backend/test/user_handler_test.go
Executable file
@@ -0,0 +1,177 @@
|
||||
package test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/rovina/zsp-backend/internal/dto"
|
||||
"github.com/rovina/zsp-backend/internal/handler"
|
||||
)
|
||||
|
||||
// mockUserService implements service.UserService for testing
|
||||
type mockUserService struct {
|
||||
registerFn func(req *dto.CreateUserRequest) (*dto.UserResponse, error)
|
||||
loginFn func(req *dto.LoginRequest) (*dto.UserResponse, string, error)
|
||||
getUserByIDFn func(id uint) (*dto.UserResponse, error)
|
||||
}
|
||||
|
||||
func (m *mockUserService) Register(req *dto.CreateUserRequest) (*dto.UserResponse, error) {
|
||||
return m.registerFn(req)
|
||||
}
|
||||
func (m *mockUserService) Login(req *dto.LoginRequest) (*dto.UserResponse, string, error) {
|
||||
return m.loginFn(req)
|
||||
}
|
||||
func (m *mockUserService) GetUserByID(id uint) (*dto.UserResponse, error) {
|
||||
return m.getUserByIDFn(id)
|
||||
}
|
||||
|
||||
func setupUserRouter(h *handler.UserHandler) *gin.Engine {
|
||||
gin.SetMode(gin.TestMode)
|
||||
r := gin.New()
|
||||
api := r.Group("/api")
|
||||
{
|
||||
auth := api.Group("/auth")
|
||||
{
|
||||
auth.POST("/register", h.Register)
|
||||
auth.POST("/login", h.Login)
|
||||
}
|
||||
users := api.Group("/users")
|
||||
{
|
||||
users.GET("/:id", h.GetUser)
|
||||
}
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
func TestUserRegister_Success(t *testing.T) {
|
||||
mock := &mockUserService{
|
||||
registerFn: func(req *dto.CreateUserRequest) (*dto.UserResponse, error) {
|
||||
return &dto.UserResponse{ID: 1, Username: "testuser", Email: "test@example.com"}, nil
|
||||
},
|
||||
}
|
||||
h := handler.NewUserHandler(mock)
|
||||
router := setupUserRouter(h)
|
||||
|
||||
body, _ := json.Marshal(dto.CreateUserRequest{Username: "testuser", Email: "test@example.com", Password: "password123"})
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("POST", "/api/auth/register", bytes.NewReader(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusCreated {
|
||||
t.Errorf("expected status 201, got %d", w.Code)
|
||||
}
|
||||
|
||||
var resp map[string]interface{}
|
||||
json.Unmarshal(w.Body.Bytes(), &resp)
|
||||
if resp["message"] != "User registered successfully" {
|
||||
t.Errorf("unexpected message: %v", resp["message"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestUserRegister_DuplicateEmail(t *testing.T) {
|
||||
mock := &mockUserService{
|
||||
registerFn: func(req *dto.CreateUserRequest) (*dto.UserResponse, error) {
|
||||
return nil, errors.New("email already exists")
|
||||
},
|
||||
}
|
||||
h := handler.NewUserHandler(mock)
|
||||
router := setupUserRouter(h)
|
||||
|
||||
body, _ := json.Marshal(dto.CreateUserRequest{Username: "testuser", Email: "existing@example.com", Password: "password123"})
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("POST", "/api/auth/register", bytes.NewReader(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusBadRequest {
|
||||
t.Errorf("expected status 400, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUserLogin_Success(t *testing.T) {
|
||||
mock := &mockUserService{
|
||||
loginFn: func(req *dto.LoginRequest) (*dto.UserResponse, string, error) {
|
||||
return &dto.UserResponse{ID: 1, Username: "testuser", Email: "test@example.com"}, "test-token", nil
|
||||
},
|
||||
}
|
||||
h := handler.NewUserHandler(mock)
|
||||
router := setupUserRouter(h)
|
||||
|
||||
body, _ := json.Marshal(dto.LoginRequest{Email: "test@example.com", Password: "password123"})
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("POST", "/api/auth/login", bytes.NewReader(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("expected status 200, got %d", w.Code)
|
||||
}
|
||||
|
||||
var resp map[string]interface{}
|
||||
json.Unmarshal(w.Body.Bytes(), &resp)
|
||||
if resp["token"] != "test-token" {
|
||||
t.Errorf("expected token 'test-token', got %v", resp["token"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestUserLogin_InvalidCredentials(t *testing.T) {
|
||||
mock := &mockUserService{
|
||||
loginFn: func(req *dto.LoginRequest) (*dto.UserResponse, string, error) {
|
||||
return nil, "", errors.New("invalid credentials")
|
||||
},
|
||||
}
|
||||
h := handler.NewUserHandler(mock)
|
||||
router := setupUserRouter(h)
|
||||
|
||||
body, _ := json.Marshal(dto.LoginRequest{Email: "wrong@example.com", Password: "wrong"})
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("POST", "/api/auth/login", bytes.NewReader(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusUnauthorized {
|
||||
t.Errorf("expected status 401, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUserGet_Success(t *testing.T) {
|
||||
mock := &mockUserService{
|
||||
getUserByIDFn: func(id uint) (*dto.UserResponse, error) {
|
||||
return &dto.UserResponse{ID: 1, Username: "testuser", Email: "test@example.com"}, nil
|
||||
},
|
||||
}
|
||||
h := handler.NewUserHandler(mock)
|
||||
router := setupUserRouter(h)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("GET", "/api/users/1", nil)
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("expected status 200, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUserGet_NotFound(t *testing.T) {
|
||||
mock := &mockUserService{
|
||||
getUserByIDFn: func(id uint) (*dto.UserResponse, error) {
|
||||
return nil, errors.New("user not found")
|
||||
},
|
||||
}
|
||||
h := handler.NewUserHandler(mock)
|
||||
router := setupUserRouter(h)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("GET", "/api/users/999", nil)
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusNotFound {
|
||||
t.Errorf("expected status 404, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
539
backend/test/zsp_contract_handler_test.go
Normal file
539
backend/test/zsp_contract_handler_test.go
Normal file
@@ -0,0 +1,539 @@
|
||||
package test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/rovina/zsp-backend/internal/dto"
|
||||
"github.com/rovina/zsp-backend/internal/handler"
|
||||
"github.com/rovina/zsp-backend/internal/model"
|
||||
"github.com/rovina/zsp-backend/internal/service"
|
||||
"mime/multipart"
|
||||
)
|
||||
|
||||
// mockZSPContractService implements service.ZSPContractService for testing
|
||||
type mockZSPContractService struct {
|
||||
createFolderFn func(req *dto.ZSPFolderCreateRequest) error
|
||||
getFoldersFn func() ([]model.ZSPContractFolder, error)
|
||||
getFolderByIdFn func(folderID string) (*model.ZSPContractFolder, error)
|
||||
deleteFolderByIdFn func(folderID string) error
|
||||
updateFolderByIdFn func(folderID string, req *dto.ZSPFolderUpdateRequest) error
|
||||
uploadContractFn func(req *dto.ZSPContractCreateRequest, fileHeader *multipart.FileHeader) error
|
||||
getContractByIdFn func(contractID string) (*model.ZSPContractFile, error)
|
||||
getContractWithDetailsFn func(contractID string) (*model.ZSPContractFile, error)
|
||||
listContractsFn func(folderID string) ([]model.ZSPContractFile, error)
|
||||
listContractsWithDetailsFn func(folderID string) ([]model.ZSPContractFile, error)
|
||||
deleteContractByIdFn func(contractID string) error
|
||||
updateContractByIdFn func(contractID string, req *dto.ZSPContractUpdateRequest) error
|
||||
createContractDetailFn func(req *dto.ZSPContractDetailCreateRequest) error
|
||||
updateContractDetailFn func(req *dto.ZSPContractDetailUpdateRequest) error
|
||||
getContractDetailByIdFn func(detailID string) (*model.ZSPContractDetail, error)
|
||||
listContractDetailsFn func(contractFileID string) ([]model.ZSPContractDetail, error)
|
||||
deleteContractDetailByIdFn func(detailID string) error
|
||||
batchCreateContractDetailsFn func(req *dto.ZSPContractDetailBatchCreateRequest) error
|
||||
}
|
||||
|
||||
func (m *mockZSPContractService) CreateFolder(req *dto.ZSPFolderCreateRequest) error {
|
||||
return m.createFolderFn(req)
|
||||
}
|
||||
func (m *mockZSPContractService) GetFolders() ([]model.ZSPContractFolder, error) {
|
||||
return m.getFoldersFn()
|
||||
}
|
||||
func (m *mockZSPContractService) GetFolderById(folderID string) (*model.ZSPContractFolder, error) {
|
||||
return m.getFolderByIdFn(folderID)
|
||||
}
|
||||
func (m *mockZSPContractService) DeleteFolderById(folderID string) error {
|
||||
return m.deleteFolderByIdFn(folderID)
|
||||
}
|
||||
func (m *mockZSPContractService) UpdateFolderById(folderID string, req *dto.ZSPFolderUpdateRequest) error {
|
||||
return m.updateFolderByIdFn(folderID, req)
|
||||
}
|
||||
func (m *mockZSPContractService) UploadContract(req *dto.ZSPContractCreateRequest, fileHeader *multipart.FileHeader) error {
|
||||
return m.uploadContractFn(req, fileHeader)
|
||||
}
|
||||
func (m *mockZSPContractService) GetContractById(contractID string) (*model.ZSPContractFile, error) {
|
||||
return m.getContractByIdFn(contractID)
|
||||
}
|
||||
func (m *mockZSPContractService) GetContractWithDetails(contractID string) (*model.ZSPContractFile, error) {
|
||||
return m.getContractWithDetailsFn(contractID)
|
||||
}
|
||||
func (m *mockZSPContractService) ListContracts(folderID string) ([]model.ZSPContractFile, error) {
|
||||
return m.listContractsFn(folderID)
|
||||
}
|
||||
func (m *mockZSPContractService) ListContractsWithDetails(folderID string) ([]model.ZSPContractFile, error) {
|
||||
return m.listContractsWithDetailsFn(folderID)
|
||||
}
|
||||
func (m *mockZSPContractService) DeleteContractById(contractID string) error {
|
||||
return m.deleteContractByIdFn(contractID)
|
||||
}
|
||||
func (m *mockZSPContractService) UpdateContract(contract *model.ZSPContractFile) error {
|
||||
return nil
|
||||
}
|
||||
func (m *mockZSPContractService) UpdateContractById(contractID string, req *dto.ZSPContractUpdateRequest) error {
|
||||
return m.updateContractByIdFn(contractID, req)
|
||||
}
|
||||
func (m *mockZSPContractService) CreateContractDetail(req *dto.ZSPContractDetailCreateRequest) error {
|
||||
return m.createContractDetailFn(req)
|
||||
}
|
||||
func (m *mockZSPContractService) UpdateContractDetail(req *dto.ZSPContractDetailUpdateRequest) error {
|
||||
return m.updateContractDetailFn(req)
|
||||
}
|
||||
func (m *mockZSPContractService) GetContractDetailById(detailID string) (*model.ZSPContractDetail, error) {
|
||||
return m.getContractDetailByIdFn(detailID)
|
||||
}
|
||||
func (m *mockZSPContractService) ListContractDetails(contractFileID string) ([]model.ZSPContractDetail, error) {
|
||||
return m.listContractDetailsFn(contractFileID)
|
||||
}
|
||||
func (m *mockZSPContractService) DeleteContractDetailById(detailID string) error {
|
||||
return m.deleteContractDetailByIdFn(detailID)
|
||||
}
|
||||
func (m *mockZSPContractService) BatchCreateContractDetails(req *dto.ZSPContractDetailBatchCreateRequest) error {
|
||||
return m.batchCreateContractDetailsFn(req)
|
||||
}
|
||||
|
||||
var _ service.ZSPContractService = (*mockZSPContractService)(nil)
|
||||
|
||||
func setupZSPContractRouter(h *handler.ZSPContractHandler) *gin.Engine {
|
||||
gin.SetMode(gin.TestMode)
|
||||
r := gin.New()
|
||||
api := r.Group("/api")
|
||||
{
|
||||
zsp := api.Group("/zsp-contract")
|
||||
{
|
||||
zsp.POST("/folders", h.CreateFolder)
|
||||
zsp.GET("/folders", h.GetFolders)
|
||||
zsp.GET("/folders/:id", h.GetFolder)
|
||||
zsp.DELETE("/folders/:id", h.DeleteFolder)
|
||||
zsp.PUT("/folders/:id", h.UpdateFolder)
|
||||
|
||||
zsp.POST("/contracts", h.UploadContract)
|
||||
zsp.GET("/contracts/:id", h.GetContractWithDetails)
|
||||
zsp.GET("/contracts", h.ListContractsWithDetails)
|
||||
zsp.PUT("/contracts/:id", h.UpdateContract)
|
||||
zsp.DELETE("/contracts/:id", h.DeleteContract)
|
||||
|
||||
zsp.POST("/contracts/:id/details", h.CreateContractDetail)
|
||||
zsp.GET("/contracts/:id/details", h.ListContractDetails)
|
||||
zsp.POST("/details/batch", h.BatchCreateContractDetails)
|
||||
zsp.GET("/details/:id", h.GetContractDetail)
|
||||
zsp.PUT("/details/:id", h.UpdateContractDetail)
|
||||
zsp.DELETE("/details/:id", h.DeleteContractDetail)
|
||||
}
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
// ========== Folder Tests ==========
|
||||
|
||||
func TestZSPContractCreateFolder_Success(t *testing.T) {
|
||||
mock := &mockZSPContractService{
|
||||
createFolderFn: func(req *dto.ZSPFolderCreateRequest) error {
|
||||
return nil
|
||||
},
|
||||
}
|
||||
h := handler.NewZSPContractHandler(mock)
|
||||
router := setupZSPContractRouter(h)
|
||||
|
||||
body, _ := json.Marshal(dto.ZSPFolderCreateRequest{Name: "test folder", Description: "test desc"})
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("POST", "/api/zsp-contract/folders", bytes.NewReader(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("expected status 200, got %d", w.Code)
|
||||
}
|
||||
|
||||
var resp map[string]interface{}
|
||||
json.Unmarshal(w.Body.Bytes(), &resp)
|
||||
if resp["success"] != true {
|
||||
t.Errorf("expected success true, got %v", resp["success"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestZSPContractCreateFolder_BadRequest(t *testing.T) {
|
||||
mock := &mockZSPContractService{
|
||||
createFolderFn: func(req *dto.ZSPFolderCreateRequest) error {
|
||||
return nil
|
||||
},
|
||||
}
|
||||
h := handler.NewZSPContractHandler(mock)
|
||||
router := setupZSPContractRouter(h)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("POST", "/api/zsp-contract/folders", bytes.NewReader([]byte("{}")))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusBadRequest {
|
||||
t.Errorf("expected status 400 for missing required field, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestZSPContractGetFolders_Success(t *testing.T) {
|
||||
mock := &mockZSPContractService{
|
||||
getFoldersFn: func() ([]model.ZSPContractFolder, error) {
|
||||
return []model.ZSPContractFolder{
|
||||
{ID: 1, Name: "folder1"},
|
||||
{ID: 2, Name: "folder2"},
|
||||
}, nil
|
||||
},
|
||||
}
|
||||
h := handler.NewZSPContractHandler(mock)
|
||||
router := setupZSPContractRouter(h)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("GET", "/api/zsp-contract/folders", nil)
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("expected status 200, got %d", w.Code)
|
||||
}
|
||||
|
||||
var resp map[string]interface{}
|
||||
json.Unmarshal(w.Body.Bytes(), &resp)
|
||||
if resp["success"] != true {
|
||||
t.Errorf("expected success true, got %v", resp["success"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestZSPContractGetFolders_Error(t *testing.T) {
|
||||
mock := &mockZSPContractService{
|
||||
getFoldersFn: func() ([]model.ZSPContractFolder, error) {
|
||||
return nil, errors.New("database error")
|
||||
},
|
||||
}
|
||||
h := handler.NewZSPContractHandler(mock)
|
||||
router := setupZSPContractRouter(h)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("GET", "/api/zsp-contract/folders", nil)
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusInternalServerError {
|
||||
t.Errorf("expected status 500, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestZSPContractGetFolder_Success(t *testing.T) {
|
||||
mock := &mockZSPContractService{
|
||||
getFolderByIdFn: func(folderID string) (*model.ZSPContractFolder, error) {
|
||||
return &model.ZSPContractFolder{ID: 1, Name: "test folder"}, nil
|
||||
},
|
||||
}
|
||||
h := handler.NewZSPContractHandler(mock)
|
||||
router := setupZSPContractRouter(h)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("GET", "/api/zsp-contract/folders/1", nil)
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("expected status 200, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestZSPContractDeleteFolder_Success(t *testing.T) {
|
||||
mock := &mockZSPContractService{
|
||||
deleteFolderByIdFn: func(folderID string) error {
|
||||
return nil
|
||||
},
|
||||
}
|
||||
h := handler.NewZSPContractHandler(mock)
|
||||
router := setupZSPContractRouter(h)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("DELETE", "/api/zsp-contract/folders/1", nil)
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("expected status 200, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestZSPContractDeleteFolder_Error(t *testing.T) {
|
||||
mock := &mockZSPContractService{
|
||||
deleteFolderByIdFn: func(folderID string) error {
|
||||
return errors.New("folder not found")
|
||||
},
|
||||
}
|
||||
h := handler.NewZSPContractHandler(mock)
|
||||
router := setupZSPContractRouter(h)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("DELETE", "/api/zsp-contract/folders/999", nil)
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusInternalServerError {
|
||||
t.Errorf("expected status 500, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestZSPContractUpdateFolder_Success(t *testing.T) {
|
||||
mock := &mockZSPContractService{
|
||||
updateFolderByIdFn: func(folderID string, req *dto.ZSPFolderUpdateRequest) error {
|
||||
return nil
|
||||
},
|
||||
}
|
||||
h := handler.NewZSPContractHandler(mock)
|
||||
router := setupZSPContractRouter(h)
|
||||
|
||||
body, _ := json.Marshal(dto.ZSPFolderUpdateRequest{Name: "updated name", Description: "updated desc"})
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("PUT", "/api/zsp-contract/folders/1", bytes.NewReader(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("expected status 200, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
// ========== Contract Tests ==========
|
||||
|
||||
func TestZSPContractListContracts_Success(t *testing.T) {
|
||||
mock := &mockZSPContractService{
|
||||
listContractsWithDetailsFn: func(folderID string) ([]model.ZSPContractFile, error) {
|
||||
return []model.ZSPContractFile{
|
||||
{ID: 1, ContractNumber: "C001", CompanyName: "公司A"},
|
||||
{ID: 2, ContractNumber: "C002", CompanyName: "公司B"},
|
||||
}, nil
|
||||
},
|
||||
}
|
||||
h := handler.NewZSPContractHandler(mock)
|
||||
router := setupZSPContractRouter(h)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("GET", "/api/zsp-contract/contracts?folderId=1", nil)
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("expected status 200, got %d", w.Code)
|
||||
}
|
||||
|
||||
var resp map[string]interface{}
|
||||
json.Unmarshal(w.Body.Bytes(), &resp)
|
||||
if resp["success"] != true {
|
||||
t.Errorf("expected success true, got %v", resp["success"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestZSPContractGetContract_Success(t *testing.T) {
|
||||
mock := &mockZSPContractService{
|
||||
getContractWithDetailsFn: func(contractID string) (*model.ZSPContractFile, error) {
|
||||
return &model.ZSPContractFile{ID: 1, ContractNumber: "C001", CompanyName: "公司A"}, nil
|
||||
},
|
||||
}
|
||||
h := handler.NewZSPContractHandler(mock)
|
||||
router := setupZSPContractRouter(h)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("GET", "/api/zsp-contract/contracts/1", nil)
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("expected status 200, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestZSPContractGetContract_Error(t *testing.T) {
|
||||
mock := &mockZSPContractService{
|
||||
getContractWithDetailsFn: func(contractID string) (*model.ZSPContractFile, error) {
|
||||
return nil, errors.New("contract not found")
|
||||
},
|
||||
}
|
||||
h := handler.NewZSPContractHandler(mock)
|
||||
router := setupZSPContractRouter(h)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("GET", "/api/zsp-contract/contracts/999", nil)
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusInternalServerError {
|
||||
t.Errorf("expected status 500, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestZSPContractDeleteContract_Success(t *testing.T) {
|
||||
mock := &mockZSPContractService{
|
||||
deleteContractByIdFn: func(contractID string) error {
|
||||
return nil
|
||||
},
|
||||
}
|
||||
h := handler.NewZSPContractHandler(mock)
|
||||
router := setupZSPContractRouter(h)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("DELETE", "/api/zsp-contract/contracts/1", nil)
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("expected status 200, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestZSPContractUpdateContract_Success(t *testing.T) {
|
||||
mock := &mockZSPContractService{
|
||||
updateContractByIdFn: func(contractID string, req *dto.ZSPContractUpdateRequest) error {
|
||||
return nil
|
||||
},
|
||||
}
|
||||
h := handler.NewZSPContractHandler(mock)
|
||||
router := setupZSPContractRouter(h)
|
||||
|
||||
body, _ := json.Marshal(dto.ZSPContractUpdateRequest{ContractNumber: "C001-Updated"})
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("PUT", "/api/zsp-contract/contracts/1", bytes.NewReader(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("expected status 200, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
// ========== Contract Detail Tests ==========
|
||||
|
||||
func TestZSPContractCreateContractDetail_Success(t *testing.T) {
|
||||
mock := &mockZSPContractService{
|
||||
createContractDetailFn: func(req *dto.ZSPContractDetailCreateRequest) error {
|
||||
return nil
|
||||
},
|
||||
}
|
||||
h := handler.NewZSPContractHandler(mock)
|
||||
router := setupZSPContractRouter(h)
|
||||
|
||||
body, _ := json.Marshal(dto.ZSPContractDetailCreateRequest{
|
||||
ContractFileID: 1,
|
||||
CommodityName: "燕麦",
|
||||
TotalQuantity: 100,
|
||||
UnitPrice: 500.0,
|
||||
})
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("POST", "/api/zsp-contract/contracts/1/details", bytes.NewReader(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("expected status 200, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestZSPContractListContractDetails_Success(t *testing.T) {
|
||||
mock := &mockZSPContractService{
|
||||
listContractDetailsFn: func(contractFileID string) ([]model.ZSPContractDetail, error) {
|
||||
return []model.ZSPContractDetail{
|
||||
{ID: 1, CommodityName: "燕麦", TotalQuantity: 100},
|
||||
{ID: 2, CommodityName: "大豆", TotalQuantity: 200},
|
||||
}, nil
|
||||
},
|
||||
}
|
||||
h := handler.NewZSPContractHandler(mock)
|
||||
router := setupZSPContractRouter(h)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("GET", "/api/zsp-contract/contracts/1/details", nil)
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("expected status 200, got %d", w.Code)
|
||||
}
|
||||
|
||||
var resp map[string]interface{}
|
||||
json.Unmarshal(w.Body.Bytes(), &resp)
|
||||
if resp["success"] != true {
|
||||
t.Errorf("expected success true, got %v", resp["success"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestZSPContractGetContractDetail_Success(t *testing.T) {
|
||||
mock := &mockZSPContractService{
|
||||
getContractDetailByIdFn: func(detailID string) (*model.ZSPContractDetail, error) {
|
||||
return &model.ZSPContractDetail{ID: 1, CommodityName: "燕麦", TotalQuantity: 100}, nil
|
||||
},
|
||||
}
|
||||
h := handler.NewZSPContractHandler(mock)
|
||||
router := setupZSPContractRouter(h)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("GET", "/api/zsp-contract/details/1", nil)
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("expected status 200, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestZSPContractDeleteContractDetail_Success(t *testing.T) {
|
||||
mock := &mockZSPContractService{
|
||||
deleteContractDetailByIdFn: func(detailID string) error {
|
||||
return nil
|
||||
},
|
||||
}
|
||||
h := handler.NewZSPContractHandler(mock)
|
||||
router := setupZSPContractRouter(h)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("DELETE", "/api/zsp-contract/details/1", nil)
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("expected status 200, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestZSPContractUpdateContractDetail_Success(t *testing.T) {
|
||||
mock := &mockZSPContractService{
|
||||
updateContractDetailFn: func(req *dto.ZSPContractDetailUpdateRequest) error {
|
||||
return nil
|
||||
},
|
||||
}
|
||||
h := handler.NewZSPContractHandler(mock)
|
||||
router := setupZSPContractRouter(h)
|
||||
|
||||
body, _ := json.Marshal(dto.ZSPContractDetailUpdateRequest{
|
||||
ID: 1,
|
||||
CommodityName: "燕麦-更新",
|
||||
TotalQuantity: 150,
|
||||
})
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("PUT", "/api/zsp-contract/details/1", bytes.NewReader(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("expected status 200, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestZSPContractBatchCreateContractDetails_Success(t *testing.T) {
|
||||
mock := &mockZSPContractService{
|
||||
batchCreateContractDetailsFn: func(req *dto.ZSPContractDetailBatchCreateRequest) error {
|
||||
return nil
|
||||
},
|
||||
}
|
||||
h := handler.NewZSPContractHandler(mock)
|
||||
router := setupZSPContractRouter(h)
|
||||
|
||||
details := []dto.ZSPContractDetailCreateRequest{
|
||||
{ContractFileID: 1, CommodityName: "燕麦", TotalQuantity: 100, UnitPrice: 500.0},
|
||||
{ContractFileID: 1, CommodityName: "大豆", TotalQuantity: 200, UnitPrice: 400.0},
|
||||
}
|
||||
body, _ := json.Marshal(dto.ZSPContractDetailBatchCreateRequest{
|
||||
ContractFileID: 1,
|
||||
Details: details,
|
||||
})
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("POST", "/api/zsp-contract/details/batch", bytes.NewReader(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("expected status 200, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
676
backend/test/zsp_finances_handler_test.go
Normal file
676
backend/test/zsp_finances_handler_test.go
Normal file
@@ -0,0 +1,676 @@
|
||||
package test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/rovina/zsp-backend/internal/dto"
|
||||
"github.com/rovina/zsp-backend/internal/handler"
|
||||
"github.com/rovina/zsp-backend/internal/model"
|
||||
"github.com/rovina/zsp-backend/internal/service"
|
||||
"time"
|
||||
)
|
||||
|
||||
// mockZSPFinancesService implements service.ZSPFinancesService for testing
|
||||
type mockZSPFinancesService struct {
|
||||
createFreightChargeFn func(req *dto.ZSPFreightChargesCreateRequest) error
|
||||
getFreightChargeFn func(id string) (*model.ZSPFreightCharges, error)
|
||||
listFreightChargesFn func() ([]model.ZSPFreightCharges, error)
|
||||
updateFreightChargeFn func(id string, req *dto.ZSPFreightChargesUpdateRequest) error
|
||||
deleteFreightChargeFn func(id string) error
|
||||
|
||||
createMiscChargeFn func(req *dto.ZSPMiscChargesCreateRequest) error
|
||||
getMiscChargeFn func(id string) (*model.ZSPMiscCharges, error)
|
||||
listMiscChargesFn func() ([]model.ZSPMiscCharges, error)
|
||||
updateMiscChargeFn func(id string, req *dto.ZSPMiscChargesUpdateRequest) error
|
||||
deleteMiscChargeFn func(id string) error
|
||||
|
||||
createServiceFeeFn func(req *dto.ZSPServiceFeesCreateRequest) error
|
||||
getServiceFeeFn func(id string) (*model.ZSPServiceFees, error)
|
||||
listServiceFeesFn func() ([]model.ZSPServiceFees, error)
|
||||
updateServiceFeeFn func(id string, req *dto.ZSPServiceFeesUpdateRequest) error
|
||||
deleteServiceFeeFn func(id string) error
|
||||
|
||||
createCostBreakdownFn func(req *dto.ZSPCostBreakdownCreateRequest) error
|
||||
getCostBreakdownFn func(id string) (*model.ZSPCostBreakdown, error)
|
||||
listCostBreakdownsFn func(businessID string) ([]model.ZSPCostBreakdown, error)
|
||||
updateCostBreakdownFn func(id string, req *dto.ZSPCostBreakdownUpdateRequest) error
|
||||
deleteCostBreakdownFn func(id string) error
|
||||
|
||||
createProfitRecordFn func(req *dto.ZSPProfitRecordCreateRequest) error
|
||||
getProfitRecordFn func(id string) (*model.ZSPProfitRecord, error)
|
||||
listProfitRecordsFn func(filters map[string]string) ([]model.ZSPProfitRecord, error)
|
||||
updateProfitRecordFn func(id string, req *dto.ZSPProfitRecordUpdateRequest) error
|
||||
deleteProfitRecordFn func(id string) error
|
||||
}
|
||||
|
||||
func (m *mockZSPFinancesService) CreateFreightCharge(req *dto.ZSPFreightChargesCreateRequest) error {
|
||||
return m.createFreightChargeFn(req)
|
||||
}
|
||||
func (m *mockZSPFinancesService) GetFreightCharge(id string) (*model.ZSPFreightCharges, error) {
|
||||
return m.getFreightChargeFn(id)
|
||||
}
|
||||
func (m *mockZSPFinancesService) ListFreightCharges() ([]model.ZSPFreightCharges, error) {
|
||||
return m.listFreightChargesFn()
|
||||
}
|
||||
func (m *mockZSPFinancesService) UpdateFreightCharge(id string, req *dto.ZSPFreightChargesUpdateRequest) error {
|
||||
return m.updateFreightChargeFn(id, req)
|
||||
}
|
||||
func (m *mockZSPFinancesService) DeleteFreightCharge(id string) error {
|
||||
return m.deleteFreightChargeFn(id)
|
||||
}
|
||||
|
||||
func (m *mockZSPFinancesService) CreateMiscCharge(req *dto.ZSPMiscChargesCreateRequest) error {
|
||||
return m.createMiscChargeFn(req)
|
||||
}
|
||||
func (m *mockZSPFinancesService) GetMiscCharge(id string) (*model.ZSPMiscCharges, error) {
|
||||
return m.getMiscChargeFn(id)
|
||||
}
|
||||
func (m *mockZSPFinancesService) ListMiscCharges() ([]model.ZSPMiscCharges, error) {
|
||||
return m.listMiscChargesFn()
|
||||
}
|
||||
func (m *mockZSPFinancesService) UpdateMiscCharge(id string, req *dto.ZSPMiscChargesUpdateRequest) error {
|
||||
return m.updateMiscChargeFn(id, req)
|
||||
}
|
||||
func (m *mockZSPFinancesService) DeleteMiscCharge(id string) error {
|
||||
return m.deleteMiscChargeFn(id)
|
||||
}
|
||||
|
||||
func (m *mockZSPFinancesService) CreateServiceFee(req *dto.ZSPServiceFeesCreateRequest) error {
|
||||
return m.createServiceFeeFn(req)
|
||||
}
|
||||
func (m *mockZSPFinancesService) GetServiceFee(id string) (*model.ZSPServiceFees, error) {
|
||||
return m.getServiceFeeFn(id)
|
||||
}
|
||||
func (m *mockZSPFinancesService) ListServiceFees() ([]model.ZSPServiceFees, error) {
|
||||
return m.listServiceFeesFn()
|
||||
}
|
||||
func (m *mockZSPFinancesService) UpdateServiceFee(id string, req *dto.ZSPServiceFeesUpdateRequest) error {
|
||||
return m.updateServiceFeeFn(id, req)
|
||||
}
|
||||
func (m *mockZSPFinancesService) DeleteServiceFee(id string) error {
|
||||
return m.deleteServiceFeeFn(id)
|
||||
}
|
||||
|
||||
func (m *mockZSPFinancesService) CreateCostBreakdown(req *dto.ZSPCostBreakdownCreateRequest) error {
|
||||
return m.createCostBreakdownFn(req)
|
||||
}
|
||||
func (m *mockZSPFinancesService) GetCostBreakdown(id string) (*model.ZSPCostBreakdown, error) {
|
||||
return m.getCostBreakdownFn(id)
|
||||
}
|
||||
func (m *mockZSPFinancesService) ListCostBreakdowns(businessID string) ([]model.ZSPCostBreakdown, error) {
|
||||
return m.listCostBreakdownsFn(businessID)
|
||||
}
|
||||
func (m *mockZSPFinancesService) UpdateCostBreakdown(id string, req *dto.ZSPCostBreakdownUpdateRequest) error {
|
||||
return m.updateCostBreakdownFn(id, req)
|
||||
}
|
||||
func (m *mockZSPFinancesService) DeleteCostBreakdown(id string) error {
|
||||
return m.deleteCostBreakdownFn(id)
|
||||
}
|
||||
|
||||
func (m *mockZSPFinancesService) CreateProfitRecord(req *dto.ZSPProfitRecordCreateRequest) error {
|
||||
return m.createProfitRecordFn(req)
|
||||
}
|
||||
func (m *mockZSPFinancesService) GetProfitRecord(id string) (*model.ZSPProfitRecord, error) {
|
||||
return m.getProfitRecordFn(id)
|
||||
}
|
||||
func (m *mockZSPFinancesService) ListProfitRecords(filters map[string]string) ([]model.ZSPProfitRecord, error) {
|
||||
return m.listProfitRecordsFn(filters)
|
||||
}
|
||||
func (m *mockZSPFinancesService) UpdateProfitRecord(id string, req *dto.ZSPProfitRecordUpdateRequest) error {
|
||||
return m.updateProfitRecordFn(id, req)
|
||||
}
|
||||
func (m *mockZSPFinancesService) DeleteProfitRecord(id string) error {
|
||||
return m.deleteProfitRecordFn(id)
|
||||
}
|
||||
|
||||
var _ service.ZSPFinancesService = (*mockZSPFinancesService)(nil)
|
||||
|
||||
func setupZSPFinancesRouter(h *handler.ZSPFinancesHandler) *gin.Engine {
|
||||
gin.SetMode(gin.TestMode)
|
||||
r := gin.New()
|
||||
api := r.Group("/api")
|
||||
{
|
||||
finances := api.Group("/zsp-finances")
|
||||
{
|
||||
// Freight charges
|
||||
finances.POST("/freight-charges", h.CreateFreightCharge)
|
||||
finances.GET("/freight-charges/:id", h.GetFreightCharge)
|
||||
finances.GET("/freight-charges", h.ListFreightCharges)
|
||||
finances.PUT("/freight-charges/:id", h.UpdateFreightCharge)
|
||||
finances.DELETE("/freight-charges/:id", h.DeleteFreightCharge)
|
||||
|
||||
// Misc charges
|
||||
finances.POST("/misc-charges", h.CreateMiscCharge)
|
||||
finances.GET("/misc-charges/:id", h.GetMiscCharge)
|
||||
finances.GET("/misc-charges", h.ListMiscCharges)
|
||||
finances.PUT("/misc-charges/:id", h.UpdateMiscCharge)
|
||||
finances.DELETE("/misc-charges/:id", h.DeleteMiscCharge)
|
||||
|
||||
// Service fees
|
||||
finances.POST("/service-fees", h.CreateServiceFee)
|
||||
finances.GET("/service-fees/:id", h.GetServiceFee)
|
||||
finances.GET("/service-fees", h.ListServiceFees)
|
||||
finances.PUT("/service-fees/:id", h.UpdateServiceFee)
|
||||
finances.DELETE("/service-fees/:id", h.DeleteServiceFee)
|
||||
|
||||
// Cost breakdowns
|
||||
finances.POST("/cost-breakdowns", h.CreateCostBreakdown)
|
||||
finances.GET("/cost-breakdowns/:id", h.GetCostBreakdown)
|
||||
finances.GET("/cost-breakdowns", h.ListCostBreakdowns)
|
||||
finances.PUT("/cost-breakdowns/:id", h.UpdateCostBreakdown)
|
||||
finances.DELETE("/cost-breakdowns/:id", h.DeleteCostBreakdown)
|
||||
|
||||
// Profit records
|
||||
finances.POST("/profit-records", h.CreateProfitRecord)
|
||||
finances.GET("/profit-records/:id", h.GetProfitRecord)
|
||||
finances.GET("/profit-records", h.ListProfitRecords)
|
||||
finances.PUT("/profit-records/:id", h.UpdateProfitRecord)
|
||||
finances.DELETE("/profit-records/:id", h.DeleteProfitRecord)
|
||||
}
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
// ========== Freight Charges Tests ==========
|
||||
|
||||
func TestZSPFinancesCreateFreightCharge_Success(t *testing.T) {
|
||||
mock := &mockZSPFinancesService{
|
||||
createFreightChargeFn: func(req *dto.ZSPFreightChargesCreateRequest) error {
|
||||
return nil
|
||||
},
|
||||
}
|
||||
h := handler.NewZSPFinancesHandler(mock)
|
||||
router := setupZSPFinancesRouter(h)
|
||||
|
||||
body, _ := json.Marshal(dto.ZSPFreightChargesCreateRequest{
|
||||
ExpenseItem: "运费",
|
||||
Amount: 1000.0,
|
||||
PaymentDate: "2024-01-15",
|
||||
FreightCompanyName: "货代公司A",
|
||||
})
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("POST", "/api/zsp-finances/freight-charges", bytes.NewReader(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("expected status 200, got %d", w.Code)
|
||||
}
|
||||
|
||||
var resp map[string]interface{}
|
||||
json.Unmarshal(w.Body.Bytes(), &resp)
|
||||
if resp["success"] != true {
|
||||
t.Errorf("expected success true, got %v", resp["success"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestZSPFinancesCreateFreightCharge_BadRequest(t *testing.T) {
|
||||
mock := &mockZSPFinancesService{
|
||||
createFreightChargeFn: func(req *dto.ZSPFreightChargesCreateRequest) error {
|
||||
return nil
|
||||
},
|
||||
}
|
||||
h := handler.NewZSPFinancesHandler(mock)
|
||||
router := setupZSPFinancesRouter(h)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("POST", "/api/zsp-finances/freight-charges", bytes.NewReader([]byte("{}")))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusBadRequest {
|
||||
t.Errorf("expected status 400 for missing required fields, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestZSPFinancesGetFreightCharge_Success(t *testing.T) {
|
||||
mock := &mockZSPFinancesService{
|
||||
getFreightChargeFn: func(id string) (*model.ZSPFreightCharges, error) {
|
||||
return &model.ZSPFreightCharges{
|
||||
ID: 1,
|
||||
ExpenseItem: "运费",
|
||||
Amount: 1000.0,
|
||||
FreightCompanyName: "货代公司A",
|
||||
}, nil
|
||||
},
|
||||
}
|
||||
h := handler.NewZSPFinancesHandler(mock)
|
||||
router := setupZSPFinancesRouter(h)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("GET", "/api/zsp-finances/freight-charges/1", nil)
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("expected status 200, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestZSPFinancesListFreightCharges_Success(t *testing.T) {
|
||||
mock := &mockZSPFinancesService{
|
||||
listFreightChargesFn: func() ([]model.ZSPFreightCharges, error) {
|
||||
return []model.ZSPFreightCharges{
|
||||
{ID: 1, ExpenseItem: "运费", Amount: 1000.0},
|
||||
{ID: 2, ExpenseItem: "港杂费", Amount: 500.0},
|
||||
}, nil
|
||||
},
|
||||
}
|
||||
h := handler.NewZSPFinancesHandler(mock)
|
||||
router := setupZSPFinancesRouter(h)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("GET", "/api/zsp-finances/freight-charges", nil)
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("expected status 200, got %d", w.Code)
|
||||
}
|
||||
|
||||
var resp map[string]interface{}
|
||||
json.Unmarshal(w.Body.Bytes(), &resp)
|
||||
if resp["success"] != true {
|
||||
t.Errorf("expected success true, got %v", resp["success"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestZSPFinancesUpdateFreightCharge_Success(t *testing.T) {
|
||||
mock := &mockZSPFinancesService{
|
||||
updateFreightChargeFn: func(id string, req *dto.ZSPFreightChargesUpdateRequest) error {
|
||||
return nil
|
||||
},
|
||||
}
|
||||
h := handler.NewZSPFinancesHandler(mock)
|
||||
router := setupZSPFinancesRouter(h)
|
||||
|
||||
body, _ := json.Marshal(dto.ZSPFreightChargesUpdateRequest{
|
||||
Amount: 1500.0,
|
||||
})
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("PUT", "/api/zsp-finances/freight-charges/1", bytes.NewReader(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("expected status 200, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestZSPFinancesDeleteFreightCharge_Success(t *testing.T) {
|
||||
mock := &mockZSPFinancesService{
|
||||
deleteFreightChargeFn: func(id string) error {
|
||||
return nil
|
||||
},
|
||||
}
|
||||
h := handler.NewZSPFinancesHandler(mock)
|
||||
router := setupZSPFinancesRouter(h)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("DELETE", "/api/zsp-finances/freight-charges/1", nil)
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("expected status 200, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestZSPFinancesDeleteFreightCharge_Error(t *testing.T) {
|
||||
mock := &mockZSPFinancesService{
|
||||
deleteFreightChargeFn: func(id string) error {
|
||||
return errors.New("record not found")
|
||||
},
|
||||
}
|
||||
h := handler.NewZSPFinancesHandler(mock)
|
||||
router := setupZSPFinancesRouter(h)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("DELETE", "/api/zsp-finances/freight-charges/999", nil)
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusInternalServerError {
|
||||
t.Errorf("expected status 500, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
// ========== Misc Charges Tests ==========
|
||||
|
||||
func TestZSPFinancesCreateMiscCharge_Success(t *testing.T) {
|
||||
mock := &mockZSPFinancesService{
|
||||
createMiscChargeFn: func(req *dto.ZSPMiscChargesCreateRequest) error {
|
||||
return nil
|
||||
},
|
||||
}
|
||||
h := handler.NewZSPFinancesHandler(mock)
|
||||
router := setupZSPFinancesRouter(h)
|
||||
|
||||
body, _ := json.Marshal(dto.ZSPMiscChargesCreateRequest{
|
||||
ExpenseItem: "报关费",
|
||||
Amount: 200.0,
|
||||
PaymentDate: "2024-01-15",
|
||||
})
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("POST", "/api/zsp-finances/misc-charges", bytes.NewReader(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("expected status 200, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestZSPFinancesListMiscCharges_Success(t *testing.T) {
|
||||
mock := &mockZSPFinancesService{
|
||||
listMiscChargesFn: func() ([]model.ZSPMiscCharges, error) {
|
||||
return []model.ZSPMiscCharges{
|
||||
{ID: 1, ExpenseItem: "报关费", Amount: 200.0},
|
||||
{ID: 2, ExpenseItem: "仓储费", Amount: 300.0},
|
||||
}, nil
|
||||
},
|
||||
}
|
||||
h := handler.NewZSPFinancesHandler(mock)
|
||||
router := setupZSPFinancesRouter(h)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("GET", "/api/zsp-finances/misc-charges", nil)
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("expected status 200, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestZSPFinancesDeleteMiscCharge_Success(t *testing.T) {
|
||||
mock := &mockZSPFinancesService{
|
||||
deleteMiscChargeFn: func(id string) error {
|
||||
return nil
|
||||
},
|
||||
}
|
||||
h := handler.NewZSPFinancesHandler(mock)
|
||||
router := setupZSPFinancesRouter(h)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("DELETE", "/api/zsp-finances/misc-charges/1", nil)
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("expected status 200, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
// ========== Service Fees Tests ==========
|
||||
|
||||
func TestZSPFinancesCreateServiceFee_Success(t *testing.T) {
|
||||
mock := &mockZSPFinancesService{
|
||||
createServiceFeeFn: func(req *dto.ZSPServiceFeesCreateRequest) error {
|
||||
return nil
|
||||
},
|
||||
}
|
||||
h := handler.NewZSPFinancesHandler(mock)
|
||||
router := setupZSPFinancesRouter(h)
|
||||
|
||||
body, _ := json.Marshal(dto.ZSPServiceFeesCreateRequest{
|
||||
Name: "代理服务费",
|
||||
Amount: 500.0,
|
||||
Date: "2024-01-15",
|
||||
})
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("POST", "/api/zsp-finances/service-fees", bytes.NewReader(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("expected status 200, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestZSPFinancesListServiceFees_Success(t *testing.T) {
|
||||
mock := &mockZSPFinancesService{
|
||||
listServiceFeesFn: func() ([]model.ZSPServiceFees, error) {
|
||||
return []model.ZSPServiceFees{
|
||||
{ID: 1, Name: "代理服务费", Amount: 500.0},
|
||||
{ID: 2, Name: "咨询费", Amount: 300.0},
|
||||
}, nil
|
||||
},
|
||||
}
|
||||
h := handler.NewZSPFinancesHandler(mock)
|
||||
router := setupZSPFinancesRouter(h)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("GET", "/api/zsp-finances/service-fees", nil)
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("expected status 200, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestZSPFinancesDeleteServiceFee_Success(t *testing.T) {
|
||||
mock := &mockZSPFinancesService{
|
||||
deleteServiceFeeFn: func(id string) error {
|
||||
return nil
|
||||
},
|
||||
}
|
||||
h := handler.NewZSPFinancesHandler(mock)
|
||||
router := setupZSPFinancesRouter(h)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("DELETE", "/api/zsp-finances/service-fees/1", nil)
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("expected status 200, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
// ========== Cost Breakdown Tests ==========
|
||||
|
||||
func TestZSPFinancesCreateCostBreakdown_Success(t *testing.T) {
|
||||
mock := &mockZSPFinancesService{
|
||||
createCostBreakdownFn: func(req *dto.ZSPCostBreakdownCreateRequest) error {
|
||||
return nil
|
||||
},
|
||||
}
|
||||
h := handler.NewZSPFinancesHandler(mock)
|
||||
router := setupZSPFinancesRouter(h)
|
||||
|
||||
body, _ := json.Marshal(dto.ZSPCostBreakdownCreateRequest{
|
||||
BusinessID: "B001",
|
||||
BusinessType: "contract",
|
||||
})
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("POST", "/api/zsp-finances/cost-breakdowns", bytes.NewReader(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("expected status 200, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestZSPFinancesListCostBreakdowns_Success(t *testing.T) {
|
||||
mock := &mockZSPFinancesService{
|
||||
listCostBreakdownsFn: func(businessID string) ([]model.ZSPCostBreakdown, error) {
|
||||
return []model.ZSPCostBreakdown{
|
||||
{ID: 1, BusinessID: "B001", BusinessType: "contract", TotalCost: 1000.0},
|
||||
}, nil
|
||||
},
|
||||
}
|
||||
h := handler.NewZSPFinancesHandler(mock)
|
||||
router := setupZSPFinancesRouter(h)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("GET", "/api/zsp-finances/cost-breakdowns?businessId=B001", nil)
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("expected status 200, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestZSPFinancesDeleteCostBreakdown_Success(t *testing.T) {
|
||||
mock := &mockZSPFinancesService{
|
||||
deleteCostBreakdownFn: func(id string) error {
|
||||
return nil
|
||||
},
|
||||
}
|
||||
h := handler.NewZSPFinancesHandler(mock)
|
||||
router := setupZSPFinancesRouter(h)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("DELETE", "/api/zsp-finances/cost-breakdowns/1", nil)
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("expected status 200, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
// ========== Profit Record Tests ==========
|
||||
|
||||
func TestZSPFinancesCreateProfitRecord_Success(t *testing.T) {
|
||||
mock := &mockZSPFinancesService{
|
||||
createProfitRecordFn: func(req *dto.ZSPProfitRecordCreateRequest) error {
|
||||
return nil
|
||||
},
|
||||
}
|
||||
h := handler.NewZSPFinancesHandler(mock)
|
||||
router := setupZSPFinancesRouter(h)
|
||||
|
||||
body, _ := json.Marshal(dto.ZSPProfitRecordCreateRequest{
|
||||
BusinessID: "B001",
|
||||
BusinessType: "contract",
|
||||
Revenue: 2000.0,
|
||||
TotalCost: 1500.0,
|
||||
RecordDate: "2024-01-15",
|
||||
})
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("POST", "/api/zsp-finances/profit-records", bytes.NewReader(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("expected status 200, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestZSPFinancesGetProfitRecord_Success(t *testing.T) {
|
||||
mock := &mockZSPFinancesService{
|
||||
getProfitRecordFn: func(id string) (*model.ZSPProfitRecord, error) {
|
||||
return &model.ZSPProfitRecord{
|
||||
ID: 1,
|
||||
BusinessID: "B001",
|
||||
BusinessType: "contract",
|
||||
Revenue: 2000.0,
|
||||
TotalCost: 1500.0,
|
||||
Profit: 500.0,
|
||||
RecordDate: time.Now(),
|
||||
}, nil
|
||||
},
|
||||
}
|
||||
h := handler.NewZSPFinancesHandler(mock)
|
||||
router := setupZSPFinancesRouter(h)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("GET", "/api/zsp-finances/profit-records/1", nil)
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("expected status 200, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestZSPFinancesListProfitRecords_Success(t *testing.T) {
|
||||
mock := &mockZSPFinancesService{
|
||||
listProfitRecordsFn: func(filters map[string]string) ([]model.ZSPProfitRecord, error) {
|
||||
return []model.ZSPProfitRecord{
|
||||
{ID: 1, BusinessID: "B001", Revenue: 2000.0, Profit: 500.0},
|
||||
{ID: 2, BusinessID: "B002", Revenue: 3000.0, Profit: 800.0},
|
||||
}, nil
|
||||
},
|
||||
}
|
||||
h := handler.NewZSPFinancesHandler(mock)
|
||||
router := setupZSPFinancesRouter(h)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("GET", "/api/zsp-finances/profit-records", nil)
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("expected status 200, got %d", w.Code)
|
||||
}
|
||||
|
||||
var resp map[string]interface{}
|
||||
json.Unmarshal(w.Body.Bytes(), &resp)
|
||||
if resp["success"] != true {
|
||||
t.Errorf("expected success true, got %v", resp["success"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestZSPFinancesListProfitRecords_WithFilters(t *testing.T) {
|
||||
mock := &mockZSPFinancesService{
|
||||
listProfitRecordsFn: func(filters map[string]string) ([]model.ZSPProfitRecord, error) {
|
||||
if filters["businessId"] != "B001" {
|
||||
t.Errorf("expected businessId filter B001, got %v", filters["businessId"])
|
||||
}
|
||||
return []model.ZSPProfitRecord{
|
||||
{ID: 1, BusinessID: "B001", Revenue: 2000.0, Profit: 500.0},
|
||||
}, nil
|
||||
},
|
||||
}
|
||||
h := handler.NewZSPFinancesHandler(mock)
|
||||
router := setupZSPFinancesRouter(h)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("GET", "/api/zsp-finances/profit-records?businessId=B001&businessType=contract", nil)
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("expected status 200, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestZSPFinancesUpdateProfitRecord_Success(t *testing.T) {
|
||||
mock := &mockZSPFinancesService{
|
||||
updateProfitRecordFn: func(id string, req *dto.ZSPProfitRecordUpdateRequest) error {
|
||||
return nil
|
||||
},
|
||||
}
|
||||
h := handler.NewZSPFinancesHandler(mock)
|
||||
router := setupZSPFinancesRouter(h)
|
||||
|
||||
body, _ := json.Marshal(dto.ZSPProfitRecordUpdateRequest{
|
||||
Revenue: 2500.0,
|
||||
TotalCost: 1600.0,
|
||||
})
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("PUT", "/api/zsp-finances/profit-records/1", bytes.NewReader(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("expected status 200, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestZSPFinancesDeleteProfitRecord_Success(t *testing.T) {
|
||||
mock := &mockZSPFinancesService{
|
||||
deleteProfitRecordFn: func(id string) error {
|
||||
return nil
|
||||
},
|
||||
}
|
||||
h := handler.NewZSPFinancesHandler(mock)
|
||||
router := setupZSPFinancesRouter(h)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("DELETE", "/api/zsp-finances/profit-records/1", nil)
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("expected status 200, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
114
docs/Git提交规范.md
Executable file
114
docs/Git提交规范.md
Executable file
@@ -0,0 +1,114 @@
|
||||
# Git 提交规范文档
|
||||
|
||||
> 本文档规范项目中的 Git 提交信息格式,便于团队协作与版本追溯。
|
||||
|
||||
---
|
||||
|
||||
## 一、提交信息格式
|
||||
|
||||
采用 **Conventional Commits** 规范,格式如下:
|
||||
|
||||
```
|
||||
<类型>[可选范围]: <简短描述>
|
||||
|
||||
[可选的详细说明]
|
||||
|
||||
[可选的脚注]
|
||||
```
|
||||
|
||||
### 1.1 类型(type)
|
||||
|
||||
| 类型 | 说明 |
|
||||
| ---------- | ------------------------ |
|
||||
| `feat` | 新功能 |
|
||||
| `fix` | 缺陷修复 |
|
||||
| `docs` | 文档变更(README、说明等) |
|
||||
| `style` | 代码格式(空格、缩进等,不影响逻辑) |
|
||||
| `refactor` | 重构( neither feat nor fix) |
|
||||
| `perf` | 性能优化 |
|
||||
| `test` | 测试相关 |
|
||||
| `chore` | 构建、工具、依赖等 |
|
||||
|
||||
### 1.2 可选范围(scope)
|
||||
|
||||
标明影响模块,便于检索,例如:
|
||||
|
||||
- `contract` - 合同管理
|
||||
- `finance` - 财务管理
|
||||
- `delivery` - 提货管理
|
||||
- `inventory` - 库存
|
||||
- `invoice` - 发票
|
||||
- `api` - 后端 API
|
||||
- `frontend` - 前端
|
||||
- `backend` - 后端
|
||||
|
||||
---
|
||||
|
||||
## 二、提交示例
|
||||
|
||||
### 2.1 新功能
|
||||
|
||||
```
|
||||
feat(contract): 新增合同批量上传功能
|
||||
feat(finance): 结算管理支持导出 Excel
|
||||
```
|
||||
|
||||
### 2.2 修复
|
||||
|
||||
```
|
||||
fix(api): 修复合同查询接口分页参数错误
|
||||
fix(frontend): 修复发票列表筛选不生效
|
||||
```
|
||||
|
||||
### 2.3 文档
|
||||
|
||||
```
|
||||
docs: 补充财务管理模块待确认问题
|
||||
docs(api): 更新合同管理 API 文档
|
||||
```
|
||||
|
||||
### 2.4 重构与优化
|
||||
|
||||
```
|
||||
refactor(invoice): 抽取发票上传公共逻辑
|
||||
perf(contract): 优化合同列表查询性能
|
||||
```
|
||||
|
||||
### 2.5 其他
|
||||
|
||||
```
|
||||
style: 统一前端代码缩进
|
||||
chore: 升级 Go 依赖版本
|
||||
test(finance): 补充结算单单元测试
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 三、书写建议
|
||||
|
||||
1. **标题**:简洁明了,控制在 50 字符内;使用中文,动词开头。
|
||||
2. **一条提交做一件事**:功能、修复、格式尽量分开提交。
|
||||
3. **说明可省略**:简单变更可不写正文,复杂逻辑建议补充说明。
|
||||
4. **关联需求/问题**:如有 Jira/飞书等,可在脚注中写 `Refs: #123`。
|
||||
|
||||
---
|
||||
|
||||
## 四、反例(应避免)
|
||||
|
||||
| 不良示例 | 问题说明 |
|
||||
| ---------------- | ---------------------- |
|
||||
| `update` | 类型不明确,描述含糊 |
|
||||
| `修改了一些东西` | 没有说明修改了什么 |
|
||||
| `fix bug` | 未说明修复的是哪个 bug |
|
||||
| `WIP` | 未完成工作不宜直接合并 |
|
||||
|
||||
---
|
||||
|
||||
## 五、工作流简要
|
||||
|
||||
- **main**:生产分支,稳定版本
|
||||
- **dev**:开发主分支
|
||||
- **feature/xxx**:功能开发分支
|
||||
- **fix/xxx**:缺陷修复分支
|
||||
|
||||
提交前请先在 `dev` 或对应 feature 分支上开发,完成后再合并到目标分支。
|
||||
70
docs/api/前端路由文档.md
Executable file
70
docs/api/前端路由文档.md
Executable file
@@ -0,0 +1,70 @@
|
||||
# 中尚鹏管理系统 - 前端路由文档
|
||||
|
||||
```
|
||||
作者: rovina
|
||||
最近修订: 2026/3/2
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 一、路由结构概览
|
||||
|
||||
前端基于 Vue 3 + Vue Router,采用扁平化二级路由结构。路由模块位于 `frontend/pure-admin-thin/src/router/modules/`。
|
||||
|
||||
---
|
||||
|
||||
## 二、合同管理模块路由
|
||||
|
||||
| 路径 | 名称 | 组件 | 说明 |
|
||||
|------|------|------|------|
|
||||
| `/system/contract` | 合同管理 | - | 父级重定向到业务合同管理 |
|
||||
| `/system/contract/add-contract/index` | AddContract | AddContract.vue | **业务合同管理**:单合同录入、批量上传归档 |
|
||||
| `/system/contract/zhongshangpeng/index` | ZhongshangpengContract | ZhongshangpengContract.vue | **中尚鹏合同管理**:公司合同文件夹、合同文件、详情表格 |
|
||||
| `/system/contract/company-info/index` | CompanyInfo | CompanyInfo.vue | **公司信息管理** |
|
||||
|
||||
### 2.1 业务合同管理(AddContract)
|
||||
|
||||
- **单合同管理**:左侧按合同类型筛选(全部/采购/上游/下游),右侧合同列表,支持新增、编辑、删除
|
||||
- **批量上传**:左侧批量归档文件夹,右侧批量记录列表,支持新建文件夹、批量上传文件
|
||||
|
||||
### 2.2 中尚鹏合同管理(ZhongshangpengContract)
|
||||
|
||||
- 文件夹归类、合同上传、合同详情(商品/数量/单价/已提货/未提货等)
|
||||
- Excel 导入导出合同详情
|
||||
- **前后端已对接**:数据从 `/api/zsp-contract` 接口加载
|
||||
|
||||
### 2.3 公司信息管理(CompanyInfo)
|
||||
|
||||
- 管理公司相关文件(营业执照、税务登记证、开票信息等)
|
||||
- 文件夹分类、文件上传、查看、下载、删除
|
||||
- **前后端已对接**:数据从 `/api/company` 接口加载
|
||||
|
||||
---
|
||||
|
||||
## 三、其他模块路由(简要)
|
||||
|
||||
| 模块 | 路径前缀 | 说明 |
|
||||
|------|----------|------|
|
||||
| 首页 | `/`, `/welcome` | 首页 |
|
||||
| 提货管理 | `/system/delivery/*` | 我要提货、提货明细、库存、货权转移 |
|
||||
| 财务管理 | `/system/finance/*` | 货代费用、杂费、服务费、成本构成、利润、结算、发票、对账单 |
|
||||
| 查询报表 | `/system/query-report/*` | 合同执行、营业明细、其它明细、商品基础资料 |
|
||||
| 采购管理 | `/system/purchase/*` | 采购报单、采购运踪、国外供应商、采购预算 |
|
||||
| 更新看板 | `/system/changelog/index` | **更新内容看板**:展示系统更新记录 |
|
||||
|
||||
---
|
||||
|
||||
## 四、路由配置说明
|
||||
|
||||
- 路由定义:`src/router/modules/contract.ts` 等
|
||||
- 自动导入:`src/router/index.ts` 通过 `import.meta.glob` 自动加载 `modules/**/*.ts`(排除 remaining.ts)
|
||||
- 菜单渲染:`constantMenus` 用于侧边栏菜单展示
|
||||
|
||||
---
|
||||
|
||||
## 五、API 请求前缀
|
||||
|
||||
合同相关接口统一使用 `/api` 前缀,经 Vite 代理转发至后端:
|
||||
|
||||
- 开发环境:`/api/*` → `http://127.0.0.1:8080/api/*`
|
||||
- 生产环境:需配置 Nginx 或同类反向代理将 `/api` 转发至后端服务
|
||||
306
docs/api/后端API文档-合同管理.md
Executable file
306
docs/api/后端API文档-合同管理.md
Executable file
@@ -0,0 +1,306 @@
|
||||
# 中尚鹏管理系统 - 后端 API 文档(合同管理 + 提货管理)
|
||||
|
||||
```
|
||||
作者: rovina
|
||||
最近修订: 2026/3/2
|
||||
说明: 含合同管理、中尚鹏合同、公司信息、提货管理(我要提货/提货明细/库存/货权转移)。
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 一、基础说明
|
||||
|
||||
- **Base URL**:`http://{host}:8080/api`
|
||||
- **数据格式**:JSON(除文件上传为 multipart/form-data)
|
||||
- **通用响应**:`{ "success": boolean, "message": string, "data"?: T }`
|
||||
- **认证方式**:除 `/auth/login`、`/auth/register` 及静态文件路由外,所有 API 需在请求头中携带 `Authorization: Bearer <token>`(JWT token 从登录接口获取)
|
||||
|
||||
---
|
||||
|
||||
## 二、业务合同管理 API(/api/contract)
|
||||
|
||||
### 2.1 单合同 CRUD
|
||||
|
||||
| 方法 | 路径 | 说明 |
|
||||
|------|------|------|
|
||||
| GET | `/contract` | 获取合同列表 |
|
||||
| GET | `/contract/:id` | 获取单个合同详情 |
|
||||
| POST | `/contract/upload` | 上传合同(表单 + 文件) |
|
||||
| PUT | `/contract/:id` | 更新合同(仅表单字段) |
|
||||
| DELETE | `/contract/:id` | 删除合同 |
|
||||
|
||||
#### GET /contract
|
||||
|
||||
**Query 参数**:`contractType`(可选):1=采购 2=上游 3=下游
|
||||
|
||||
**响应示例**:
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": [
|
||||
{
|
||||
"id": 1,
|
||||
"name": "合同名称",
|
||||
"contractType": "1",
|
||||
"contractCode": "HT001",
|
||||
"buyParty": "甲方",
|
||||
"sellParty": "乙方",
|
||||
"commodity": "商品",
|
||||
"count": 100,
|
||||
"unitPrice": 10.5,
|
||||
"blNumber": "BL001;BL002",
|
||||
"deliveryTime": "",
|
||||
"pickUpTime": "",
|
||||
"packaging": "",
|
||||
"remarks": "",
|
||||
"createTime": "2026-03-02T10:00:00Z",
|
||||
"updateTime": "2026-03-02T10:00:00Z",
|
||||
"files": [
|
||||
{ "id": 1, "name": "合同.pdf", "url": "uploads/123_合同.pdf" }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**文件 URL 说明**:`url` 为相对路径,完整访问地址为 `GET /api/contract-files/{url}`,例如 `/api/contract-files/uploads/123_合同.pdf`。
|
||||
|
||||
#### POST /contract/upload
|
||||
|
||||
**Content-Type**:`multipart/form-data`
|
||||
|
||||
**表单字段**:
|
||||
| 字段 | 类型 | 必填 | 说明 |
|
||||
|------|------|------|------|
|
||||
| name | string | 是 | 合同名称 |
|
||||
| contractType | string | 是 | 1/2/3 |
|
||||
| contractCode | string | 是 | 合同编号 |
|
||||
| buyParty | string | 是 | 甲方 |
|
||||
| sellParty | string | 是 | 乙方 |
|
||||
| commodity | string | 是 | 商品 |
|
||||
| count | string | 是 | 数量 |
|
||||
| unitPrice | string | 是 | 单价 |
|
||||
| BLNumber | string | 是 | 提单号,最多 15 个,分号分隔 |
|
||||
| deliveryTime | string | 否 | 交货时间 |
|
||||
| pickUpTime | string | 否 | 提货时间 |
|
||||
| packaging | string | 否 | 包装 |
|
||||
| remarks | string | 否 | 备注 |
|
||||
| files | File[] | 是 | 合同扫描件(PDF/图片,单文件≤10MB) |
|
||||
|
||||
---
|
||||
|
||||
### 2.2 批量归档(文件夹 + 批量记录)
|
||||
|
||||
| 方法 | 路径 | 说明 |
|
||||
|------|------|------|
|
||||
| GET | `/contract/folders` | 获取批量归档文件夹列表 |
|
||||
| POST | `/contract/folders` | 新建文件夹 |
|
||||
| GET | `/contract/folders/:id` | 获取单个文件夹 |
|
||||
| PUT | `/contract/folders/:id` | 更新文件夹 |
|
||||
| DELETE | `/contract/folders/:id` | 删除文件夹 |
|
||||
| GET | `/contract/batches` | 获取批量记录列表 |
|
||||
| POST | `/contract/batches` | 批量上传 |
|
||||
| GET | `/contract/batches/:id` | 获取批量记录详情 |
|
||||
| DELETE | `/contract/batches/:id` | 删除批量记录 |
|
||||
|
||||
#### POST /contract/folders
|
||||
|
||||
**请求体**:
|
||||
```json
|
||||
{
|
||||
"name": "文件夹名称",
|
||||
"description": "描述(可选)"
|
||||
}
|
||||
```
|
||||
|
||||
#### GET /contract/batches
|
||||
|
||||
**Query 参数**:`folderId`(可选):按文件夹筛选
|
||||
|
||||
#### POST /contract/batches
|
||||
|
||||
**Content-Type**:`multipart/form-data`
|
||||
|
||||
**表单字段**:
|
||||
| 字段 | 类型 | 必填 | 说明 |
|
||||
|------|------|------|------|
|
||||
| folderId | string | 是 | 归档文件夹 ID |
|
||||
| batchName | string | 是 | 批次名称 |
|
||||
| remark | string | 否 | 备注 |
|
||||
| files | File[] | 是 | 批量文件 |
|
||||
|
||||
---
|
||||
|
||||
### 2.3 合同文件静态资源(供前端预览/下载)
|
||||
|
||||
| 方法 | 路径 | 说明 |
|
||||
|------|------|------|
|
||||
| GET | `/api/contract-files/uploads/{filename}` | 单合同附件 |
|
||||
| GET | `/api/contract-files/batch_archive/{filename}` | 批量归档文件 |
|
||||
| GET | `/api/zsp-contract-files/files/{filename}` | 中尚鹏合同文件 |
|
||||
|
||||
---
|
||||
|
||||
## 三、中尚鹏合同管理 API(/api/zsp-contract)
|
||||
|
||||
与业务合同管理为两套独立体系,用于公司合同(上游/下游/外商采购/货代物流等)。
|
||||
|
||||
| 方法 | 路径 | 说明 |
|
||||
|------|------|------|
|
||||
| GET | `/zsp-contract/folders` | 获取合同文件夹列表 |
|
||||
| POST | `/zsp-contract/folders` | 创建文件夹 |
|
||||
| GET | `/zsp-contract/folders/:id` | 获取单个文件夹 |
|
||||
| PUT | `/zsp-contract/folders/:id` | 更新文件夹 |
|
||||
| DELETE | `/zsp-contract/folders/:id` | 删除文件夹 |
|
||||
| GET | `/zsp-contract/contracts` | 获取合同列表(含详情,可选 folderId) |
|
||||
| POST | `/zsp-contract/contracts` | 上传合同文件 |
|
||||
| GET | `/zsp-contract/contracts/:id` | 获取单个合同详情 |
|
||||
| DELETE | `/zsp-contract/contracts/:id` | 删除合同 |
|
||||
| POST | `/zsp-contract/contracts/:id/details` | 创建合同详情 |
|
||||
| GET | `/zsp-contract/contracts/:id/details` | 获取合同详情列表 |
|
||||
| PUT | `/zsp-contract/details/:id` | 更新合同详情 |
|
||||
| DELETE | `/zsp-contract/details/:id` | 删除合同详情 |
|
||||
| POST | `/zsp-contract/details/batch` | 批量创建合同详情 |
|
||||
| POST | `/zsp-contract/contracts/:id/details/import` | Excel 导入合同详情 |
|
||||
| GET | `/zsp-contract/contracts/:id/details/export` | 导出合同详情到 Excel |
|
||||
|
||||
---
|
||||
|
||||
## 四、公司信息管理 API(/api/company)
|
||||
|
||||
管理公司相关文件(营业执照、开票信息等)。
|
||||
|
||||
| 方法 | 路径 | 说明 |
|
||||
|------|------|------|
|
||||
| GET | `/company/folders` | 获取文件夹列表 |
|
||||
| POST | `/company/folders` | 创建文件夹 |
|
||||
| PUT | `/company/folders/:id` | 更新文件夹 |
|
||||
| DELETE | `/company/folders/:id` | 删除文件夹 |
|
||||
| GET | `/company/files` | 获取文件列表(可选 folderId) |
|
||||
| POST | `/company/files/upload` | 上传公司文件 |
|
||||
| DELETE | `/company/files/:id` | 删除文件 |
|
||||
|
||||
#### POST /company/files/upload
|
||||
|
||||
**Content-Type**:`multipart/form-data`
|
||||
|
||||
**表单字段**:
|
||||
| 字段 | 类型 | 必填 | 说明 |
|
||||
|------|------|------|------|
|
||||
| files | File[] | 是 | 文件列表 |
|
||||
| data | string | 否 | JSON 字符串,含 companyName、fileCategory、expireDate、description、folderId |
|
||||
|
||||
**文件访问**:`GET /api/company-files/{fileUrl}`,其中 fileUrl 为接口返回的 fileUrl 字段(如 `files/xxx.pdf`)。
|
||||
|
||||
---
|
||||
|
||||
## 五、提货管理 API
|
||||
|
||||
提货管理包含四块:**我要提货**(提货委托)、**提货明细**(出库单)、**库存**(仓库/入库单/库存汇总)、**货权转移**。以下路径均需在 Base URL 下加 `/api` 前缀(与合同等接口一致),且需登录态。
|
||||
|
||||
### 5.1 我要提货(/api/apply-delivery)
|
||||
|
||||
| 方法 | 路径 | 说明 |
|
||||
|------|------|------|
|
||||
| GET | `/apply-delivery/list` | 分页列表 |
|
||||
| GET | `/apply-delivery/detail/:id` | 单条详情 |
|
||||
| POST | `/apply-delivery/create` | 新增 |
|
||||
| PUT | `/apply-delivery/update/:id` | 更新 |
|
||||
| PUT | `/apply-delivery/cancel/:id` | 取消(状态改为 cancelled) |
|
||||
| DELETE | `/apply-delivery/:id` | 删除 |
|
||||
|
||||
**GET /apply-delivery/list**
|
||||
Query:`page`、`pageSize`、`orderNumber`、`blNumber`、`status`(可选)。
|
||||
|
||||
**响应**:`data: { items: DeliveryApply[], total, page, pageSize }`。
|
||||
单条字段:id, orderNumber, blNumber, licensePlate, driverName, driverPhone, driverIdCard, commodity, quantity, unit, deliveryDate, deliveryAddress, warehouse, status, remark, createTime, updateTime。
|
||||
status:pending / approved / in_progress / completed / cancelled。
|
||||
|
||||
**POST /apply-delivery/create**
|
||||
Body(JSON):blNumber, licensePlate, driverName, driverPhone, driverIdCard, commodity, quantity, unit, deliveryDate, deliveryAddress, warehouse, remark。
|
||||
服务端自动生成 orderNumber、status=pending、createTime/updateTime。
|
||||
|
||||
---
|
||||
|
||||
### 5.2 提货明细(/api/delivery-details)
|
||||
|
||||
| 方法 | 路径 | 说明 |
|
||||
|------|------|------|
|
||||
| GET | `/delivery-details/list` | 分页列表 |
|
||||
| GET | `/delivery-details/detail/:id` | 单条详情 |
|
||||
| POST | `/delivery-details/create` | 新增出库单 |
|
||||
| PUT | `/delivery-details/update/:id` | 更新 |
|
||||
| DELETE | `/delivery-details/:id` | 删除 |
|
||||
|
||||
**GET /delivery-details/list**
|
||||
Query:`page`、`pageSize`、`outboundNumber`、`blNumber`、`outboundStatus`(可选)。
|
||||
|
||||
**响应**:`data: { items: DeliveryOutbound[], total, page, pageSize }`。
|
||||
单条字段:id, outboundNumber, deliveryOrderNumber, blNumber, licensePlate, driverName, driverPhone, commodity, commodityCode, quantity, unit, outboundDate, warehouse, operator, outboundStatus, receiptStatus, remark, createTime, updateTime。
|
||||
|
||||
---
|
||||
|
||||
### 5.3 库存(/api/inventory)
|
||||
|
||||
#### 仓库
|
||||
|
||||
| 方法 | 路径 | 说明 |
|
||||
|------|------|------|
|
||||
| GET | `/inventory/warehouses` | 仓库列表(不分页) |
|
||||
| POST | `/inventory/warehouses/create` | 新增仓库 |
|
||||
| PUT | `/inventory/warehouses/update/:id` | 更新 |
|
||||
| DELETE | `/inventory/warehouses/delete/:id` | 删除 |
|
||||
|
||||
仓库字段:id, code, name, address, contactPerson, contactPhone, status, remark, createTime, updateTime。
|
||||
|
||||
#### 入库单
|
||||
|
||||
| 方法 | 路径 | 说明 |
|
||||
|------|------|------|
|
||||
| GET | `/inventory/inbound/list` | 分页列表 |
|
||||
| POST | `/inventory/inbound/create` | 新增入库单 |
|
||||
| PUT | `/inventory/inbound/update/:id` | 更新 |
|
||||
| DELETE | `/inventory/inbound/delete/:id` | 删除 |
|
||||
|
||||
**GET /inventory/inbound/list**
|
||||
Query:`page`、`pageSize`、`warehouse`、`commodity`(可选)。
|
||||
入库单字段:id, inboundNumber, warehouse, commodity, commodityCode, blNumber, blWeight, inboundWeight, owner, contractNumber, inboundDate, operator, remark, createTime, updateTime。
|
||||
|
||||
#### 库存汇总
|
||||
|
||||
| 方法 | 路径 | 说明 |
|
||||
|------|------|------|
|
||||
| GET | `/inventory/summary` | 按仓库+商品汇总:入库 - 出库 - 货权转移 = 当前库存 |
|
||||
|
||||
Query:`warehouse`、`commodity`(可选)。
|
||||
响应:`data` 为汇总列表,含仓库、商品、入库量、出库量、货权转移量、当前库存等。
|
||||
|
||||
---
|
||||
|
||||
### 5.4 货权转移(/api/ownership-transfer)
|
||||
|
||||
| 方法 | 路径 | 说明 |
|
||||
|------|------|------|
|
||||
| GET | `/ownership-transfer/list` | 分页列表 |
|
||||
| GET | `/ownership-transfer/detail/:id` | 单条详情(含附件列表) |
|
||||
| POST | `/ownership-transfer/create` | 新增(仅表单) |
|
||||
| POST | `/ownership-transfer/create-with-files` | 新增(表单 + 附件) |
|
||||
| DELETE | `/ownership-transfer/:id` | 删除 |
|
||||
|
||||
**GET /ownership-transfer/list**
|
||||
Query:分页及筛选(如 transferNumber, blNumber, status, transferor, transferee, startDate, endDate 等,以实际后端为准)。
|
||||
|
||||
**POST /ownership-transfer/create-with-files**
|
||||
Content-Type:`multipart/form-data`。
|
||||
表单字段:transferDate, blNumber, quantity, commodity, warehouse, transferor, transferee, remark;files:附件。
|
||||
货权转移单字段:id, transferNumber, transferDate, blNumber, quantity, commodity, warehouse, transferor, transferee, status, remark, fileList, createTime, updateTime。
|
||||
附件存于服务端 `./data/ownership/`,返回的 fileList 中 url 为相对路径(如 `ownership/xxx`)。若需通过 HTTP 访问,可配置静态路由(如 `/api/ownership-files/` 指向该目录),并在文档中说明。
|
||||
|
||||
---
|
||||
|
||||
## 六、错误码与状态
|
||||
|
||||
- `200`:成功
|
||||
- `400`:请求参数错误
|
||||
- `404`:资源不存在
|
||||
- `500`:服务器内部错误
|
||||
77
docs/deploy/nginx.conf
Executable file
77
docs/deploy/nginx.conf
Executable file
@@ -0,0 +1,77 @@
|
||||
# 中尚鹏管理系统 - Nginx 生产配置
|
||||
# 放置于 /etc/nginx/conf.d/zsp.conf 或 /etc/nginx/sites-enabled/zsp
|
||||
#
|
||||
# 前置条件:
|
||||
# 1. 前端构建产物位于 /var/www/zsp/dist
|
||||
# 2. 后端服务运行于 127.0.0.1:8080
|
||||
# 3. 后端静态文件目录 /var/www/zsp/data 可读
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
server_name your-domain.com;
|
||||
|
||||
# 前端静态资源
|
||||
root /var/www/zsp/dist;
|
||||
index index.html;
|
||||
|
||||
# Gzip 压缩
|
||||
gzip on;
|
||||
gzip_types text/plain text/css application/json application/javascript text/xml application/xml text/javascript image/svg+xml;
|
||||
gzip_min_length 1024;
|
||||
|
||||
# 前端 SPA 路由:所有非文件/非 API 请求返回 index.html
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
|
||||
# API 反向代理到后端
|
||||
location /api/ {
|
||||
proxy_pass http://127.0.0.1:8080;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
|
||||
# 大文件上传支持(合同附件等)
|
||||
client_max_body_size 100m;
|
||||
proxy_read_timeout 300s;
|
||||
proxy_send_timeout 300s;
|
||||
}
|
||||
|
||||
# 健康检查端点
|
||||
location /health {
|
||||
proxy_pass http://127.0.0.1:8080/health;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
access_log off;
|
||||
}
|
||||
|
||||
# 静态文件缓存(合同附件、公司文件等)
|
||||
location /api/contract-files/ {
|
||||
proxy_pass http://127.0.0.1:8080;
|
||||
proxy_set_header Host $host;
|
||||
expires 7d;
|
||||
add_header Cache-Control "public, immutable";
|
||||
}
|
||||
|
||||
location /api/zsp-contract-files/ {
|
||||
proxy_pass http://127.0.0.1:8080;
|
||||
proxy_set_header Host $host;
|
||||
expires 7d;
|
||||
add_header Cache-Control "public, immutable";
|
||||
}
|
||||
|
||||
location /api/company-files/ {
|
||||
proxy_pass http://127.0.0.1:8080;
|
||||
proxy_set_header Host $host;
|
||||
expires 7d;
|
||||
add_header Cache-Control "public, immutable";
|
||||
}
|
||||
|
||||
# 错误页面
|
||||
error_page 404 /index.html;
|
||||
error_page 500 502 503 504 /50x.html;
|
||||
location = /50x.html {
|
||||
root /usr/share/nginx/html;
|
||||
}
|
||||
}
|
||||
160
docs/deploy/部署说明.md
Executable file
160
docs/deploy/部署说明.md
Executable file
@@ -0,0 +1,160 @@
|
||||
# 中尚鹏管理系统 - 部署说明
|
||||
|
||||
```
|
||||
作者: rovina
|
||||
最近修订: 2026/3/2
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 一、环境要求
|
||||
|
||||
| 组件 | 版本 |
|
||||
|------|------|
|
||||
| Node.js | 18+ |
|
||||
| pnpm | 8+ |
|
||||
| Go | 1.21+ |
|
||||
| MySQL | 8.0+ |
|
||||
|
||||
---
|
||||
|
||||
## 二、后端部署
|
||||
|
||||
### 2.1 配置文件
|
||||
|
||||
复制并修改 `backend/configs/config.dev.yaml`:
|
||||
|
||||
```yaml
|
||||
server:
|
||||
port: 8080
|
||||
mode: "release" # 生产环境建议 release
|
||||
read_timeout: 30
|
||||
write_timeout: 30
|
||||
|
||||
database:
|
||||
host: "127.0.0.1" # 或 MySQL 服务地址
|
||||
port: 3306
|
||||
user: "root"
|
||||
password: "your_password"
|
||||
dbname: "myapp"
|
||||
max_open_conns: 100
|
||||
max_idle_conns: 10
|
||||
reset_database: false # 生产务必 false
|
||||
|
||||
jwt:
|
||||
secret: "your_jwt_secret"
|
||||
expire_hours: 24
|
||||
```
|
||||
|
||||
### 2.2 启动
|
||||
|
||||
```bash
|
||||
cd backend
|
||||
|
||||
# 指定配置文件(可选,默认 ./configs/config.dev.yaml)
|
||||
export CONFIG_PATH=./configs/config.prod.yaml
|
||||
|
||||
go run ./cmd/api
|
||||
# 或编译后运行
|
||||
go build -o zsp-api ./cmd/api && ./zsp-api
|
||||
```
|
||||
|
||||
### 2.3 数据目录
|
||||
|
||||
- 单合同附件:`./data/contract/uploads/`
|
||||
- 批量归档:`./data/contract/batch_archive/`
|
||||
|
||||
首次运行会自动创建,需确保进程有写权限。
|
||||
|
||||
---
|
||||
|
||||
## 三、前端部署
|
||||
|
||||
### 3.1 开发环境
|
||||
|
||||
```bash
|
||||
cd frontend
|
||||
pnpm install
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
前端默认端口 8848(可改 `.env.development` 中 VITE_PORT),API 请求 `/api/*` 会代理到 `http://127.0.0.1:8080`。
|
||||
|
||||
### 3.2 生产构建
|
||||
|
||||
```bash
|
||||
cd frontend
|
||||
pnpm install
|
||||
pnpm build
|
||||
```
|
||||
|
||||
产物在 `dist/`,需配合 Nginx 等 Web 服务器部署。
|
||||
|
||||
### 3.3 为什么打包部署后访问不到 8080 的数据?
|
||||
|
||||
打包后前端是**静态资源**,在**用户浏览器**里运行。请求会发到「打开页面时的那台服务器的同源地址」:
|
||||
|
||||
- 开发时:页面来自 `http://127.0.0.1:5173`,Vite 把 `/api` 代理到本机 8080,所以能访问。
|
||||
- 部署后:页面来自 `http://你的服务器:80`,浏览器只会把 `/api` 发到 `http://你的服务器:80/api`,**不会**自动发到 8080。
|
||||
|
||||
因此必须让**提供前端页面的 Web 服务器**(如 Nginx)把 `/api` 反向代理到本机 8080(或后端容器)。同时前端已统一用**相对路径** `/api/xxx`,不再写死 `127.0.0.1:8080`,这样部署后即可通过同源访问到后端。
|
||||
|
||||
### 3.4 Nginx 配置
|
||||
|
||||
完整配置文件见 `docs/deploy/nginx.conf`,包含:
|
||||
|
||||
- SPA 路由重写(`try_files $uri /index.html`)
|
||||
- API 反向代理(`/api` → `127.0.0.1:8080`)
|
||||
- 大文件上传支持(`client_max_body_size 100m`)
|
||||
- 静态文件缓存(合同附件 7 天缓存)
|
||||
- Gzip 压缩
|
||||
|
||||
---
|
||||
|
||||
## 四、Docker 部署(可选)
|
||||
|
||||
### 4.1 后端 Dockerfile 示例
|
||||
|
||||
```dockerfile
|
||||
FROM golang:1.21-alpine AS builder
|
||||
WORKDIR /app
|
||||
COPY go.mod go.sum ./
|
||||
RUN go mod download
|
||||
COPY . .
|
||||
RUN CGO_ENABLED=0 go build -o zsp-api ./cmd/api
|
||||
|
||||
FROM alpine:latest
|
||||
WORKDIR /app
|
||||
COPY --from=builder /app/zsp-api .
|
||||
COPY --from=builder /app/configs ./configs
|
||||
RUN mkdir -p data/contract/uploads data/contract/batch_archive
|
||||
EXPOSE 8080
|
||||
CMD ["./zsp-api"]
|
||||
```
|
||||
|
||||
### 4.2 运行
|
||||
|
||||
```bash
|
||||
# 需挂载配置与数据目录
|
||||
docker run -d -p 8080:8080 \
|
||||
-v $(pwd)/configs:/app/configs \
|
||||
-v $(pwd)/data:/app/data \
|
||||
-e CONFIG_PATH=/app/configs/config.prod.yaml \
|
||||
zsp-backend
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 五、健康检查
|
||||
|
||||
- 后端:`GET http://{host}:8080/health` 返回 `{"status":"ok"}`
|
||||
- 合同 API:`GET http://{host}:8080/api/contract` 需返回 JSON
|
||||
|
||||
---
|
||||
|
||||
## 六、常见问题
|
||||
|
||||
1. **跨域**:后端已启用 CORS,生产环境若前后端同域可无需额外配置。
|
||||
2. **文件访问**:合同文件通过 `/api/contract-files/{path}` 访问,确保 Nginx 将 `/api` 转发至后端。
|
||||
3. **数据库**:首次部署需确保 MySQL 已创建 `myapp` 库,表由 GORM AutoMigrate 自动创建。
|
||||
4. **JWT 认证**:除 `/api/auth/login`、`/api/auth/register` 外,所有 API 均需 JWT 认证。前端需在登录后存储 token,并在后续请求中通过 `Authorization: Bearer <token>` 头传递。token 有效期为 24 小时。
|
||||
15
docs/dev/Talk.md
Executable file
15
docs/dev/Talk.md
Executable file
@@ -0,0 +1,15 @@
|
||||
# 开发要求
|
||||
|
||||
和甲方沟通过程中的内容
|
||||
|
||||
他们其实诉求就是他们跟单人的诉求是这样的,因为我们比如说现在手上有1000吨货,那么这批货客户是要分很长时间才会提完的,可能就几十吨或者100吨,慢慢的提每一次,那么在第一个合同管理栏他。少写了,比如说除了上传合同之外,然后去做就空出来一个excel表格。这个Excel表格呢,他们就手动录入合同里面的一些数据信息数量啊,单价呀,已经提货的数量啊,然后这些东西。然后,系统呢,它那个excel表格就会自动生成海应提货,就是未提的货物对应那个合同有多少吨就会显示出来。然后,显示出来之后呢,合同管理这一栏就跟第四个查询系统这一块儿。就是要有关联起来,比如说关联就是关联合同编号,或者是商品信息,或者是提单号这样。
|
||||
|
||||
他这个大框架改的,我刚刚电话沟通了,我算是明白了一些,但是就是明天,我让他们上午再去做最后的就是确定下来,因为。就是这个主菜单,肯定是后面不能再改的。然后呢,二级菜单我们每一次做二级菜单,比如说合同管理这一栏要实现什么,在做之前我们再沟通一次。就是我觉得这样就也免得就是。就就麻烦你改来改去,因为我虽然不懂这个东西,但我知道可能大框架他们有变动,那等于你前面的可能就都白做了,你要重新再来。
|
||||
|
||||
|
||||
1、会涉及到多处表格内部分数据关联后生成新的表格数据。2、比如在利润表中的“成本“栏,是根据”货代费用+杂费”表格中数据以及利润表中单独设的“服务费”相加而成。但是能不能实现点“成本”金额,自动显示出这个成本总金额的构成,比如“货代费20万,杂费12万”。
|
||||
|
||||
问题:现在前端部分有两个问题:第一个:"营业明细表"的具体内容和字段是什么?是按年/月/合同维度汇总的报表?还是某个甲方提供的 Excel 模板?
|
||||
第二个:合同执行情况中"差款"的计算规则是什么?是 合同金额 − 已结算金额?还是 合同金额 − 银行流水中已收金额?数据分别来自哪些表?
|
||||
甲方回答:第一个,汇总的报表。
|
||||
第二个,等于收(付)款金额-提货数量*提货单价(提货数量需手填,单价可应用合同单价)第一个数据来自于“营业明细表”。第二个数据来自于“提货明细”这个表格。
|
||||
220
docs/dev/当前程序不足分析.md
Executable file
220
docs/dev/当前程序不足分析.md
Executable file
@@ -0,0 +1,220 @@
|
||||
# 中尚鹏管理系统 - 当前程序不足分析
|
||||
|
||||
本文档基于对现有前端代码与《项目开发要求文档》的对照,整理当前实现的不足与待确认点。
|
||||
|
||||
> **更新说明**:以下问题已在后续迭代中修复:
|
||||
> - JWT 认证中间件已启用(`backend/internal/server/route.go`),所有业务 API 需携带 `Authorization: Bearer <token>` 头
|
||||
> - `reset_database` 已设为 `false`(配置文件和部署说明均已更新),重启不再清空数据库
|
||||
> - Handler 层测试已编写(`backend/test/`),覆盖用户注册/登录/查询、合同列表/查询/文件夹管理
|
||||
> - Delivery Handler 已重构为 Handler → Service → Repository 三层架构,不再直接使用 `*gorm.DB`
|
||||
|
||||
---
|
||||
|
||||
## 一、与需求文档的菜单对照
|
||||
|
||||
| 需求文档模块 | 需求三级菜单概要 | 当前前端情况 |
|
||||
|------------------------|--------------------------------------|--------------|
|
||||
| 合同管理 | 新增合同 / 中尚鹏合同管理 / 公司信息 | 路由与页面均有 |
|
||||
| 提货管理 | 我要提货 / 提货明细 / 库存 / 货权转移 / 其它 | 路由与页面均有 |
|
||||
| 财务管理 | 结算 / 发票 / 利润明细 / 货代杂费+其它 / 对账单 | 路由与页面均有 |
|
||||
| 查询+报表中心 | 合同执行 / 营业明细表 / 其它明细表 / 商品基础资料 / 其它 | 路由与页面均有 |
|
||||
| 采购管理 | 采购报单 / 采购运踪 / 国外供应商 / 采购预算 | 路由与页面均有 |
|
||||
| 权限管理 | 1 / 2(需求未展开) | 现有 permission 页面/按钮 |
|
||||
|
||||
---
|
||||
|
||||
## 二、功能与业务层面的不足
|
||||
|
||||
### 2.1 合同管理
|
||||
|
||||
- **新增合同**
|
||||
- 已实现:单合同录入、批量上传、提单号最多 15 个校验。
|
||||
- 不足/待确认:需求中“录入合同内容中相关数据的表格(会涉及简单的公式,我们会给出)”是否已按贵方提供的公式在表格中实现?若未提供公式,当前表格是否有明确占位或说明?
|
||||
|
||||
- **中尚鹏合同管理**
|
||||
- 已实现:文件夹归类、上传、按合同编号/公司名/商品/提单号等搜索。
|
||||
- 不足:需求要求“通过文件名包含商品、公司名、合同编号”进行搜索。当前是否对上传文件的**文件名**做解析并写入可搜索字段?若未解析文件名,则与“通过三要素搜索对应合同”的预期存在差距。
|
||||
|
||||
- **公司信息管理**
|
||||
- 已存在页面,需确认:是否仅做“公司相关文件存储(如营业执照、开票信息)”即可,有无额外字段或审批流程要求。
|
||||
|
||||
### 2.2 提货管理
|
||||
|
||||
- **我要提货**
|
||||
- 已实现:新增/变更/取消提货委托,以及模板下载等。
|
||||
- 不足/待确认:提货单是否已完全按贵方提供的**提货单模板**(含车牌号、提单号、司机信息、数量、商品品类等)生成与展示?模板若后续变更,是否有统一维护方式?
|
||||
|
||||
- **提货明细**
|
||||
- 需求:新增出库单、查看出库明细、收货确认单,且“录入数据的表格(后期我们给模板)”。
|
||||
- 不足:当前是否已接入“后期给的模板”?若未接入,表格结构与字段是否已与业务方对齐?
|
||||
|
||||
- **库存信息**
|
||||
- 需求公式:**入库表格中数量 − 新增出库单中数量 − 货权转移数量 = 库存数量**。
|
||||
- 不足:该公式是否在**前端展示与后端计算**中都已严格按此逻辑实现?库存明细是否按“仓库+商品+货主”等维度正确汇总?
|
||||
|
||||
- **货权转移**
|
||||
- 需求字段:货转日期、提单号、数量、商品、库方、货权出让方、货权接收方等。
|
||||
- 不足:当前“货权转移”相关表格与表单字段是否已全部包含上述字段,且与扫描件录入需求一致?
|
||||
|
||||
### 2.3 财务管理
|
||||
|
||||
- **结算管理**
|
||||
- 需求:新增结算单(导出下游合同结算单)、按单号查询、上传已结算单备份。
|
||||
- 不足:导出下游合同结算单的**数据来源与模板**是否已定稿?上传备份后的检索与展示是否满足“按单号查询”的完整流程?
|
||||
|
||||
- **发票管理**
|
||||
- 需求:上游发票、下游发票、货代杂费发票的上传及“对应表格录入发票对应信息”。
|
||||
- 不足:发票表格的字段是否与后续“待开发票情况”“已开票明细表”等统计完全一致,便于关联与报表使用?
|
||||
|
||||
- **利润明细**
|
||||
- 需求:“根据我们提供的 excel 表格创建一个表格(需要关联其他表格中数据 + 设置好栏目以及函数,后续自行手动录入)”。
|
||||
- 不足:当前利润明细页是否已**关联合同/结算/发票/货代杂费等其它表数据**?栏目与公式是否已按贵方提供的 Excel 定稿?若 Excel 尚未提供,当前页面是占位还是已有临时结构?
|
||||
|
||||
- **货代费用 + 其他杂费**
|
||||
- 需求字段:提单号、上游合同编号、费用项目、金额、付款日期、货代/物流公司名。
|
||||
- 不足:当前表单与列表是否已完全覆盖上述字段,并与“利润明细”“历史清关成本”等可关联?
|
||||
|
||||
- **对账单**
|
||||
- 需求文档中“5.对账单”无具体内容描述。
|
||||
- 不足:对账单的业务范围、字段、与结算/发票的关系是否已有口头或补充文档说明?当前页面若存在,是否仅为占位?
|
||||
|
||||
### 2.4 查询 + 报表中心
|
||||
|
||||
- **查询上下游客户合同执行情况**
|
||||
- 需求:按合同编号/公司名/商品/提单号/日期查询,且**可查询到**:
|
||||
- 该合同**剩余未提数量**(与“新增合同”表格关联);
|
||||
- **差款**及**待开发票情况**(与“发票管理”关联);
|
||||
- 支持**上传 Excel 表格(银行流水)**。
|
||||
- 不足:
|
||||
- 当前“合同执行”页的**付款状态**为前端写死的 `"unpaid"`,并未与真实结算/发票数据关联,无法体现真实“差款”与“待开发票”。
|
||||
- “剩余未提数量”是否已与“新增合同”及出库/货权转移数据打通并一致?
|
||||
- “上传银行流水”功能是否已实现?若已实现,银行流水与合同、收付款的关联规则是否明确?
|
||||
|
||||
- **营业明细表**
|
||||
- 需求:① 2025 年中尚鹏业务明细表。
|
||||
- 不足:当前“营业明细表”路由对应的是 **BusinessDetails.vue**,从代码看更偏向“合同文件/营业相关合同”的列表与搜索,与需求中的“2025 年业务明细表”可能不是同一张表。需确认:**营业明细表** 究竟是“按年的业务汇总表”,还是“合同维度明细表”,以便与菜单命名和实现对齐。
|
||||
|
||||
- **其它明细表**
|
||||
- 需求:① 已开票明细表 ② 货代杂费明细表 ③ 历史清关成本明细表。
|
||||
- 不足:当前“其它明细表”为单一列表(OtherDetails),未在菜单或页面内拆分为上述**三个子表**。三个表的字段与数据来源是否已定义?是否需要在同一页用 Tab 区分,还是独立三个页面?
|
||||
|
||||
- **商品基础资料**
|
||||
- 需求:燕麦/大麦/豌豆/亚麻籽/葵粕/菜籽粕等,可批量上传扫描件或文档。
|
||||
- 不足:当前是否已按“商品类型”或“品类”做分类归档与展示?批量上传后的检索方式是否明确?
|
||||
|
||||
### 2.5 采购管理
|
||||
|
||||
- **采购报单、采购运踪明细、国外供应商**
|
||||
- 不足:需求为“批量上传扫描件 + 录入扫描件内容的表格”或“根据所提供的 excel 在系统中创建表格”。当前各页是否已接入贵方提供的**模板/Excel 结构**?若未提供,当前表格是否为暂定结构?
|
||||
|
||||
- **采购预算**
|
||||
- 需求:“已有单独的预算测算系统,最后按该预算系统功能等添加到我们系统中(这个最后有时间慢慢弄)”。
|
||||
- 不足:当前采购预算页是占位还是已有部分对接?与“单独预算系统”的对接方式(单点登录、接口、嵌入等)是否有初步方案?
|
||||
|
||||
### 2.6 权限管理
|
||||
|
||||
- 需求文档仅写“1”“2”,无具体说明。
|
||||
- 不足:当前为通用“页面权限 + 按钮权限”结构。贵方是否需要**角色名、数据范围(如按公司/部门)、审批流**等具体需求?若有,需补充说明以便后续设计。
|
||||
|
||||
---
|
||||
|
||||
## 三、技术与实现层面的不足
|
||||
|
||||
### 3.1 API 与后端
|
||||
|
||||
- **双套 API 基址**
|
||||
- 合同模块(如 `contract/upload`、`contract/batch-upload`)通过 `baseUrlApi` 直接写死 `http://127.0.0.1:8080/api/`。
|
||||
- 其余模块(如中尚鹏合同、财务、提货等)通过 `@/utils/http` 走相对路径(通常为 `/api`),在开发环境会经 Vite 代理到 127.0.0.1:8080。
|
||||
- 不足:基址不统一,不利于环境切换(开发/测试/生产)与后续维护;若后端实际未部署在 8080 或路径不同,合同相关请求易出错。
|
||||
|
||||
- **后端存在性**
|
||||
- 当前仓库内未见后端工程目录,仅前端调用 8080 接口。
|
||||
- 不足:需确认后端是否独立仓库或独立服务、接口文档是否齐全、现有前端调用的接口是否已全部实现并联调通过。
|
||||
|
||||
### 3.2 功能未完成或占位
|
||||
|
||||
- **合同执行情况**
|
||||
- 导出功能为“导出功能开发中”提示,未实现真实导出。
|
||||
|
||||
- **差款与待开发票**
|
||||
- 未与结算、发票、银行流水等数据打通,无法展示真实“差款”与“待开发票情况”。
|
||||
|
||||
- **银行流水**
|
||||
- 需求提到“上传 excel 表格(银行流水)”,当前代码中未检索到“银行流水”相关实现,需确认是否未做或位于其他模块。
|
||||
|
||||
### 3.3 数据关联与一致性
|
||||
|
||||
- 需求多处强调“关联其他表格”“设置好栏目以及函数”,例如:
|
||||
- 利润明细 关联 合同/结算/发票/货代等;
|
||||
- 合同执行 关联 新增合同、发票管理;
|
||||
- 库存 = 入库 − 出库 − 货权转移。
|
||||
- 不足:当前实现中,这些**跨模块的数据关联与计算**是否已在后端或前端全部落地并校验一致,需要逐项对照需求做确认与测试。
|
||||
|
||||
---
|
||||
|
||||
## 四、小结
|
||||
|
||||
- **菜单与路由**:与需求文档的一、二、三级菜单基本对应,但“营业明细表”与“其它明细表”的页面职责与需求描述需要对齐。
|
||||
- **业务逻辑**:合同执行中的“差款”“待开发票”“剩余未提数量”“银行流水”未按需求打通;利润明细、库存公式、对账单、采购/提货模板等依赖贵方提供的表格或公式,需确认是否已提供并已实现。
|
||||
- **技术**:API 基址不统一、后端与接口实现情况不透明、部分导出与上传功能仍为占位或未实现。
|
||||
|
||||
建议在进入下一阶段开发前,先根据下文《需求确认问题清单》与业务方确认,再制定实现与联调计划。
|
||||
|
||||
---
|
||||
|
||||
## 五、需求确认问题清单(请产品/业务方确认)
|
||||
|
||||
以下问题基于《项目开发要求文档》整理,便于明确开发范围与优先级,**请逐项确认或补充**。
|
||||
|
||||
### 合同管理
|
||||
|
||||
1. **新增合同**:需求中提到“会涉及简单的公式,我们会给出”。公式是否已提供?若已提供,请说明存放位置或文档名;若未提供,当前表格中的计算逻辑是否按临时规则处理即可?
|
||||
2. **中尚鹏合同管理**:搜索是否必须基于**上传文件的文件名**解析出“商品、公司名、合同编号”?
|
||||
- 若是,文件名命名规范是什么(例如:`商品_公司名_合同编号.pdf`)?是否需要在系统中做“文件名解析并写入可搜索字段”的功能?
|
||||
3. **公司信息管理**:除“上传扫描件 + 文件夹归类”外,是否需要维护公司主数据(如公司名、税号、开户行等)?还是仅文件存储即可?
|
||||
|
||||
### 提货管理
|
||||
|
||||
4. **我要提货**:提货单模板(含车牌号、提单号、司机信息、数量、商品品类等)是否已定稿?若已提供,请说明文件位置;若会变更,变更后由谁在系统中更新?
|
||||
5. **提货明细**:需求写“录入数据的表格(后期我们给模板)”。模板是否已提供?若未提供,当前表格字段是否可作为正式版使用?
|
||||
6. **库存信息**:公式“入库 − 出库 − 货权转移 = 库存”是否必须按**仓库 + 商品 + 货主(及合同)**等维度精确到最细粒度?是否有“只按仓库汇总”等简化展示需求?
|
||||
7. **货权转移**:除需求列出的字段外,是否还有必填或必展示字段?已生效的货权转移函“上传”与“新增货权转移函”是两条独立流程还是需要关联?
|
||||
|
||||
### 财务管理
|
||||
|
||||
8. **结算管理**:“导出下游合同结算单”的数据来源是哪些表?是否有固定 Excel 模板?若有,请提供模板或字段说明。
|
||||
9. **发票管理**:上游/下游/货代杂费发票的“对应表格”字段是否已定稿?是否需要与“已开票明细表”“待开发票情况”完全一致以便关联?
|
||||
10. **利润明细**:需求写“根据我们提供的 excel 表格创建……关联其他表格中数据 + 设置好栏目以及函数”。
|
||||
- Excel 模板是否已提供?若已提供,请说明位置。
|
||||
- “其他表格”具体指哪些(合同、结算、发票、货代费用、清关成本等)?关联规则(按合同号、提单号、日期等)是什么?
|
||||
11. **货代费用 + 其他杂费**:与“利润明细”“历史清关成本明细表”的关联方式是否已明确(例如通过提单号、合同编号、日期)?
|
||||
12. **对账单**:对账单的业务含义、使用场景、字段与数据来源是否已有说明?若有补充文档或样例,请提供或简述。
|
||||
|
||||
### 查询 + 报表中心
|
||||
|
||||
13. **合同执行情况**:
|
||||
- “差款”的定义是什么(例如:合同金额 − 已收款项)?数据来源是结算单、银行流水还是其它?
|
||||
- “待开发票情况”是指:已结算但未开票的金额/列表?数据来源是否与发票管理表一致?
|
||||
- “上传银行流水”的 Excel 格式是否已定?上传后是否需要解析并自动匹配到合同/收付款,还是仅作附件存档?
|
||||
14. **营业明细表**:“2025 年中尚鹏业务明细表”与当前菜单中的“营业明细表”是否同一张表?若否,请说明需求中的这张表应展示哪些维度(按合同、按客户、按商品、按月份等)。
|
||||
15. **其它明细表**:已开票明细表、货代杂费明细表、历史清关成本明细表:
|
||||
- 三张表是独立页面还是同一页面下三个 Tab/子表?
|
||||
- 每张表的字段与数据来源是否已定义?历史清关成本的数据从哪里录入或导入?
|
||||
|
||||
### 采购管理
|
||||
|
||||
16. **采购报单 / 采购运踪明细 / 国外供应商**:各模块的“我们提供的 excel”或“扫描件内容表格”是否已提供?若未提供,是否同意以当前系统中的表格结构为初版,后续再按模板调整?
|
||||
17. **采购预算**:与“单独的预算测算系统”的对接方式是否有初步方案(例如:链接跳转、单点登录、接口拉取数据、iframe 嵌入等)?优先级是否确认为“最后有时间再弄”?
|
||||
|
||||
### 权限管理
|
||||
|
||||
18. 需求中“权限管理 1、2”具体指什么?是否需要角色管理、数据权限(按公司/部门)、审批流、操作日志等?若有,请列出清单或提供简单说明。
|
||||
|
||||
### 其它
|
||||
|
||||
19. 是否有未在《项目开发要求文档》中体现的隐藏需求或口头约定(例如:某表需要审批后才能生效、某报表需要定时发送邮件等)?
|
||||
20. 后端服务与接口:当前后端是否已有独立项目或部署地址?前端调用的接口是否有完整文档(如 Swagger/Word)?若部分接口尚未实现,请标明模块或接口路径,便于排期。
|
||||
|
||||
---
|
||||
|
||||
*文档生成后请根据实际业务与最新需求增删改,本分析不修改任何代码。*
|
||||
85
docs/dev/财务管理-待确认问题.md
Executable file
85
docs/dev/财务管理-待确认问题.md
Executable file
@@ -0,0 +1,85 @@
|
||||
# 财务管理模块 - 待确认问题清单
|
||||
|
||||
> 在实现财务管理前后端前,请逐条回答或标注「暂不考虑」。回答完成后再补充前后端代码。
|
||||
|
||||
---
|
||||
|
||||
## 一、结算管理
|
||||
|
||||
**1.1** 需求写「新增结算单(导出下游合同结算单)」:
|
||||
「导出下游合同结算单」是指:**(a)** 从系统中已有的下游合同数据(如中尚鹏合同/业务合同)里选一份合同,一键生成并下载一份结算单文档(PDF/Excel)?**(b)** 还是仅在前端提供「结算单表单」,手动录入后保存,再支持导出为文件?**(c)** 其他方式?
|
||||
|
||||
**1.2** 「根据单号查询」:结算单号规则是否由系统自动生成(如 `STL2026030001`)?还是允许用户手动填写单号?
|
||||
|
||||
**1.3** 「上传结算单(已交易且结算完毕的,主要为了备份)」:
|
||||
上传的结算单是否需要和「已录入的结算单记录」关联(例如上传后关联到某条结算单号)?还是仅作为独立附件存储,按文件夹/时间归类即可?
|
||||
|
||||
**1.4** 结算单「录入扫描件内容数据的表格」除当前模型已有字段(结算单号、合同编号、合同类型、公司名、商品、数量、单价、金额、结算日期、状态、附件路径、备注)外,是否还需要增加或删减字段?若有,请列出。
|
||||
|
||||
---
|
||||
|
||||
## 二、发票管理
|
||||
|
||||
**2.1** 需求为「上游发票(上传)、下游发票(上传)、货代杂费发票(上传)」且已确认三张独立表。
|
||||
当前三张表均含:发票号、合同编号、提单号、金额/税额/价税合计、开票日期、状态、附件路径、备注;上游多了「供应商名」、下游多了「客户名」、货代杂费多了「公司名」「费用项目」。
|
||||
是否还需要其他字段(如税率、发票类型、购方/销方税号等)?
|
||||
|
||||
**2.2** 发票的「合同编号」是否需要与系统内已有合同(业务合同/中尚鹏合同)做下拉关联选择?还是自由文本即可?
|
||||
|
||||
**2.3** 上传发票后,附件存储方式是否与「合同管理/公司信息」一致:按文件夹归类 + 记录文件路径到该条发票记录的 `file_path`?
|
||||
|
||||
---
|
||||
|
||||
## 三、利润明细
|
||||
|
||||
**3.1** 需求为「2025年业务利润表」「关联其他表格中数据 + 设置好栏目以及函数,后续自行手动录入」。
|
||||
当前理解:收入来自合同金额,成本 = 货代费用 + 杂费 + 服务费,利润 = 收入 − 成本。
|
||||
请确认:**(a)** 利润表是按「年度」一张总表(如 2025 年一整年),**(b)** 还是按「合同/业务单」逐条有对应利润记录?**(c)** 或两者都要(既有年度汇总,又有单笔明细)?
|
||||
|
||||
**3.2** 「栏目以及函数」除「利润 = 收入 − 成本」、以及可能的「利润率 = 利润/收入」外,是否还有其它公式或统计维度(如按商品、按公司汇总)?若甲方尚未提供,是否先只做「收入、成本、利润、利润率」四栏?
|
||||
|
||||
**3.3** 利润明细的数据来源:**(a)** 完全由后端根据合同、货代/杂费/服务费自动汇总计算,前端只读展示;**(b)** 支持在前端表格中手动改写部分单元格(覆盖自动结果);**(c)** 以手动录入为主,仅部分字段可从其他表带出。请选一种或说明组合方式。
|
||||
|
||||
---
|
||||
|
||||
## 四、货代费用 + 杂费(与菜单结构)
|
||||
|
||||
**4.1** 需求表里写「4.货代费用+其他杂费」合并为一条二级菜单,但当前前端是「货代费用」「杂费管理」两个并列子菜单。
|
||||
是否保持现有两个子菜单(货代费用、杂费管理)不变?还是希望合并为一个「货代费用与杂费」页面,用 Tab 或筛选区分「货代」与「杂费」?
|
||||
|
||||
**4.2** 货代费用、杂费、服务费三者是否都需要「支持上传附件」(如发票扫描件)?当前货代/杂费/服务费模型是否需增加 `file_path` 或类似字段?
|
||||
|
||||
---
|
||||
|
||||
## 五、对账单
|
||||
|
||||
**5.1** 需求仅写「5.对账单」,无具体说明。当前后端有 `ZSPReconciliation` 模型(对账单号、合同编号、公司名、账期起止、总金额、已付、未付、状态、附件路径等)。
|
||||
请确认对账单的业务含义:**(a)** 与「结算单」一一对应或一对多(如多笔结算汇总成一张对账单)?**(b)** 与结算单无关,是独立与客户/供应商的阶段性对账?**(c)** 其他(请简要说明)。
|
||||
|
||||
**5.2** 对账单的「总金额 / 已付 / 未付」是否由系统根据结算单或付款记录自动汇总?还是完全手动录入?
|
||||
|
||||
**5.3** 对账单是否也需要「上传已确认的对账单扫描件」作为备份?若需要,是否与结算单一样按「记录 + 附件路径」即可?
|
||||
|
||||
---
|
||||
|
||||
## 六、接口与路由约定
|
||||
|
||||
**6.1** 结算、发票、对账单的后端 API 是否统一放在现有 `/api/zsp-finances/` 下,例如:
|
||||
- 结算:`/api/zsp-finances/settlements`(CRUD)
|
||||
- 上游/下游/货代杂费发票:`/api/zsp-finances/upstream-invoices`、`downstream-invoices`、`freight-misc-invoices`
|
||||
- 对账单:`/api/zsp-finances/reconciliations`
|
||||
是否同意?若有不同路径规范请说明。
|
||||
|
||||
**6.2** 财务相关列表接口是否需要分页、按结算单号/合同号/公司名/日期范围等筛选?若需要,请列出各模块的常用筛选条件。
|
||||
|
||||
---
|
||||
|
||||
## 七、其他
|
||||
|
||||
**7.1** 财务管理各子模块(结算、发票、利润、货代、杂费、服务费、对账单)的菜单顺序是否与当前前端路由一致(结算 → 发票 → 利润明细 → 货代费用 → 杂费管理 → 服务费管理 → 对账单)?如需调整请说明。
|
||||
|
||||
**7.2** 是否有「财务数据不可删除、仅可作废/红冲」等合规要求?若有,哪些表需要支持状态为「作废」而不是物理删除?
|
||||
|
||||
---
|
||||
|
||||
*回答方式:可在每题后直接写答案,或另起文档写「题号 + 答案」。完成后告知,再开始补充前后端实现。*
|
||||
299
docs/dev/项目开发文档.md
Executable file
299
docs/dev/项目开发文档.md
Executable file
@@ -0,0 +1,299 @@
|
||||
# 项目开发文档
|
||||
|
||||
```
|
||||
作者:rovina
|
||||
最近修订日期:2026/3/2
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 一、项目技术栈概览
|
||||
|
||||
|
||||
| 层级 | 技术 | 说明 |
|
||||
| --- | --------------------------------- | --------------------- |
|
||||
| 前端 | Vue 3 + TypeScript + Element Plus | 基于 pure-admin-thin 模板 |
|
||||
| 后端 | Go + Gin + GORM | RESTful API,MySQL 存储 |
|
||||
| 数据库 | MySQL 8 (Docker) | 数据库名 `myapp_dev` |
|
||||
|
||||
|
||||
---
|
||||
|
||||
## 二、后端已实现的路由与模型
|
||||
|
||||
### 2.1 已有后端路由(`route.go`)
|
||||
|
||||
```
|
||||
# 认证
|
||||
POST /api/auth/register
|
||||
POST /api/auth/login
|
||||
GET /api/users/:id
|
||||
|
||||
# 合同上传(旧版,Contract 模型)
|
||||
POST /api/contract/upload
|
||||
POST /api/contract/batch-upload
|
||||
|
||||
# 中尚鹏合同管理(ZSPContract 模型)
|
||||
POST /api/zsp-contract/folders
|
||||
GET /api/zsp-contract/folders
|
||||
GET /api/zsp-contract/folders/:id
|
||||
PUT /api/zsp-contract/folders/:id
|
||||
DELETE /api/zsp-contract/folders/:id
|
||||
POST /api/zsp-contract/contracts (上传合同文件)
|
||||
GET /api/zsp-contract/contracts (含详情列表)
|
||||
GET /api/zsp-contract/contracts/:id (含详情)
|
||||
DELETE /api/zsp-contract/contracts/:id
|
||||
POST /api/zsp-contract/contracts/:id/details
|
||||
GET /api/zsp-contract/contracts/:id/details
|
||||
POST /api/zsp-contract/details/batch
|
||||
GET /api/zsp-contract/details/:id
|
||||
PUT /api/zsp-contract/details/:id
|
||||
DELETE /api/zsp-contract/details/:id
|
||||
POST /api/zsp-contract/contracts/:id/details/import (Excel导入)
|
||||
GET /api/zsp-contract/contracts/:id/details/export (Excel导出)
|
||||
|
||||
# 财务管理(ZSPFinances 模型)
|
||||
CRUD /api/zsp-finances/freight-charges (货代费用)
|
||||
CRUD /api/zsp-finances/misc-charges (杂费)
|
||||
CRUD /api/zsp-finances/service-fees (服务费)
|
||||
CRUD /api/zsp-finances/cost-breakdowns (成本构成)
|
||||
CRUD /api/zsp-finances/profit-records (利润记录)
|
||||
```
|
||||
|
||||
### 2.2 已有后端数据模型
|
||||
|
||||
|
||||
| 模型 | 说明 | AutoMigrate |
|
||||
| ------------------- | -------------------------- | --------------------- |
|
||||
| `User` | 用户表 | 是 |
|
||||
| `Contract` | 合同(旧版,仅名称/编号/甲乙方等基础字段) | 是 |
|
||||
| `ContractFile` | 合同附件(旧版) | 是 |
|
||||
| `ZSPContractFolder` | 合同文件夹 | 是 |
|
||||
| `ZSPContractFile` | 合同文件(含合同编号/公司/商品/签订日期/类型) | 是 |
|
||||
| `ZSPContractDetail` | 合同详情(商品/数量/单价/已提货/应提货/提单号) | **否,未加入 AutoMigrate** |
|
||||
| `ZSPFreightCharges` | 货代费用 | **否,未加入 AutoMigrate** |
|
||||
| `ZSPMiscCharges` | 杂费 | **否,未加入 AutoMigrate** |
|
||||
| `ZSPServiceFees` | 服务费 | **否,未加入 AutoMigrate** |
|
||||
| `ZSPCostBreakdown` | 成本构成(关联货代/杂费/服务费) | **否,未加入 AutoMigrate** |
|
||||
| `ZSPProfitRecord` | 利润记录(关联成本构成) | **否,未加入 AutoMigrate** |
|
||||
|
||||
|
||||
### 2.3 后端完全缺失的模块(前端有 API 定义但后端无路由/模型)
|
||||
|
||||
|
||||
| 前端 API 文件 | 调用路径前缀 | 后端状态 |
|
||||
| ---------------------- | ----------------------- | ----------------------------------- |
|
||||
| `applyDelivery.ts` | `/apply-delivery/`* | 无路由、无模型、无 handler |
|
||||
| `deliveryDetails.ts` | `/delivery-details/`* | 无路由、无模型、无 handler |
|
||||
| `inventory.ts` | `/inventory/*` | 无路由、无模型、无 handler |
|
||||
| `ownershipTrAns1fer.ts` | `/ownership-trAns1fer/*` | 无路由、无模型、无 handler(且前端大部分 API 函数被注释) |
|
||||
| `company.ts` | `/company/*` | 无路由、无模型、无 handler |
|
||||
| 结算管理 | 未知 | 无前端 API 文件、无后端 |
|
||||
| 发票管理 | 未知 | 无前端 API 文件、无后端 |
|
||||
| 采购管理 | 未知 | 无前端 API 文件、无后端 |
|
||||
| 查询报表 | 复用 zspContract API | 无独立后端 |
|
||||
|
||||
|
||||
---
|
||||
|
||||
## 三、已发现的技术问题
|
||||
|
||||
### 3.1 AutoMigrate 不完整
|
||||
|
||||
`pkg/database/migrate.go` 只注册了 5 个模型,缺少 `ZSPContractDetail` 及所有财务相关模型(共 6 个),后端启动后这些表不会自动创建。
|
||||
|
||||
### 3.2 双套合同模型
|
||||
|
||||
后端同时存在 `Contract`(旧版)和 `ZSPContractFile`(新版)两套合同模型:
|
||||
|
||||
- `Contract` 字段:Name/ContractType/ContractCode/BuyParty/SellParty/Count/UnitPrice/BLNumber 等,且 Count、UnitPrice 均为 string 类型。
|
||||
- `ZSPContractFile` + `ZSPContractDetail`:更完整,有数值类型的 TotalQuantity/UnitPrice/DeliveredQty/PendingQty。
|
||||
|
||||
前端 `AddContract.vue` 调用旧版 `contract/upload`;中尚鹏合同管理、合同执行等调用新版 `zsp-contract/*`。
|
||||
|
||||
### 3.3 Auth 中间件已注释
|
||||
|
||||
`route.go` 中 `authorized.Use(authMiddleware)` 被注释,所有接口无鉴权保护。
|
||||
|
||||
### 3.4 API 基址不统一
|
||||
|
||||
- `contract.ts`:用 `baseUrlApi()` 写死 `http://127.0.0.1:8080/api/`,不走 Vite 代理。
|
||||
- 其余 API:用 `@/utils/http`,走 Vite 代理 `/api` -> `127.0.0.1:8080`。
|
||||
|
||||
### 3.5 财务模型字段与需求不匹配
|
||||
|
||||
需求中"货代费用"要求字段为:**提单号、上游合同编号、费用项目、金额、付款日期、货代/物流公司名**。
|
||||
|
||||
当前后端 `ZSPFreightCharges` 模型仅有:Name、Amount、Currency、Date、Remark,**缺少提单号、上游合同编号、货代公司名等关键业务字段**。
|
||||
|
||||
---
|
||||
|
||||
## 四、需要你确认的问题
|
||||
|
||||
> 以下问题分为 **业务理解** 和 **技术决策** 两类,请逐条回答或标注"暂不考虑"。
|
||||
|
||||
### A. 合同管理
|
||||
|
||||
**A1.** 旧版 `Contract` 模型和新版 `ZSPContractFile + ZSPContractDetail` 模型都在使用中。你的计划是:
|
||||
|
||||
- (a) 废弃旧版,把"新增合同"页面也改用新版模型?
|
||||
- (b) 两套并存,各管各的?
|
||||
- (c) 其他方案?
|
||||
|
||||
**Ans1: b新增合同部分要注意区分中尚鹏合同管理和新增合同是两套并行的合同管理,在业务需求端新增合同其实应该是合同管理或者叫做业务合同管理,建议改掉,然后中尚鹏合同管理只用负责公司相关的合同就行**
|
||||
|
||||
**A2.** 旧版 `Contract` 的 `Count`(数量)和 `UnitPrice`(单价)字段是 `string` 类型,无法直接参与数值计算。如果保留旧版,是否需要改成数值类型?
|
||||
**Ans1: 我害怕有浮点计算风险,如果你能解决这个问题也未尝不可。**
|
||||
|
||||
**A3.** Talk 中提到"合同管理里空一个 Excel 表格,手动录入合同数据(数量、单价、已提货数量等),系统自动计算未提货数量"。这个"Excel 表格"是否就是当前 `ZSPContractDetail`(合同详情)表的逐行录入形式?还是甲方期望一个**真正可编辑的类 Excel 界面**(如可拖拽选区、复制粘贴公式等 SpreadSheet 组件)?
|
||||
**Ans1: 甲方可能希望的是一个表的逐行录入形式类似csv格式的前端显示,但是有并发修改的需求**
|
||||
|
||||
**A4.** 甲方说"公式后续会给出"。目前后端 `ZSPContractDetail.BeforeCreate` 只做了一个公式:`PendingQty = TotalQuantity - DeliveredQty`。除此之外是否还有其他公式需要实现?如果甲方还没给,是否先只保留这一个?
|
||||
**Ans1: 甲方还没给**
|
||||
|
||||
**A5.** 中尚鹏合同管理的搜索:甲方说"我们会备注好文件名包含商品、公司名、合同编号"。当前后端 `ZSPContractFile` 模型已有独立的 `ContractNumber`、`CompanyName`、`Commodity` 字段。那么搜索逻辑是:
|
||||
|
||||
- (a) 上传时手动填写这三个字段,搜索时按字段查?(当前实现)
|
||||
- (b) 从文件名自动解析出这三个字段?如果是,文件名格式是什么?
|
||||
**Ans1: (a)方法**
|
||||
|
||||
|
||||
### B. 提货管理
|
||||
|
||||
**B1.** 提货管理(我要提货、提货明细、库存、货权转移)的后端完全没有实现。前端已定义了 API 类型和调用路径。开发顺序是否按 `我要提货 → 提货明细 → 库存 → 货权转移` 依次实现?
|
||||
**Ans1: 是的**
|
||||
|
||||
**B2.** "我要提货"需求中说"根据我们提供的提货单模板创建"。模板是否已提供?如果提供了,请告诉我文件位置;如果没有,是否按当前前端 `ApplyDelivery.vue` 的字段结构先做?
|
||||
**Ans1: 目前还没提供,这个部分先不做**
|
||||
|
||||
**B3.** 库存计算公式:`库存 = 入库重 − 出库数量 − 货权转移数量`。这个计算在后端做(API 返回已算好的库存值),还是前端拿到三个数字自己算?
|
||||
**Ans1: 这个后端来算吧**
|
||||
|
||||
**B4.** 货权转移前端 `ownershipTrAns1fer.ts` 中大部分 API 函数被注释了(create、print、export、delete、upload),只保留了 `getOwnershipList`。这些注释掉的功能是否都要实现?
|
||||
**Ans1: 可能是Ai生成的内容,可信度不高,你按照自己的想法来**
|
||||
|
||||
**B5.** 需求中"货权转移"要求字段包含**库方**(warehouse)。这里的"库方"是否就是"仓库管理"中已维护的仓库列表?即两者共用同一套仓库数据?
|
||||
**Ans1: 公司是一个对接上游公司和下游公司的业务,从上游公司拿货给下游公司这样的,库方一个指的是仓库管理中的仓库列表**
|
||||
|
||||
### C. 财务管理
|
||||
|
||||
**C1.** 当前后端的 `ZSPFreightCharges`(货代费用)模型字段很少(仅 name/amount/currency/date/remark),需求要求的"提单号、上游合同编号、费用项目、金额、付款日期、货代/物流公司名"基本都缺。是否需要重新设计这个模型的字段?
|
||||
**Ans1: 是的,重新设计**
|
||||
|
||||
**C2.** Talk 中提到"利润表中的成本栏 = 货代费用 + 杂费 + 服务费,且点击成本金额能显示构成明细(如货代费 20 万、杂费 12 万)"。当前后端已有 `ZSPCostBreakdown` 模型关联了三种费用。这个设计方向是否正确?是否需要调整?
|
||||
**Ans1: 是的,重新设计**
|
||||
|
||||
**C3.** 利润明细需要"关联其他表格中数据"。Talk 中提到的"其他表格",你认为具体是指哪些?我目前的理解是:
|
||||
|
||||
- 收入 ← 来自合同金额(ZSPContractDetail.TotalQuantity × UnitPrice)
|
||||
- 成本 ← 来自货代费用 + 杂费 + 服务费(ZSPCostBreakdown)
|
||||
- 利润 = 收入 − 成本
|
||||
- 这个理解对吗?还是收入来源不同(比如来自结算单)?
|
||||
**Ans1: 应该是对的**
|
||||
|
||||
**C4.** 结算管理、发票管理、对账单:前端目前没有独立的 API 文件,后端也没有实现。这三个模块的优先级如何?是否排在合同和提货之后?
|
||||
**Ans1: 需要你来实现**
|
||||
|
||||
|
||||
**C5.** 发票管理中"上游/下游/货代杂费发票",这三类发票是否共用一个数据表(通过 type 字段区分),还是需要独立三张表?
|
||||
**Ans1: 独立三张表**
|
||||
|
||||
|
||||
### D. 查询 + 报表中心
|
||||
|
||||
**D1.** 合同执行情况中"差款"的计算规则是什么?是 `合同金额 − 已结算金额`?还是 `合同金额 − 银行流水中已收金额`?数据分别来自哪些表?
|
||||
**Ans1: 先不做这一部分**
|
||||
|
||||
**D2.** "待开发票情况"的计算规则是什么?是 `已结算金额 − 已开票金额`?
|
||||
**Ans1: 是**
|
||||
|
||||
**D3.** "上传银行流水":上传后是否需要**解析 Excel 内容并自动匹配到合同**?还是仅当附件保存,人工查看?如果要解析,Excel 的列格式是什么?
|
||||
**Ans1: 仅当附件保存,人工查看**
|
||||
|
||||
**D4.** "其它明细表"下有三个子表:已开票明细表、货代杂费明细表、历史清关成本明细表。当前前端只有一个 `OtherDetails.vue`。是在一个页面用 Tab 切换三个子表,还是拆成三个独立页面/路由?
|
||||
**Ans1: 用 Tab 切换三个子表**
|
||||
|
||||
**D5.** "营业明细表"的具体内容和字段是什么?是按年/月/合同维度汇总的报表?还是某个甲方提供的 Excel 模板?
|
||||
**Ans1: 先不做这一部分**
|
||||
|
||||
|
||||
### E. 采购管理
|
||||
|
||||
**E1.** 采购管理的四个子模块(采购报单、采购运踪明细、国外供应商、采购预算)目前前端有路由和页面,但前端 API 只有 `PurchaseOrder.vue` 有部分内容,后端完全没有。这些模块的开发优先级是否排在合同/提货/财务之后?
|
||||
**Ans1: 这是之前用AI生成的前端内容,可信度并不高**
|
||||
|
||||
**E2.** 采购预算需求说"已有单独的预算测算系统"。对接方式你倾向哪种:链接跳转、iframe 嵌入、还是接口拉数据?还是说现阶段完全不做?
|
||||
**Ans1: 完全不做**
|
||||
|
||||
### F. 权限管理
|
||||
|
||||
**F1.** 需求中"权限管理 1、2"没有展开。当前系统是否只需要最基础的**角色 + 菜单权限**(如管理员看全部、普通用户看部分菜单)?还是需要更复杂的**数据权限**(如只能看自己创建的合同)或**审批流**?
|
||||
**Ans1: 只需要最基础的**
|
||||
|
||||
**F2.** 当前后端 Auth 中间件已注释。是否需要在这一阶段启用 JWT 鉴权?还是等功能都做完再加?
|
||||
**Ans1: 等所有的内容做完了再加吧**
|
||||
|
||||
### G. 整体开发计划
|
||||
|
||||
**G1.** 你期望的模块开发顺序是什么?我建议按依赖关系排序:
|
||||
|
||||
1. 合同管理(基础数据,其他模块依赖合同编号/提单号)
|
||||
2. 提货管理(依赖合同)
|
||||
3. 库存信息(依赖入库/出库/货权转移)
|
||||
4. 财务管理(依赖合同、提货数据)
|
||||
5. 查询报表(依赖以上所有数据)
|
||||
6. 采购管理(相对独立)
|
||||
7. 权限管理(最后统一加)
|
||||
|
||||
这个顺序你是否同意?或者有其他优先级安排?
|
||||
**Ans1: 同意**
|
||||
|
||||
**G2.** 甲方 Talk 中说"主菜单后面不能再改,二级菜单每次做之前再沟通一次"。是否意味着我每做完一个二级菜单就要等你确认后再做下一个?
|
||||
**Ans1: 不需要**
|
||||
|
||||
**G3.** 前端页面大部分已经有了 UI 和模拟逻辑。后续开发的重心是否主要在**后端 API 实现 + 前后端联调**,前端 UI 改动较少?
|
||||
**Ans1: 前端UI的改动,现在我对新增合同这个菜单并不满意,建议修改为业务合同管理,然后前端UI做的和中尚鹏合同管理一样这些**
|
||||
|
||||
---
|
||||
|
||||
## 五、Ans 修改完成备注
|
||||
|
||||
以下为根据你的回复已完成的修改及代码位置说明。
|
||||
|
||||
**Solve A1(两套合同并存 + 业务合同管理命名)**
|
||||
- 完成:菜单「新增合同」改为「业务合同管理」;业务合同管理仅负责业务合同,中尚鹏合同管理负责公司相关合同。
|
||||
- 参考位置:`frontend/pure-admin-thin/src/router/modules/contract.ts` 中 `meta.title` 改为「业务合同管理」;`frontend/pure-admin-thin/src/views/system/contract/AddContract.vue` 页面标题与描述改为「业务合同管理」「管理业务合同信息、上传合同相关文件、录入合同数据」。
|
||||
|
||||
**Solve A2(Contract 数量/单价改为数值并避免浮点风险)**
|
||||
- 完成:旧版 `Contract` 的 `Count`、`UnitPrice` 改为 `decimal(16,4)` 数值类型,后端解析表单字符串为 float64。
|
||||
- 参考位置:`backend/go-backend/internal/model/contract.go` 中 `Count`、`UnitPrice` 定义为 `float64` 且 `gorm:"type:decimal(16,4)"`;`backend/go-backend/internal/handler/contract_handler.go` 中 `strconv.ParseFloat` 解析并赋值。
|
||||
|
||||
**Solve A3、A4、A5**
|
||||
- 当前保留:合同详情逐行录入形式(ZSPContractDetail);公式仅保留 `PendingQty = TotalQuantity - DeliveredQty`;搜索按手动填写的合同编号/公司名/商品字段查询。无新增代码改动,逻辑维持现状。
|
||||
|
||||
**Solve C1(货代费用模型重新设计)**
|
||||
- 完成:`ZSPFreightCharges` 增加提单号、上游合同编号、费用项目、金额、付款日期、货代/物流公司名等字段。
|
||||
- 参考位置:`backend/go-backend/internal/model/zsp_freight_charges.go` 中 `ZSPFreightCharges` 结构体;`internal/dto/zsp_finances.go` 中 Create/Update 请求体;`internal/service/zsp_finances_service.go`、`internal/repository/zsp_finances.go`、`internal/handler/zsp_finances_handler.go` 对应 CRUD;前端 `frontend/pure-admin-thin/src/api/zspFinances.ts` 类型与接口;`frontend/pure-admin-thin/src/views/system/finance/FreightCharges.vue` 表格与表单字段。
|
||||
|
||||
**Solve C2(成本构成重新设计)**
|
||||
- 完成:`ZSPCostBreakdown` 改为按金额汇总设计,包含 `FreightChargeTotal`、`MiscChargeTotal`、`ServiceFeeTotal` 及 `TotalCost`,不再用外键关联单条费用记录。
|
||||
- 参考位置:`backend/go-backend/internal/model/zsp_freight_charges.go` 中 `ZSPCostBreakdown`;`internal/dto/zsp_finances.go` 中成本构成 DTO;service/repository 中成本构成的计算与更新逻辑;前端 `zspFinances.ts` 中 `ZSPCostBreakdown` 及相关 API。
|
||||
|
||||
**Solve C3**
|
||||
- 理解已确认:收入来自合同金额,成本=货代+杂费+服务费,利润=收入−成本。当前后端利润记录与成本构成已按此逻辑实现,无需额外改动。
|
||||
|
||||
**Solve C4(结算/发票/对账单需实现)**
|
||||
- 完成:后端新增结算、对账单及三张发票表模型并加入迁移;前端新增对应 API 文件。后端路由与完整 CRUD 待后续补全。
|
||||
- 参考位置:`backend/go-backend/internal/model/zsp_invoice.go`(上游/下游/货代杂费发票);`backend/go-backend/internal/model/zsp_settlement.go`(结算、对账单);`backend/go-backend/pkg/database/migrate.go` 中 AutoMigrate 新增上述模型;`frontend/pure-admin-thin/src/api/settlement.ts`、`invoice.ts`、`reconciliation.ts`。
|
||||
|
||||
**Solve C5(发票独立三张表)**
|
||||
- 完成:已建 `ZSPUpstreamInvoice`、`ZSPDownstreamInvoice`、`ZSPFreightMiscInvoice` 三张独立表及对应模型与迁移。
|
||||
- 参考位置:`backend/go-backend/internal/model/zsp_invoice.go`。
|
||||
|
||||
**Solve 其他(杂费/服务费字段、API 基址、AutoMigrate)**
|
||||
- 杂费:`ZSPMiscCharges` 增加提单号、合同编号、费用项目、付款日期、公司名等,见 `zsp_freight_charges.go`、DTO、`MiscCharges.vue`。
|
||||
- 服务费:`ZSPServiceFees` 增加提单号、合同编号,见 `zsp_freight_charges.go`、DTO、`ServiceFees.vue`。
|
||||
- API 基址统一:业务合同上传改用 `@/utils/http`,走 Vite 代理。参考 `frontend/pure-admin-thin/src/api/contract.ts`(`submitContract`、`batchUploadContracts` 使用 `http.request`)。
|
||||
- AutoMigrate 补全:`ZSPContractDetail`、所有财务相关模型、三张发票表、结算、对账单已加入。参考 `backend/go-backend/pkg/database/migrate.go`。
|
||||
|
||||
*以上为当前已完成的修改与对应代码位置;提货/库存/货权转移、结算与发票的后端路由及利润明细页与新成本结构的联调等,按开发计划后续迭代。*
|
||||
28
docs/dev/项目开发要求文档.md
Executable file
28
docs/dev/项目开发要求文档.md
Executable file
@@ -0,0 +1,28 @@
|
||||
| 中尚鹏管理系统 | 主菜单 | 二级菜单 | 三级菜单 | 内容 | 需要实现的功能 |
|
||||
| -------------- | ------------- | ---------------------------- | ------------------------------------------------------------ | ------------------------------------------------------------ | ------------------------------------------------------------ |
|
||||
| | 合同管理 | 1.新增合同 | ①新增合同信息②上传合同相关文件 | 合同类型,合同编号,商品,数量,单价,卖方,买方,提单号<一个上游合同可关联的提单号最多15个>(体现在一个表格中);上传合同扫描附件(上传扫描件的文件夹,可以后续新建多个文件夹作为合同归类)。(非必选:提货时间·交货时间·包装(散装/吨袋)·备注) | 可批量上传合同+有一个录入合同内容中相关数据的表格(会涉及简单的公式,我们会给出) |
|
||||
| | 合同管理 | 2.中尚鹏合同管理 | ①上游合同②下游合同③外商采购合同④货代物流协议⑤其它合同 | 上传合同扫描附件(上传扫描件的文件夹,可以后续新建多个文件夹作为合同归类);提供按照签订时间/合同编号/公司名 的搜索功能(我们会备注好文件名包含商品、公司名、合同编号,我们就通过三个要素搜索对应的合同) | |
|
||||
| | 合同管理 | 3.公司信息管理 | 主要是公司相关文件存储,营业执照开票信息等 | 上传合同扫描附件(上传扫描件的文件夹,可以后续新建多个文件夹作为合同归类) | |
|
||||
| | 提货管理 | 1.我要提货 | ①新增提货委托②变更提货委托③取消提货委托④ | 根据我们提供提货单模板创建在系统内,需要新增提货单时,我们手动录入可变信息内容(车牌号、提单号、司机信息、数量、商品品类) | |
|
||||
| | 提货管理 | 2.提货明细 | ①新增出库单②查看出库明细③收货确认单 | 录入数据的表格(后期我们给模板) | |
|
||||
| | 提货管理 | 3.库存信息 | ①新增入库单(入库单上传+录入表格)②库存明细(入库表格中数量-新增出库单中数量-货权转移数量=库存数量)③仓库管理(需要录入几个目前固定存放货物的仓库名,且后续可能会增加新的仓库名) | 批量上传扫描件的文件夹;录入扫描件内容数据的表格。表格内容:入库单:仓库,商品,提单号,提单重,入库重,货主,对应上游合同编号。可选项(仓库地址,包装) | |
|
||||
| | 提货管理 | 4.货权转移 | ①新增货权转移函②上传货权转移函(已生效的)③查询/导出货转函 | 批量上传扫描件的文件夹;录入货权转移函数据的表格内容:货转日期,提单号,数量,商品,库方,货权出让方,货权接收方等 | |
|
||||
| | 提货管理 | 5.其它 | | | |
|
||||
| | 财务管理 | 1.结算管理 | ①新增结算单(导出下游合同结算单)②结算单根据单号查询③上传结算单(已交易且结算完毕的,主要为了备份) | 批量上传扫描件的文件夹;录入扫描件内容数据的表格 | |
|
||||
| | 财务管理 | 2.发票管理 | ①上游发票(上传)②下游发票(上传)③货代杂费发票(上传) | 上传文件;对应表格录入发票对应信息 | |
|
||||
| | 财务管理 | 3.利润明细 | ①2025年业务利润表 | 根据我们提供的excel表格创建一个表格(需要关联其他表格中数据+设置好栏目以及函数,后续自行手动录入) | |
|
||||
| | 财务管理 | 4.货代费用+其他杂费 | ①新增货代费用/其它费用 | 货代费用/其它费用:提单号,上游合同编号,费用项目,金额,付款日期,货代/物流公司名 | |
|
||||
| | 财务管理 | 5.对账单 | | | |
|
||||
| | 查询+报表中心 | 1.查询上下游客户合同执行情况 | ①查询上下游客户合同执行(可按照合同编号/公司名/商品/提单号/日期 查询)②上传excel表格(银行流水) | 查询上下游客户货款及提货进度 | 可查询到该合同剩余未提数量(与二级菜单中新增合同表格关联)及差款及待开发票情况(与二级菜单中发票管理关联) |
|
||||
| | 查询+报表中心 | 2.营业明细表 | ①2025年中尚鹏业务明细表 | | |
|
||||
| | 查询+报表中心 | 3.其它明细表 | ①已开票明细表②货代杂费明细表③历史清关成本明细表 | | |
|
||||
| | 查询+报表中心 | 4.商品基础资料 | 燕麦/大麦/豌豆/亚麻籽/葵粕/菜籽粕 | 可批量上传扫描件or文档即可 | |
|
||||
| | 查询+报表中心 | 5.其它 | | | |
|
||||
| | 采购管理 | 1.采购报单 | ①新增采购报单②变更采购报单 | 批量上传扫描件+录入扫描件中内容的表格 | |
|
||||
| | 采购管理 | 2.采购运踪明细 | 采购进度明细表(相当于一个共享表格) | 根据我们所提供的excel在系统中创建一个表格 | |
|
||||
| | 采购管理 | 3.国外供应商 | 国外供应商明细表 | 可批量上传扫描件or文档即可 | |
|
||||
| | 采购管理 | 4.采购预算 | | 已有单独的预算测算系统,最后按照该预算系统功能等添加到我们系统中(这个最后有时间慢慢弄,不着急) | |
|
||||
| | 权限管理 | 1 | | | |
|
||||
| | 权限管理 | 2 | | | |
|
||||
| | | | | | |
|
||||
|
||||
1314
docs/superpowers/plans/2026-04-25-frontend-api-test-infra.md
Normal file
1314
docs/superpowers/plans/2026-04-25-frontend-api-test-infra.md
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,274 @@
|
||||
---
|
||||
name: 前端 API 测试基础设施
|
||||
description: 为 Vue3+TypeScript 前端建立 Vitest 测试框架,Mock Axios 测试 11 个 API 模块
|
||||
created: 2026-04-25
|
||||
---
|
||||
|
||||
# 前端 API 测试基础设施设计文档
|
||||
|
||||
## 一、背景与目标
|
||||
|
||||
### 当前状态
|
||||
|
||||
- 前端基于 Vue 3 + TypeScript + Vite + Axios
|
||||
- 11 个 API 模块 (`src/api/*.ts`) 封装 HTTP 请求
|
||||
- **测试完全缺失**:无 vitest 配置,无 .spec/.test 文件
|
||||
|
||||
### 目标
|
||||
|
||||
建立前端 API 层测试基础设施,确保:
|
||||
1. API 请求参数/响应类型正确
|
||||
2. 错误处理逻辑覆盖
|
||||
3. 前后端契约一致性验证
|
||||
4. CI/CD 集成准备
|
||||
|
||||
## 二、技术选型
|
||||
|
||||
| 项目 | 选择 | 原因 |
|
||||
|------|------|------|
|
||||
| 测试框架 | Vitest | Vite 生态原生支持,配置简单,ESM 原生 |
|
||||
| Mock 工具 | vi.mock (Vitest 内置) | 无额外依赖,Axios mock 简洁 |
|
||||
| 断言库 | Vitest expect | 内置,支持 TypeScript 类型推断 |
|
||||
| 覆盖率 | @vitest/coverage-v8 | Istanbul 覆盖率报告 |
|
||||
|
||||
## 三、架构设计
|
||||
|
||||
### 目录结构
|
||||
|
||||
```
|
||||
frontend/
|
||||
├── vitest.config.ts # Vitest 配置文件
|
||||
├── src/api/
|
||||
│ ├── __tests__/ # API 测试目录
|
||||
│ │ ├── contract.spec.ts # 合同 API 测试
|
||||
│ │ ├── zspContract.spec.ts # 中尚鹏合同
|
||||
│ │ ├── zspFinances.spec.ts # 财务模块
|
||||
│ │ ├── applyDelivery.spec.ts # 提货申请
|
||||
│ │ ├── deliveryDetails.spec.ts
|
||||
│ │ ├── inventory.spec.ts
|
||||
│ │ ├── ownershipTransfer.spec.ts
|
||||
│ │ ├── company.spec.ts
|
||||
│ │ ├── user.spec.ts
|
||||
│ │ ├── invoice.spec.ts
|
||||
│ │ ├── settlement.spec.ts
|
||||
│ │ └── reconciliation.spec.ts
|
||||
│ └── test-utils.ts # Mock 工具函数
|
||||
└── package.json # 新增 vitest 相关依赖
|
||||
```
|
||||
|
||||
### Vitest 配置
|
||||
|
||||
```typescript
|
||||
// vitest.config.ts
|
||||
import { defineConfig } from 'vitest/config'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import path from 'path'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [vue()],
|
||||
test: {
|
||||
environment: 'jsdom',
|
||||
include: ['src/api/__tests__/**/*.spec.ts'],
|
||||
coverage: {
|
||||
provider: 'v8',
|
||||
reporter: ['text', 'html'],
|
||||
include: ['src/api/**/*.ts'],
|
||||
exclude: ['src/api/__tests__/**', 'src/api/test-utils.ts']
|
||||
}
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': path.resolve(__dirname, './src')
|
||||
}
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
### Mock 工具函数
|
||||
|
||||
```typescript
|
||||
// src/api/test-utils.ts
|
||||
import { vi } from 'vitest'
|
||||
import { http } from '@/utils/http'
|
||||
|
||||
/**
|
||||
* Mock http.request 方法
|
||||
* @param response 模拟响应数据
|
||||
* @param status HTTP 状态码 (默认 200)
|
||||
*/
|
||||
export function mockHttpRequest(response: any, status = 200) {
|
||||
vi.spyOn(http, 'request').mockResolvedValue({
|
||||
success: true,
|
||||
data: response,
|
||||
status
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Mock HTTP 错误响应
|
||||
* @param message 错误信息
|
||||
* @param status HTTP 状态码
|
||||
*/
|
||||
export function mockHttpError(message: string, status: number) {
|
||||
vi.spyOn(http, 'request').mockRejectedValue({
|
||||
success: false,
|
||||
message,
|
||||
status
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除所有 mock
|
||||
*/
|
||||
export function clearMocks() {
|
||||
vi.restoreAllMocks()
|
||||
}
|
||||
```
|
||||
|
||||
## 四、测试模式
|
||||
|
||||
每个 API 模块测试遵循统一模式:
|
||||
|
||||
### 4.1 请求成功测试
|
||||
|
||||
```typescript
|
||||
describe('getContractList', () => {
|
||||
it('应返回合同列表', async () => {
|
||||
const mockData = [{ id: 1, name: '合同1' }]
|
||||
mockHttpRequest(mockData)
|
||||
|
||||
const result = await getContractList()
|
||||
expect(result.success).toBe(true)
|
||||
expect(result.data).toEqual(mockData)
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
### 4.2 参数验证测试
|
||||
|
||||
```typescript
|
||||
it('带 contractType 参数应正确传递', async () => {
|
||||
mockHttpRequest([])
|
||||
|
||||
await getContractList({ contractType: 'business' })
|
||||
expect(http.request).toHaveBeenCalledWith('get', '/api/contract', {
|
||||
params: { contractType: 'business' }
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
### 4.3 错误处理测试
|
||||
|
||||
```typescript
|
||||
describe('错误处理', () => {
|
||||
it('401 未授权应抛出错误', async () => {
|
||||
mockHttpError('Unauthorized', 401)
|
||||
|
||||
await expect(getContractList()).rejects.toMatchObject({
|
||||
status: 401
|
||||
})
|
||||
})
|
||||
|
||||
it('500 服务器错误应抛出错误', async () => {
|
||||
mockHttpError('Internal Server Error', 500)
|
||||
|
||||
await expect(getContractList()).rejects.toMatchObject({
|
||||
status: 500
|
||||
})
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
### 4.4 类型验证
|
||||
|
||||
```typescript
|
||||
it('响应应符合 BusinessContract 类型', async () => {
|
||||
const mockData: BusinessContract[] = [
|
||||
{
|
||||
id: 1,
|
||||
name: '测试合同',
|
||||
contractType: 'business',
|
||||
contractCode: 'C001',
|
||||
// ... 其他必填字段
|
||||
}
|
||||
]
|
||||
mockHttpRequest(mockData)
|
||||
|
||||
const result = await getContractList()
|
||||
// TypeScript 编译时类型检查,运行时验证结构
|
||||
expect(result.data[0]).toHaveProperty('id')
|
||||
expect(result.data[0]).toHaveProperty('name')
|
||||
})
|
||||
```
|
||||
|
||||
## 五、API 模块测试清单
|
||||
|
||||
| 模块 | 文件 | 主要 API 函数 | 预估测试数 |
|
||||
|------|------|---------------|------------|
|
||||
| Contract | `contract.spec.ts` | getContractList, getContract, submitContract, batchUploadContracts, updateContract, deleteContract + folders/batches | 15-20 |
|
||||
| ZSP Contract | `zspContract.spec.ts` | getFolders, getContracts, uploadContract, createDetail, importExcel, exportExcel | 10-15 |
|
||||
| ZSP Finances | `zspFinances.spec.ts` | CRUD for freight/misc/service/cost/profit | 12-15 |
|
||||
| Apply Delivery | `applyDelivery.spec.ts` | list, get, create, update, cancel, delete | 10 |
|
||||
| Delivery Details | `deliveryDetails.spec.ts` | list, get, create, update, delete | 8 |
|
||||
| Inventory | `inventory.spec.ts` | warehouses, inbound, summary | 8 |
|
||||
| Ownership Transfer | `ownershipTransfer.spec.ts` | list, get, create, createWithFiles, delete | 8 |
|
||||
| Company | `company.spec.ts` | folders, files CRUD | 8 |
|
||||
| User | `user.spec.ts` | login, register, getUser, refreshToken | 8 |
|
||||
| Invoice | `invoice.spec.ts` | 上游/下游/货代发票 CRUD | 10 |
|
||||
| Settlement | `settlement.spec.ts` | CRUD | 6 |
|
||||
| Reconciliation | `reconciliation.spec.ts` | 对账单 CRUD | 6 |
|
||||
|
||||
**总计预估:90-110 个测试用例**
|
||||
|
||||
## 六、执行命令
|
||||
|
||||
```bash
|
||||
# 安装依赖
|
||||
pnpm add -D vitest @vitest/coverage-v8 jsdom
|
||||
|
||||
# 运行测试
|
||||
pnpm test
|
||||
|
||||
# 运行覆盖率
|
||||
pnpm test:coverage
|
||||
|
||||
# 监听模式
|
||||
pnpm test:watch
|
||||
```
|
||||
|
||||
### package.json 新增脚本
|
||||
|
||||
```json
|
||||
{
|
||||
"scripts": {
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest",
|
||||
"test:coverage": "vitest run --coverage"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 七、CI/CD 集成预留
|
||||
|
||||
后续可添加 GitHub Actions 或其他 CI 配置:
|
||||
|
||||
```yaml
|
||||
# .github/workflows/test.yml (预留)
|
||||
- name: Run frontend tests
|
||||
run: cd frontend && pnpm test:coverage
|
||||
```
|
||||
|
||||
## 八、验收标准
|
||||
|
||||
1. 所有 11 个 API 模块有对应 `.spec.ts` 文件
|
||||
2. 测试覆盖率 > 80%(API 模块代码行覆盖)
|
||||
3. 所有测试通过,无 TypeScript 编译错误
|
||||
4. `pnpm test` 命令可用
|
||||
5. 覆盖率报告生成 (`coverage/index.html`)
|
||||
|
||||
## 九、后续扩展方向
|
||||
|
||||
- 组件测试 (`src/components/`)
|
||||
- 页面集成测试 (`src/views/`)
|
||||
- E2E 测试 (Playwright)
|
||||
- 真实后端集成测试层
|
||||
4
frontend/.browserslistrc
Executable file
4
frontend/.browserslistrc
Executable file
@@ -0,0 +1,4 @@
|
||||
> 1%
|
||||
last 2 versions
|
||||
not dead
|
||||
not ie 11
|
||||
55
frontend/.dockerignore
Executable file
55
frontend/.dockerignore
Executable file
@@ -0,0 +1,55 @@
|
||||
# Include any files or directories that you don't want to be copied to your
|
||||
# container here (e.g., local build artifacts, temporary files, etc.).
|
||||
#
|
||||
# For more help, visit the .dockerignore file reference guide at
|
||||
# https://docs.docker.com/engine/reference/builder/#dockerignore-file
|
||||
|
||||
**/.DS_Store
|
||||
**/__pycache__
|
||||
**/.venv
|
||||
**/.classpath
|
||||
**/.dockerignore
|
||||
**/.env
|
||||
**/.git
|
||||
**/.gitignore
|
||||
**/.project
|
||||
**/.settings
|
||||
**/.toolstarget
|
||||
**/.vs
|
||||
**/.vscode
|
||||
**/*.*proj.user
|
||||
**/*.dbmdl
|
||||
**/*.jfm
|
||||
**/bin
|
||||
**/charts
|
||||
**/docker-compose*
|
||||
**/compose*
|
||||
**/Dockerfile*
|
||||
**/node_modules
|
||||
**/npm-debug.log
|
||||
**/obj
|
||||
**/secrets.dev.yaml
|
||||
**/values.dev.yaml
|
||||
LICENSE
|
||||
README.md
|
||||
node_modules
|
||||
.DS_Store
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
.eslintcache
|
||||
report.html
|
||||
|
||||
yarn.lock
|
||||
npm-debug.log*
|
||||
.pnpm-error.log*
|
||||
.pnpm-debug.log
|
||||
tests/**/coverage/
|
||||
|
||||
# Editor directories and files
|
||||
.idea
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
tsconfig.tsbuildinfo
|
||||
14
frontend/.editorconfig
Executable file
14
frontend/.editorconfig
Executable file
@@ -0,0 +1,14 @@
|
||||
# http://editorconfig.org
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[*.md]
|
||||
insert_final_newline = false
|
||||
trim_trailing_whitespace = false
|
||||
5
frontend/.env
Executable file
5
frontend/.env
Executable file
@@ -0,0 +1,5 @@
|
||||
# 平台本地运行端口号
|
||||
VITE_PORT = 12345
|
||||
|
||||
# 是否隐藏首页 隐藏 true 不隐藏 false (勿删除,VITE_HIDE_HOME只需在.env文件配置)
|
||||
VITE_HIDE_HOME = true
|
||||
8
frontend/.env.development
Executable file
8
frontend/.env.development
Executable file
@@ -0,0 +1,8 @@
|
||||
# 平台本地运行端口号(若 12345 无权限可改用 5173)
|
||||
VITE_PORT = 5173
|
||||
|
||||
# 开发环境读取配置文件路径
|
||||
VITE_PUBLIC_PATH = /
|
||||
|
||||
# 开发环境路由历史模式(Hash模式传"hash"、HTML5模式传"h5"、Hash模式带base参数传"hash,base参数"、HTML5模式带base参数传"h5,base参数")
|
||||
VITE_ROUTER_HISTORY = "hash"
|
||||
17
frontend/.env.production
Executable file
17
frontend/.env.production
Executable file
@@ -0,0 +1,17 @@
|
||||
# 线上环境平台打包路径
|
||||
VITE_PUBLIC_PATH = /
|
||||
|
||||
# 线上环境路由历史模式(Hash模式传"hash"、HTML5模式传"h5"、Hash模式带base参数传"hash,base参数"、HTML5模式带base参数传"h5,base参数")
|
||||
VITE_ROUTER_HISTORY = "hash"
|
||||
|
||||
# 是否在打包时使用cdn替换本地库 替换 true 不替换 false
|
||||
VITE_CDN = false
|
||||
|
||||
# 是否启用gzip压缩或brotli压缩(分两种情况,删除原始文件和不删除原始文件)
|
||||
# 压缩时不删除原始文件的配置:gzip、brotli、both(同时开启 gzip 与 brotli 压缩)、none(不开启压缩,默认)
|
||||
# 压缩时删除原始文件的配置:gzip-clear、brotli-clear、both-clear(同时开启 gzip 与 brotli 压缩)、none(不开启压缩,默认)
|
||||
VITE_COMPRESSION = "none"
|
||||
|
||||
# 生产环境 API 地址(可选)。不填则默认请求「当前访问的域名:8080」,前后端同机时无需 Nginx 代理 /api
|
||||
# 示例:VITE_API_BASE_URL = http://129.204.148.31:8080
|
||||
# VITE_API_BASE_URL =
|
||||
16
frontend/.env.staging
Executable file
16
frontend/.env.staging
Executable file
@@ -0,0 +1,16 @@
|
||||
# 预发布也需要生产环境的行为
|
||||
# https://cn.vitejs.dev/guide/env-and-mode.html#modes
|
||||
# NODE_ENV = development
|
||||
|
||||
VITE_PUBLIC_PATH = /
|
||||
|
||||
# 预发布环境路由历史模式(Hash模式传"hash"、HTML5模式传"h5"、Hash模式带base参数传"hash,base参数"、HTML5模式带base参数传"h5,base参数")
|
||||
VITE_ROUTER_HISTORY = "hash"
|
||||
|
||||
# 是否在打包时使用cdn替换本地库 替换 true 不替换 false
|
||||
VITE_CDN = true
|
||||
|
||||
# 是否启用gzip压缩或brotli压缩(分两种情况,删除原始文件和不删除原始文件)
|
||||
# 压缩时不删除原始文件的配置:gzip、brotli、both(同时开启 gzip 与 brotli 压缩)、none(不开启压缩,默认)
|
||||
# 压缩时删除原始文件的配置:gzip-clear、brotli-clear、both-clear(同时开启 gzip 与 brotli 压缩)、none(不开启压缩,默认)
|
||||
VITE_COMPRESSION = "none"
|
||||
22
frontend/.gitignore
vendored
Executable file
22
frontend/.gitignore
vendored
Executable file
@@ -0,0 +1,22 @@
|
||||
node_modules
|
||||
.DS_Store
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
.eslintcache
|
||||
report.html
|
||||
vite.config.*.timestamp*
|
||||
|
||||
yarn.lock
|
||||
npm-debug.log*
|
||||
.pnpm-error.log*
|
||||
.pnpm-debug.log
|
||||
tests/**/coverage/
|
||||
|
||||
# Editor directories and files
|
||||
.idea
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
tsconfig.tsbuildinfo
|
||||
8
frontend/.husky/commit-msg
Executable file
8
frontend/.husky/commit-msg
Executable file
@@ -0,0 +1,8 @@
|
||||
#!/bin/sh
|
||||
|
||||
# shellcheck source=./_/husky.sh
|
||||
. "$(dirname "$0")/_/husky.sh"
|
||||
|
||||
PATH="/usr/local/bin:$PATH"
|
||||
|
||||
npx --no-install commitlint --edit "$1"
|
||||
9
frontend/.husky/common.sh
Executable file
9
frontend/.husky/common.sh
Executable file
@@ -0,0 +1,9 @@
|
||||
#!/bin/sh
|
||||
command_exists () {
|
||||
command -v "$1" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
# Workaround for Windows 10, Git Bash and Pnpm
|
||||
if command_exists winpty && test -t 1; then
|
||||
exec < /dev/tty
|
||||
fi
|
||||
10
frontend/.husky/pre-commit
Executable file
10
frontend/.husky/pre-commit
Executable file
@@ -0,0 +1,10 @@
|
||||
#!/bin/sh
|
||||
. "$(dirname "$0")/_/husky.sh"
|
||||
. "$(dirname "$0")/common.sh"
|
||||
|
||||
[ -n "$CI" ] && exit 0
|
||||
|
||||
PATH="/usr/local/bin:$PATH"
|
||||
|
||||
# Perform lint check on files in the staging area through .lintstagedrc configuration
|
||||
pnpm exec lint-staged
|
||||
20
frontend/.lintstagedrc
Executable file
20
frontend/.lintstagedrc
Executable file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"*.{js,jsx,ts,tsx}": [
|
||||
"prettier --cache --ignore-unknown --write",
|
||||
"eslint --cache --fix"
|
||||
],
|
||||
"{!(package)*.json,*.code-snippets,.!({browserslist,npm,nvm})*rc}": [
|
||||
"prettier --cache --write--parser json"
|
||||
],
|
||||
"package.json": ["prettier --cache --write"],
|
||||
"*.vue": [
|
||||
"prettier --write",
|
||||
"eslint --cache --fix",
|
||||
"stylelint --fix --allow-empty-input"
|
||||
],
|
||||
"*.{css,scss,html}": [
|
||||
"prettier --cache --ignore-unknown --write",
|
||||
"stylelint --fix --allow-empty-input"
|
||||
],
|
||||
"*.md": ["prettier --cache --ignore-unknown --write"]
|
||||
}
|
||||
11
frontend/.markdownlint.json
Executable file
11
frontend/.markdownlint.json
Executable file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"default": true,
|
||||
"MD003": false,
|
||||
"MD033": false,
|
||||
"MD013": false,
|
||||
"MD001": false,
|
||||
"MD025": false,
|
||||
"MD024": false,
|
||||
"MD007": { "indent": 4 },
|
||||
"no-hard-tabs": false
|
||||
}
|
||||
4
frontend/.npmrc
Executable file
4
frontend/.npmrc
Executable file
@@ -0,0 +1,4 @@
|
||||
shell-emulator=true
|
||||
shamefully-hoist=true
|
||||
enable-pre-post-scripts=false
|
||||
strict-peer-dependencies=false
|
||||
1
frontend/.nvmrc
Executable file
1
frontend/.nvmrc
Executable file
@@ -0,0 +1 @@
|
||||
v22.14.0
|
||||
9
frontend/.prettierrc.js
Executable file
9
frontend/.prettierrc.js
Executable file
@@ -0,0 +1,9 @@
|
||||
// @ts-check
|
||||
|
||||
/** @type {import("prettier").Config} */
|
||||
export default {
|
||||
bracketSpacing: true,
|
||||
singleQuote: false,
|
||||
arrowParens: "avoid",
|
||||
trailingComma: "none"
|
||||
};
|
||||
4
frontend/.stylelintignore
Executable file
4
frontend/.stylelintignore
Executable file
@@ -0,0 +1,4 @@
|
||||
/dist/*
|
||||
/public/*
|
||||
public/*
|
||||
src/style/reset.scss
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user