Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8673b539b7 | ||
|
|
2ec7f21cb1 | ||
|
|
4802c97012 | ||
|
|
207345713d | ||
|
|
c0c32e7d68 | ||
|
|
2e6c0d0b15 | ||
|
|
dd0100e984 | ||
|
|
4254b6c81d | ||
|
|
d10ae77fd3 | ||
|
|
6d363ae64e | ||
|
|
8d2e17478d | ||
|
|
838e61ee44 | ||
|
|
b02479e3fb | ||
|
|
3f6910a9c2 | ||
|
|
7cc8c8d63f | ||
|
|
55fe12b30c | ||
|
|
19eade5f32 | ||
|
|
282a089c5c | ||
|
|
00d959064a | ||
|
|
96cf623fbd | ||
|
|
13160d54e1 | ||
|
|
cccfca6ef1 | ||
|
|
34e18f8a47 | ||
|
|
60b7fb3a27 | ||
|
|
ffed027ba1 | ||
|
|
aa43a51db5 | ||
|
|
0279083c59 | ||
|
|
81761798ea | ||
|
|
3622adaa33 | ||
|
|
b56c9061d3 | ||
|
|
c88da44db0 | ||
|
|
311bd719c9 | ||
|
|
246d49a038 | ||
|
|
6bcd8f4d09 | ||
|
|
183f5e7896 | ||
|
|
fa0dc7e7e1 | ||
|
|
e85a741159 | ||
|
|
cd6852775a | ||
|
|
b83662b3ad | ||
|
|
41bd418896 | ||
|
|
afacf22700 | ||
|
|
d00f1b0115 | ||
|
|
df34fc30cc | ||
|
|
bb4de28b78 | ||
|
|
3b09705c25 | ||
|
|
014039cbd1 | ||
|
|
e18a2cf9e5 | ||
|
|
22dcff976d | ||
|
|
98b78e8ed9 | ||
|
|
92c93cb24f | ||
|
|
cceecc29a8 | ||
|
|
62dd1051dd | ||
|
|
a0b506cc38 | ||
|
|
3f291cc363 | ||
|
|
af382db83f | ||
|
|
1c677f9987 | ||
|
|
e6bc739db9 | ||
|
|
609698be81 | ||
|
|
7cd5b4fb66 | ||
|
|
38afe0d00e | ||
|
|
ef6ee72866 | ||
|
|
1c6e6f826b | ||
|
|
db7ad2827a |
No files matched your search
@@ -0,0 +1,19 @@
|
||||
# 生产环境配置示例
|
||||
# 复制此文件为 .env 并修改相应配置
|
||||
|
||||
# MySQL配置
|
||||
MYSQL_ROOT_PASSWORD=your-strong-root-password
|
||||
MYSQL_PASSWORD=your-strong-user-password
|
||||
MYSQL_PORT=3306
|
||||
|
||||
# 应用端口配置
|
||||
BACKEND_PORT=18888
|
||||
FRONTEND_PORT=80
|
||||
|
||||
# Redis配置 (可选)
|
||||
REDIS_PASSWORD=your-redis-password
|
||||
REDIS_PORT=6379
|
||||
|
||||
# 镜像配置 (用于生产环境)
|
||||
BACKEND_IMAGE=ghcr.io/your-org/license-manager-backend:latest
|
||||
FRONTEND_IMAGE=ghcr.io/your-org/license-manager-frontend:latest
|
||||
@@ -49,7 +49,7 @@ jobs:
|
||||
username: root
|
||||
password: ${{ secrets.PASS_1 }}
|
||||
port: 22
|
||||
source: "backend-image.tar,backend/configs/config.example.yaml,backend/configs/config.yaml"
|
||||
source: "backend-image.tar"
|
||||
target: "/opt/license-manager/"
|
||||
|
||||
- name: Deploy to server
|
||||
@@ -73,23 +73,11 @@ jobs:
|
||||
docker rmi ${{ env.IMAGE_NAME }}:old || true
|
||||
docker tag ${{ env.IMAGE_NAME }}:latest ${{ env.IMAGE_NAME }}:old || true
|
||||
|
||||
# Create config file if not exists
|
||||
if [ ! -f config.yaml ]; then
|
||||
if [ -f backend/configs/config.yaml ]; then
|
||||
cp backend/configs/config.yaml config.yaml
|
||||
echo "Created config.yaml from backend configs."
|
||||
else
|
||||
cp backend/configs/config.example.yaml config.yaml
|
||||
echo "Created config.yaml from example. Please review and update configuration."
|
||||
fi
|
||||
fi
|
||||
|
||||
# Run new container
|
||||
# Run new container with host network
|
||||
docker run -d \
|
||||
--name ${{ env.CONTAINER_NAME }} \
|
||||
--restart unless-stopped \
|
||||
-p 18888:18888 \
|
||||
-v /opt/license-manager/config.yaml:/app/config.yaml \
|
||||
--network host \
|
||||
-v /opt/license-manager/logs:/app/logs \
|
||||
${{ env.IMAGE_NAME }}:latest
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ jobs:
|
||||
|
||||
- name: Build Docker image
|
||||
run: |
|
||||
docker build -f Dockerfile.frontend -t ${{ env.IMAGE_NAME }}:latest .
|
||||
docker build -f Dockerfile.frontend --build-arg NGINX_CONF=nginx-ci.conf -t ${{ env.IMAGE_NAME }}:latest .
|
||||
docker save ${{ env.IMAGE_NAME }}:latest > frontend-image.tar
|
||||
|
||||
- name: Copy files to server
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
name: Release Backend Image
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: 'Release version (e.g., v1.0.0)'
|
||||
required: true
|
||||
type: string
|
||||
|
||||
env:
|
||||
REGISTRY: ghcr.io
|
||||
IMAGE_NAME: ${{ github.repository }}-backend
|
||||
|
||||
jobs:
|
||||
test-and-build:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: '1.23'
|
||||
|
||||
- name: Cache Go modules
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: |
|
||||
~/.cache/go-build
|
||||
~/go/pkg/mod
|
||||
key: ${{ runner.os }}-go-${{ hashFiles('backend/go.sum') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-go-
|
||||
|
||||
- name: Run tests
|
||||
run: |
|
||||
cd backend
|
||||
go mod tidy
|
||||
go mod verify
|
||||
go test -v ./...
|
||||
go build -ldflags="-s -w" -o license-manager cmd/main.go
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Log in to Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Extract metadata
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||
tags: |
|
||||
type=raw,value=latest
|
||||
type=raw,value=${{ github.event.inputs.version }}
|
||||
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile.backend
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
|
||||
- name: Generate summary
|
||||
run: |
|
||||
echo "## 🚀 Backend Release Successful!" >> $GITHUB_STEP_SUMMARY
|
||||
echo "**Version:** ${{ github.event.inputs.version }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "**Image:** ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "**Tags:**" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- ${{ github.event.inputs.version }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- latest" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "### Usage:" >> $GITHUB_STEP_SUMMARY
|
||||
echo '```bash' >> $GITHUB_STEP_SUMMARY
|
||||
echo "docker pull ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.event.inputs.version }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo '```' >> $GITHUB_STEP_SUMMARY
|
||||
@@ -0,0 +1,71 @@
|
||||
name: Release Frontend Image
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: 'Release version (e.g., v1.0.0)'
|
||||
required: true
|
||||
type: string
|
||||
|
||||
env:
|
||||
REGISTRY: ghcr.io
|
||||
IMAGE_NAME: ${{ github.repository }}-frontend
|
||||
|
||||
jobs:
|
||||
build-and-push:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Log in to Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Extract metadata
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||
tags: |
|
||||
type=raw,value=latest
|
||||
type=raw,value=${{ github.event.inputs.version }}
|
||||
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile.frontend
|
||||
build-args: |
|
||||
NGINX_CONF=nginx-ci.conf
|
||||
platforms: linux/amd64
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
|
||||
- name: Generate summary
|
||||
run: |
|
||||
echo "## 🎉 Frontend Release Successful!" >> $GITHUB_STEP_SUMMARY
|
||||
echo "**Version:** ${{ github.event.inputs.version }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "**Image:** ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "**Tags:**" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- ${{ github.event.inputs.version }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- latest" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "### Usage:" >> $GITHUB_STEP_SUMMARY
|
||||
echo '```bash' >> $GITHUB_STEP_SUMMARY
|
||||
echo "docker pull ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.event.inputs.version }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo '```' >> $GITHUB_STEP_SUMMARY
|
||||
@@ -1,23 +0,0 @@
|
||||
# Multi-stage build
|
||||
FROM node:18-alpine AS frontend-builder
|
||||
WORKDIR /app/frontend
|
||||
COPY frontend/package*.json ./
|
||||
RUN npm install
|
||||
COPY frontend/ ./
|
||||
RUN npm run build
|
||||
|
||||
FROM golang:1.23-alpine AS backend-builder
|
||||
WORKDIR /app/backend
|
||||
COPY backend/go.mod backend/go.sum ./
|
||||
RUN go mod download
|
||||
COPY backend/ ./
|
||||
RUN go build -o license-manager cmd/main.go
|
||||
|
||||
FROM alpine:latest
|
||||
RUN apk --no-cache add ca-certificates
|
||||
WORKDIR /app
|
||||
COPY --from=backend-builder /app/backend/license-manager .
|
||||
COPY --from=frontend-builder /app/frontend/dist ./static
|
||||
COPY backend/configs/config.example.yaml ./config.yaml
|
||||
EXPOSE 8080
|
||||
CMD ["./license-manager"]
|
||||
@@ -7,9 +7,9 @@ COPY backend/ ./
|
||||
RUN go build -ldflags="-s -w" -o license-manager cmd/main.go
|
||||
|
||||
FROM alpine:latest
|
||||
RUN apk --no-cache add ca-certificates tzdata
|
||||
WORKDIR /app
|
||||
COPY --from=builder /app/backend/license-manager .
|
||||
COPY backend/configs/config.example.yaml ./config.example.yaml
|
||||
RUN apk --no-cache add ca-certificates tzdata curl
|
||||
WORKDIR /app/backend/cmd
|
||||
COPY --from=builder /app/backend/license-manager ./license-manager
|
||||
COPY backend/configs/ ../configs/
|
||||
EXPOSE 18888
|
||||
CMD ["./license-manager"]
|
||||
@@ -4,7 +4,7 @@ FROM node:20-alpine AS builder
|
||||
WORKDIR /app
|
||||
COPY frontend/package.json ./
|
||||
|
||||
# Install dependencies without package-lock.json constraints
|
||||
# Install dependencies (ignore package-lock.json since it's outdated)
|
||||
RUN npm install
|
||||
|
||||
COPY frontend/ ./
|
||||
@@ -14,7 +14,7 @@ ENV VITE_OUTDIR=dist
|
||||
ENV VITE_ADDRESS_BASE_URL=/
|
||||
|
||||
# Build the project and verify output
|
||||
RUN npm run prod && ls -la && ls -la dist/
|
||||
RUN npm run build:prod && ls -la && ls -la dist/
|
||||
|
||||
# Production stage with Nginx
|
||||
FROM nginx:alpine
|
||||
@@ -22,8 +22,10 @@ FROM nginx:alpine
|
||||
# Copy built assets
|
||||
COPY --from=builder /app/dist /usr/share/nginx/html
|
||||
|
||||
# Copy custom nginx configuration
|
||||
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
||||
# Selectable nginx configuration (default: nginx.conf). Use build-arg to switch.
|
||||
ARG NGINX_CONF=nginx.conf
|
||||
ENV NGINX_CONF=${NGINX_CONF}
|
||||
COPY ${NGINX_CONF} /etc/nginx/conf.d/default.conf
|
||||
|
||||
# Create log directory
|
||||
RUN mkdir -p /var/log/nginx
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
# Docker 部署指南
|
||||
|
||||
本项目提供两种部署:开发环境与生产环境。以下说明已与当前 `docker-compose` 配置、镜像与反向代理实现对齐。
|
||||
|
||||
## 开发环境
|
||||
|
||||
使用 `docker compose`(Docker Desktop 或 Docker CE 20.10+):
|
||||
|
||||
```bash
|
||||
# 启动(首次建议带 --build)
|
||||
docker compose up -d --build
|
||||
|
||||
# 查看状态/日志
|
||||
docker compose ps
|
||||
docker compose logs -f backend
|
||||
|
||||
# 停止
|
||||
docker compose down
|
||||
```
|
||||
|
||||
### 访问地址(开发)
|
||||
- 前端: http://localhost:18080
|
||||
- 后端 API: http://localhost:18888
|
||||
- MySQL: localhost:13306(数据库、用户、密码见 compose)
|
||||
|
||||
## 生产环境
|
||||
|
||||
使用 `docker-compose.prod.yml`:
|
||||
|
||||
```bash
|
||||
# 启动生产环境
|
||||
docker compose -f docker-compose.prod.yml up -d
|
||||
|
||||
# 查看服务状态
|
||||
docker compose -f docker-compose.prod.yml ps
|
||||
```
|
||||
|
||||
### 生产配置说明
|
||||
- 后端配置文件已通过卷挂载至容器内:
|
||||
- 开发:`backend/configs/config.dev.yaml` → 容器内 `/app/backend/cmd/config.yaml`
|
||||
- 生产:`backend/configs/config.prod.yaml` → 容器内 `/app/backend/cmd/config.yaml`
|
||||
- 前端 Nginx 反向代理已指向容器网络内的 `backend:18888`,无需依赖 `host.docker.internal`。
|
||||
- 健康检查统一使用 `curl`,后端镜像已内置 `curl`。
|
||||
- 如需隐藏后端对外端口,可在生产 compose 中移除后端端口映射,仅通过前端或网关暴露。
|
||||
|
||||
## 服务说明
|
||||
|
||||
### MySQL 8.0
|
||||
- 端口:3306(生产)/ 13306(开发宿主机映射)
|
||||
- 库:`license_manager`,用户:`license_user`
|
||||
- 字符集/排序:`utf8mb4`/`utf8mb4_unicode_ci`
|
||||
- 开启慢查询日志(生产)
|
||||
|
||||
### 后端服务
|
||||
- 端口:18888
|
||||
- 健康检查:`/health`
|
||||
- 配置:通过卷挂载 `config.dev.yaml` / `config.prod.yaml` 为容器内 `config.yaml`
|
||||
- 日志:标准输出 + `./logs` 挂载目录
|
||||
|
||||
### 前端服务
|
||||
- 端口:80(生产)/ 18080(开发映射)
|
||||
- 反向代理:`/api/` → `backend:18888`
|
||||
- 静态资源:启用 Gzip 与长缓存
|
||||
|
||||
### Redis(可选,仅生产)
|
||||
- 端口:6379,AOF 持久化
|
||||
|
||||
## 常用命令
|
||||
|
||||
```bash
|
||||
# 重新构建镜像
|
||||
docker compose build
|
||||
|
||||
# 启动指定服务
|
||||
docker compose up -d mysql backend
|
||||
|
||||
# 进入容器
|
||||
docker compose exec backend sh
|
||||
docker compose exec mysql mysql -uroot -p
|
||||
|
||||
# 备份/恢复数据库
|
||||
docker compose exec mysql mysqldump -uroot -p license_manager > backup.sql
|
||||
docker compose exec -T mysql mysql -uroot -p license_manager < backup.sql
|
||||
|
||||
# 清理未使用资源
|
||||
docker system prune -f
|
||||
```
|
||||
|
||||
## 故障排查
|
||||
|
||||
1) MySQL 启动失败
|
||||
```bash
|
||||
docker compose logs mysql
|
||||
docker compose down -v && docker compose up -d
|
||||
```
|
||||
|
||||
2) 后端连接数据库失败
|
||||
```bash
|
||||
# 确认 MySQL 健康
|
||||
docker compose ps
|
||||
|
||||
# 检查容器内配置(已挂载到 /app/backend/cmd/config.yaml)
|
||||
docker compose exec backend cat /app/backend/cmd/config.yaml
|
||||
```
|
||||
|
||||
3) 前端无法访问后端 API
|
||||
```bash
|
||||
# 容器内连通性
|
||||
docker compose exec frontend sh -c "curl -sS http://backend:18888/health"
|
||||
|
||||
# 检查 nginx 配置
|
||||
docker compose exec frontend cat /etc/nginx/conf.d/default.conf
|
||||
```
|
||||
|
||||
## 注意事项
|
||||
|
||||
- 生产环境请修改默认账号、数据库口令与 JWT 秘钥(见 `backend/configs/config.prod.yaml`)。
|
||||
- 如使用较新 Docker,YAML 顶部 `version` 字段会被忽略,可按需移除提示。
|
||||
- `deploy.resources` 仅在 Swarm/K8s 生效,单机 Compose 可忽略。
|
||||
- 如置于公网,建议仅暴露前端端口,通过前端或网关转发后端 API。
|
||||
@@ -6,7 +6,15 @@
|
||||
|
||||
## 项目概述
|
||||
|
||||
软件授权管理平台是一个独立的软件授权管理系统,为IoT平台等软件系统提供授权码生成、分发、验证和管理服务。支持在线和离线两种授权模式,基于硬件绑定的授权机制确保安全性。
|
||||
软件授权管理平台是一个独立的软件授权管理系统,为软件系统提供授权码生成、分发、验证和管理服务。支持在线和离线两种授权模式,基于硬件绑定的授权机制确保安全性。
|
||||
|
||||
## 系统界面
|
||||
|
||||
### 登录界面
|
||||

|
||||
|
||||
### 管理仪表盘
|
||||

|
||||
|
||||
## 核心功能
|
||||
|
||||
@@ -21,59 +29,89 @@
|
||||
## 技术栈
|
||||
|
||||
- **前端**:Vue.js 3+ 配合现代化UI组件
|
||||
- **后端**:Go 1.23+ 使用Gorilla Mux路由和Logrus日志
|
||||
- **数据库**:PostgreSQL 12+ / MySQL 12+
|
||||
- **后端**:Go 1.23+ 使用Gin框架、GORM、Viper配置和Logrus日志
|
||||
- **数据库**:MySQL 8+
|
||||
- **配置**:YAML格式配置文件
|
||||
- **部署**:Docker、单机部署或系统服务
|
||||
|
||||
## API接口
|
||||
|
||||
```
|
||||
POST /api/validate - 授权验证
|
||||
POST /api/activate - 授权激活
|
||||
POST /api/heartbeat - 心跳上报
|
||||
GET /api/license/{code} - 授权信息查询
|
||||
GET /api/customers - 客户列表API
|
||||
GET /tools/{tool} - 工具下载
|
||||
```
|
||||
|
||||
## 安全与性能
|
||||
|
||||
- **安全特性**:HMAC-SHA256签名,硬件指纹绑定,HTTPS传输加密
|
||||
- **性能表现**:支持100+并发用户,API响应时间<2秒
|
||||
- **可靠性**:完善的错误处理和日志记录
|
||||
- **安全特性**:
|
||||
- JWT身份认证和授权机制
|
||||
- HMAC-SHA256签名验证
|
||||
- 硬件指纹绑定防盗版
|
||||
- HTTPS传输加密
|
||||
- 多语言错误信息支持(中/英/日)
|
||||
|
||||
- **性能表现**:
|
||||
- **高并发**:Go原生协程支持,理论支持10,000+并发连接
|
||||
- **低延迟**:API平均响应时间 < 50ms
|
||||
- **内存优化**:Go GC优化,内存占用 < 100MB
|
||||
- **数据库连接池**:支持连接复用,最大化数据库性能
|
||||
|
||||
- **可靠性**:
|
||||
- 完善的错误处理和多语言错误信息
|
||||
- 结构化日志记录和监控
|
||||
- 自动数据库迁移
|
||||
- 优雅关闭和资源清理
|
||||
|
||||
## 安装部署
|
||||
|
||||
```bash
|
||||
# 克隆项目
|
||||
git clone <repository-url>
|
||||
git clone https://github.com/cedar-v/license-manager.git
|
||||
cd license-manager
|
||||
|
||||
# 构建应用
|
||||
# 本地后端构建与运行(可选)
|
||||
cd backend/cmd
|
||||
go build -o license-manager
|
||||
|
||||
# 配置应用
|
||||
cp config.example.yaml config.yaml
|
||||
# 编辑 config.yaml 配置文件
|
||||
# 配置(二选一)
|
||||
# 1) 在当前目录创建配置文件(程序优先读取工作目录下的 config.yaml)
|
||||
# cp ../configs/config.example.yaml ./config.yaml && 编辑
|
||||
# 2) 直接编辑 ../configs/config.yaml(程序会自动回退读取该文件)
|
||||
|
||||
# 运行应用
|
||||
./license-manager
|
||||
```
|
||||
|
||||
## Docker部署
|
||||
## Docker 部署
|
||||
|
||||
推荐使用 Docker Compose,已提供开发与生产编排文件:
|
||||
|
||||
```bash
|
||||
# 构建Docker镜像
|
||||
docker build -t license-manager .
|
||||
# 开发环境(首次建议带 --build)
|
||||
docker compose up -d --build
|
||||
|
||||
# 使用Docker运行
|
||||
docker run -p 8080:8080 -v $(pwd)/config.yaml:/app/config.yaml license-manager
|
||||
# 生产环境
|
||||
docker compose -f docker-compose.prod.yml up -d
|
||||
|
||||
# 验证后端健康
|
||||
curl http://localhost:18888/health
|
||||
```
|
||||
|
||||
完整部署说明(反向代理、健康检查、配置挂载等)请参见 `README-Docker.md`。
|
||||
|
||||
## 开源许可证
|
||||
|
||||
本项目采用GNU通用公共许可证v3.0 - 详见[LICENSE](LICENSE)文件。
|
||||
本项目采用 **GNU General Public License v3.0 (GPL-3.0)** 开源许可证。
|
||||
|
||||
### 许可证说明
|
||||
|
||||
- **自由使用**:您可以自由地使用、学习、修改和分发本软件
|
||||
- **开源要求**:如果您分发修改版本,必须同样开源并采用GPL-3.0许可证
|
||||
- **版权保护**:使用本软件的衍生作品必须保留原始版权声明
|
||||
- **无担保**:软件按"现状"提供,不提供任何明示或暗示的担保
|
||||
|
||||
### 商业使用
|
||||
|
||||
- 允许商业使用,但衍生作品必须同样开源
|
||||
- 如需专有许可证或商业支持,请联系项目维护者
|
||||
|
||||
### 许可证全文
|
||||
|
||||
详细许可证条款请查看项目根目录的 [LICENSE](LICENSE) 文件,或访问:
|
||||
https://www.gnu.org/licenses/gpl-3.0.html
|
||||
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -8,6 +8,14 @@
|
||||
|
||||
License Manager is an independent software licensing platform that provides license code generation, distribution, validation, and management services for IoT platforms and other software systems. It offers both online and offline licensing modes with hardware-based binding for enhanced security.
|
||||
|
||||
## System Interface
|
||||
|
||||
### Login Page
|
||||

|
||||
|
||||
### Management Dashboard
|
||||

|
||||
|
||||
## Key Features
|
||||
|
||||
- 🔧 **Customer Management**: Complete customer information management with status control
|
||||
@@ -21,8 +29,8 @@ License Manager is an independent software licensing platform that provides lice
|
||||
## Technical Stack
|
||||
|
||||
- **Frontend**: Vue.js 3+ with modern UI components
|
||||
- **Backend**: Go 1.23+ with Gorilla Mux router and Logrus logging
|
||||
- **Database**: PostgreSQL 12+ / MySQL 12+
|
||||
- **Backend**: Go 1.23+ with Gin framework, GORM, Viper configuration and Logrus logging
|
||||
- **Database**: MySQL 8+
|
||||
- **Configuration**: YAML format configuration files
|
||||
- **Deployment**: Docker, single machine, or system service
|
||||
|
||||
@@ -39,41 +47,87 @@ GET /tools/{tool} - Tool download
|
||||
|
||||
## Security & Performance
|
||||
|
||||
- **Security**: HMAC-SHA256 signing, hardware fingerprint binding, HTTPS encryption
|
||||
- **Performance**: Supports 100+ concurrent users with <2s API response time
|
||||
- **Reliability**: Comprehensive error handling and logging
|
||||
- **Security Features**:
|
||||
- JWT authentication and authorization
|
||||
- HMAC-SHA256 signature verification
|
||||
- Hardware fingerprint binding for anti-piracy
|
||||
- HTTPS transport encryption
|
||||
- Multi-language error message support (Chinese/English/Japanese)
|
||||
|
||||
- **Performance**:
|
||||
- **High Concurrency**: Go native goroutines support, theoretically supports 10,000+ concurrent connections
|
||||
- **Low Latency**: Average API response time < 50ms
|
||||
- **Memory Optimized**: Go GC optimization, memory usage < 100MB
|
||||
- **Database Connection Pool**: Connection reuse for maximum database performance
|
||||
|
||||
- **Reliability**:
|
||||
- Comprehensive error handling with multi-language error messages
|
||||
- Structured logging and monitoring
|
||||
- Automatic database migrations
|
||||
- Graceful shutdown and resource cleanup
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
# Clone the repository
|
||||
git clone <repository-url>
|
||||
git clone https://github.com/cedar-v/license-manager.git
|
||||
cd license-manager
|
||||
|
||||
# Build the application
|
||||
# Local backend build & run (optional)
|
||||
cd backend/cmd
|
||||
go build -o license-manager
|
||||
|
||||
# Configure the application
|
||||
cp config.example.yaml config.yaml
|
||||
# Edit config.yaml with your settings
|
||||
# Configuration (choose one)
|
||||
# 1) Create config.yaml in the working directory (the app looks here first)
|
||||
# cp ../configs/config.example.yaml ./config.yaml && edit it
|
||||
# 2) Edit ../configs/config.yaml (the app will fall back to this path)
|
||||
|
||||
# Run the application
|
||||
./license-manager
|
||||
```
|
||||
|
||||
## Docker Deployment
|
||||
|
||||
```bash
|
||||
# Build Docker image
|
||||
docker build -t license-manager .
|
||||
We provide ready-to-use Docker Compose files for development and production:
|
||||
|
||||
# Run with Docker
|
||||
docker run -p 8080:8080 -v $(pwd)/config.yaml:/app/config.yaml license-manager
|
||||
```bash
|
||||
# Development (first run with --build is recommended)
|
||||
docker compose up -d --build
|
||||
|
||||
# Production
|
||||
docker compose -f docker-compose.prod.yml up -d
|
||||
|
||||
# Verify backend health
|
||||
curl http://localhost:18888/health
|
||||
```
|
||||
|
||||
## License
|
||||
Notes
|
||||
- Backend config is mounted by Compose:
|
||||
- Dev: `backend/configs/config.dev.yaml` → `/app/backend/cmd/config.yaml`
|
||||
- Prod: `backend/configs/config.prod.yaml` → `/app/backend/cmd/config.yaml`
|
||||
- Frontend Nginx proxies `/api/` to `backend:18888` inside the Docker network.
|
||||
- Health checks use `curl`; the backend image includes `curl`.
|
||||
- For full details (reverse proxy, health checks, mounts, troubleshooting), see `README-Docker.md`.
|
||||
|
||||
This project is licensed under the GNU General Public License v3.0 - see the [LICENSE](LICENSE) file for details.
|
||||
## Open Source License
|
||||
|
||||
This project is licensed under the **GNU General Public License v3.0 (GPL-3.0)**.
|
||||
|
||||
### License Description
|
||||
|
||||
- **Free Use**: You are free to use, study, modify, and distribute this software
|
||||
- **Open Source Requirement**: If you distribute modified versions, they must also be open source under GPL-3.0 license
|
||||
- **Copyright Protection**: Derivative works using this software must retain the original copyright notice
|
||||
- **No Warranty**: The software is provided "as is" without any express or implied warranty
|
||||
|
||||
### Commercial Use
|
||||
|
||||
- Commercial use is permitted, but derivative works must also be open source
|
||||
- For proprietary licensing or commercial support, please contact the project maintainers
|
||||
|
||||
### Full License Text
|
||||
|
||||
For detailed license terms, please see the [LICENSE](LICENSE) file in the project root directory, or visit:
|
||||
https://www.gnu.org/licenses/gpl-3.0.html
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -3,11 +3,15 @@ package main
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"license-manager/internal/api/routes"
|
||||
"license-manager/internal/config"
|
||||
"license-manager/internal/database"
|
||||
"license-manager/pkg/i18n"
|
||||
"license-manager/pkg/logger"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// @title License Manager API
|
||||
@@ -22,8 +26,8 @@ import (
|
||||
// @license.name MIT
|
||||
// @license.url https://opensource.org/licenses/MIT
|
||||
|
||||
// @host localhost:18888
|
||||
// @BasePath /
|
||||
// @schemes http https
|
||||
|
||||
// @securityDefinitions.apikey BearerAuth
|
||||
// @in header
|
||||
@@ -32,7 +36,12 @@ import (
|
||||
|
||||
func main() {
|
||||
// 加载配置
|
||||
if err := config.Load(""); err != nil {
|
||||
configPath := "config.yaml" // Docker 环境默认路径
|
||||
if _, err := os.Stat(configPath); os.IsNotExist(err) {
|
||||
configPath = "../configs/config.yaml" // 本地开发环境
|
||||
}
|
||||
|
||||
if err := config.Load(configPath); err != nil {
|
||||
log.Fatalf("Failed to load config: %v", err)
|
||||
}
|
||||
|
||||
@@ -43,7 +52,37 @@ func main() {
|
||||
log.Info("启动 License Manager 服务器...")
|
||||
|
||||
cfg := config.GetConfig()
|
||||
|
||||
|
||||
// 初始化数据库
|
||||
if err := database.InitDatabase(&cfg.Database); err != nil {
|
||||
log.Fatalf("数据库初始化失败: %v", err)
|
||||
}
|
||||
defer database.Close()
|
||||
|
||||
// 运行数据库迁移
|
||||
if err := database.RunMigrationIfEnabled(); err != nil {
|
||||
log.Fatalf("数据库迁移失败: %v", err)
|
||||
}
|
||||
|
||||
// 初始化多语言支持
|
||||
if cfg.I18n.Enable {
|
||||
if err := i18n.InitGlobalManager(cfg.I18n.ConfigPath, cfg.I18n.DefaultLang); err != nil {
|
||||
log.Fatalf("多语言初始化失败: %v", err)
|
||||
}
|
||||
|
||||
// 预加载支持的语言
|
||||
for _, lang := range cfg.I18n.SupportLangs {
|
||||
if err := i18n.LoadLanguage(lang); err != nil {
|
||||
log.Warnf("加载语言包 %s 失败: %v", lang, err)
|
||||
} else {
|
||||
log.Infof("成功加载语言包: %s", lang)
|
||||
}
|
||||
}
|
||||
log.Info("多语言支持已启用")
|
||||
} else {
|
||||
log.Info("多语言支持已禁用")
|
||||
}
|
||||
|
||||
// 设置Gin模式
|
||||
gin.SetMode(cfg.Server.Mode)
|
||||
|
||||
@@ -53,8 +92,8 @@ func main() {
|
||||
// 启动服务器
|
||||
addr := fmt.Sprintf("%s:%d", cfg.Server.Host, cfg.Server.Port)
|
||||
log.Infof("服务器启动在 %s", addr)
|
||||
|
||||
|
||||
if err := router.Run(addr); err != nil {
|
||||
log.Fatalf("服务器启动失败: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
server:
|
||||
host: "0.0.0.0"
|
||||
port: 18888
|
||||
mode: "debug"
|
||||
|
||||
database:
|
||||
driver: "mysql"
|
||||
host: "mysql"
|
||||
port: 3306
|
||||
username: "license_user"
|
||||
password: "license@123"
|
||||
database: "license_manager"
|
||||
charset: "utf8mb4"
|
||||
parse_time: true
|
||||
loc: "Local"
|
||||
max_idle_conns: 10
|
||||
max_open_conns: 100
|
||||
conn_max_lifetime: 3600
|
||||
auto_migrate: true
|
||||
|
||||
auth:
|
||||
jwt:
|
||||
secret: "license-manager-dev-secret-key-32-bytes"
|
||||
expire_hours: 1
|
||||
refresh_threshold_minutes: 30
|
||||
admin:
|
||||
username: "admin"
|
||||
password: "admin@123"
|
||||
security:
|
||||
max_login_attempts: 5
|
||||
lockout_duration_minutes: 15
|
||||
|
||||
log:
|
||||
level: "info"
|
||||
format: "text"
|
||||
|
||||
i18n:
|
||||
enable: true
|
||||
default_lang: "zh-CN"
|
||||
config_path: "../configs/i18n/errors"
|
||||
support_langs: ["zh-CN", "en-US", "ja-JP"]
|
||||
|
||||
|
||||
@@ -1,30 +1,42 @@
|
||||
# 自动部署使用
|
||||
server:
|
||||
host: "0.0.0.0"
|
||||
port: 8080
|
||||
mode: "release" # debug, release, test
|
||||
port: 18888
|
||||
mode: "debug" # debug, release, test 开发环境使用debug模式
|
||||
|
||||
database:
|
||||
driver: "mysql"
|
||||
host: "127.0.0.1"
|
||||
port: 13306
|
||||
username: "root"
|
||||
password: "root@123"
|
||||
database: "license_manager"
|
||||
charset: "utf8mb4"
|
||||
parse_time: true
|
||||
loc: "Local"
|
||||
max_idle_conns: 10
|
||||
max_open_conns: 100
|
||||
conn_max_lifetime: 3600 # 连接最大生存时间(秒)
|
||||
auto_migrate: true # 是否自动迁移数据库
|
||||
|
||||
auth:
|
||||
jwt:
|
||||
secret: "your-256-bit-secret-key-change-this-in-production"
|
||||
expire_hours: 1
|
||||
refresh_threshold_minutes: 30
|
||||
secret: "license-manager-dev-secret-key-32-bytes" # 32字节的随机字符串
|
||||
expire_hours: 1 # 1小时后过期
|
||||
refresh_threshold_minutes: 30 # 30分钟后可刷新token
|
||||
admin:
|
||||
username: "admin"
|
||||
password: "admin@123" # 明文密码,部署人员可修改
|
||||
password: "admin@123"
|
||||
security:
|
||||
max_login_attempts: 5
|
||||
lockout_duration_minutes: 15
|
||||
max_login_attempts: 5 # 5次失败后锁定15分钟
|
||||
lockout_duration_minutes: 15 # 15分钟后解锁
|
||||
|
||||
log:
|
||||
level: "info" # debug, info, warn, error
|
||||
format: "json" # json, text
|
||||
level: "info" # 日志级别
|
||||
format: "text" # 日志格式
|
||||
|
||||
# 数据库配置(暂未使用)
|
||||
database:
|
||||
type: "postgres" # postgres or mysql
|
||||
host: "localhost"
|
||||
port: 5432
|
||||
name: "license_manager"
|
||||
user: "postgres"
|
||||
password: "password"
|
||||
ssl_mode: "disable"
|
||||
i18n:
|
||||
enable: true # 是否启用多语言支持
|
||||
default_lang: "zh-CN" # 默认语言
|
||||
config_path: "../configs/i18n/errors" # 语言包文件路径(相对于backend目录)
|
||||
support_langs: ["zh-CN", "en-US", "ja-JP"] # 支持的语言列表
|
||||
@@ -0,0 +1,43 @@
|
||||
server:
|
||||
host: "0.0.0.0"
|
||||
port: 18888
|
||||
mode: "release"
|
||||
|
||||
database:
|
||||
driver: "mysql"
|
||||
host: "mysql"
|
||||
port: 3306
|
||||
username: "license_user"
|
||||
password: "license@123"
|
||||
database: "license_manager"
|
||||
charset: "utf8mb4"
|
||||
parse_time: true
|
||||
loc: "Local"
|
||||
max_idle_conns: 50
|
||||
max_open_conns: 300
|
||||
conn_max_lifetime: 3600
|
||||
auto_migrate: true
|
||||
|
||||
auth:
|
||||
jwt:
|
||||
secret: "please-change-in-prod"
|
||||
expire_hours: 1
|
||||
refresh_threshold_minutes: 30
|
||||
admin:
|
||||
username: "admin"
|
||||
password: "changeMe!"
|
||||
security:
|
||||
max_login_attempts: 5
|
||||
lockout_duration_minutes: 15
|
||||
|
||||
log:
|
||||
level: "info"
|
||||
format: "json"
|
||||
|
||||
i18n:
|
||||
enable: true
|
||||
default_lang: "zh-CN"
|
||||
config_path: "../configs/i18n/errors"
|
||||
support_langs: ["zh-CN", "en-US", "ja-JP"]
|
||||
|
||||
|
||||
@@ -3,6 +3,21 @@ server:
|
||||
port: 18888
|
||||
mode: "debug" # debug, release, test 开发环境使用debug模式
|
||||
|
||||
database:
|
||||
driver: "mysql"
|
||||
host: "104.156.140.42"
|
||||
port: 13306
|
||||
username: "root"
|
||||
password: "root@123"
|
||||
database: "license_manager"
|
||||
charset: "utf8mb4"
|
||||
parse_time: true
|
||||
loc: "Local"
|
||||
max_idle_conns: 10
|
||||
max_open_conns: 100
|
||||
conn_max_lifetime: 3600 # 连接最大生存时间(秒)
|
||||
auto_migrate: true # 是否自动迁移数据库
|
||||
|
||||
auth:
|
||||
jwt:
|
||||
secret: "license-manager-dev-secret-key-32-bytes" # 32字节的随机字符串
|
||||
@@ -17,4 +32,10 @@ auth:
|
||||
|
||||
log:
|
||||
level: "info" # 日志级别
|
||||
format: "text" # 日志格式
|
||||
format: "text" # 日志格式
|
||||
|
||||
i18n:
|
||||
enable: true # 是否启用多语言支持
|
||||
default_lang: "zh-CN" # 默认语言
|
||||
config_path: "../configs/i18n/errors" # 语言包文件路径(相对于backend目录)
|
||||
support_langs: ["zh-CN", "en-US", "ja-JP"] # 支持的语言列表
|
||||
@@ -0,0 +1,39 @@
|
||||
system:
|
||||
name: "English"
|
||||
locale: "en-US"
|
||||
|
||||
errors:
|
||||
# Success status
|
||||
"000000": "Success"
|
||||
|
||||
# Authentication module (10xxxx)
|
||||
auth:
|
||||
"100001": "Authentication expired"
|
||||
"100002": "Invalid authentication"
|
||||
"100003": "Invalid username or password"
|
||||
"100004": "Authentication required"
|
||||
"100005": "Insufficient permissions"
|
||||
|
||||
# Customer module (20xxxx) - Reserved
|
||||
customer:
|
||||
"200001": "Customer not found"
|
||||
"200002": "Customer already exists"
|
||||
"200003": "Customer name is required"
|
||||
"200004": "Invalid customer information format"
|
||||
|
||||
# License module (30xxxx) - Reserved
|
||||
license:
|
||||
"300001": "Invalid license key"
|
||||
"300002": "License expired"
|
||||
"300003": "License key is required"
|
||||
"300004": "License key already used"
|
||||
|
||||
# System general (90xxxx)
|
||||
system:
|
||||
"900001": "Invalid request parameters"
|
||||
"900002": "Resource not found"
|
||||
"900003": "Resource conflict"
|
||||
"900004": "Internal server error"
|
||||
|
||||
# Default error message
|
||||
default_error: "Unknown error"
|
||||
@@ -0,0 +1,39 @@
|
||||
system:
|
||||
name: "日本語"
|
||||
locale: "ja-JP"
|
||||
|
||||
errors:
|
||||
# 成功状態
|
||||
"000000": "成功"
|
||||
|
||||
# 認証モジュール (10xxxx)
|
||||
auth:
|
||||
"100001": "認証が期限切れです"
|
||||
"100002": "無効な認証"
|
||||
"100003": "ユーザー名またはパスワードが無効です"
|
||||
"100004": "認証が必要です"
|
||||
"100005": "権限が不足しています"
|
||||
|
||||
# 顧客モジュール (20xxxx) - 予約済み
|
||||
customer:
|
||||
"200001": "顧客が見つかりません"
|
||||
"200002": "顧客が既に存在します"
|
||||
"200003": "顧客名は必須です"
|
||||
"200004": "無効な顧客情報形式"
|
||||
|
||||
# ライセンスモジュール (30xxxx) - 予約済み
|
||||
license:
|
||||
"300001": "無効なライセンスキー"
|
||||
"300002": "ライセンスが期限切れです"
|
||||
"300003": "ライセンスキーは必須です"
|
||||
"300004": "ライセンスキーは既に使用されています"
|
||||
|
||||
# システム共通 (90xxxx)
|
||||
system:
|
||||
"900001": "無効なリクエストパラメータ"
|
||||
"900002": "リソースが見つかりません"
|
||||
"900003": "リソースの競合"
|
||||
"900004": "内部サーバーエラー"
|
||||
|
||||
# デフォルトエラーメッセージ
|
||||
default_error: "不明なエラー"
|
||||
@@ -0,0 +1,39 @@
|
||||
system:
|
||||
name: "中文"
|
||||
locale: "zh-CN"
|
||||
|
||||
errors:
|
||||
# 成功状态
|
||||
"000000": "成功"
|
||||
|
||||
# 认证模块 (10xxxx)
|
||||
auth:
|
||||
"100001": "认证已过期"
|
||||
"100002": "认证无效"
|
||||
"100003": "用户名或密码错误"
|
||||
"100004": "缺少认证信息"
|
||||
"100005": "权限不足"
|
||||
|
||||
# 客户模块 (20xxxx) - 预留
|
||||
customer:
|
||||
"200001": "客户不存在"
|
||||
"200002": "客户已存在"
|
||||
"200003": "客户名称不能为空"
|
||||
"200004": "客户信息格式无效"
|
||||
|
||||
# 授权模块 (30xxxx) - 预留
|
||||
license:
|
||||
"300001": "授权码无效"
|
||||
"300002": "授权已过期"
|
||||
"300003": "授权码不能为空"
|
||||
"300004": "授权已被使用"
|
||||
|
||||
# 系统通用 (90xxxx)
|
||||
system:
|
||||
"900001": "请求参数无效"
|
||||
"900002": "资源不存在"
|
||||
"900003": "资源冲突"
|
||||
"900004": "服务器内部错误"
|
||||
|
||||
# 默认错误信息
|
||||
default_error: "未知错误"
|
||||
@@ -1,35 +1,50 @@
|
||||
# API开发规范
|
||||
# API 开发规范
|
||||
|
||||
## 设计原则
|
||||
|
||||
遵循系统设计理念,通过代码结构和命名体现设计思路:
|
||||
|
||||
- **分层架构**:明确职责边界,保持高内聚低耦合
|
||||
- **代码即文档**:通过良好的命名和结构让代码自己说话
|
||||
- **渐进式复杂度**:从简单到复杂的实现路径
|
||||
- **失败快速原则**:早期验证,明确错误
|
||||
|
||||
## 目录结构规范
|
||||
|
||||
### 分层架构
|
||||
|
||||
```
|
||||
internal/
|
||||
├── models/ # 数据模型层
|
||||
├── service/ # 业务逻辑层
|
||||
├── repository/ # 数据访问层
|
||||
└── api/handlers/ # HTTP处理层
|
||||
├── models/ # 数据模型层:结构体定义、验证规则
|
||||
├── service/ # 业务逻辑层:核心业务规则实现
|
||||
├── repository/ # 数据访问层:数据库、缓存操作
|
||||
└── api/handlers/ # HTTP处理层:请求处理、响应格式化
|
||||
```
|
||||
|
||||
### 职责划分
|
||||
- **Models**: 定义请求/响应结构体,数据验证规则
|
||||
- **Service**: 实现业务逻辑,处理业务规则
|
||||
- **Repository**: 数据库操作,缓存操作
|
||||
- **Handler**: HTTP请求处理,参数绑定,响应格式化
|
||||
### 职责边界
|
||||
|
||||
## 文件命名规范
|
||||
每层专注单一职责,避免跨层调用:
|
||||
|
||||
- **Models**:数据结构定义,输入验证规则,响应格式规范
|
||||
- **Service**:业务规则实现,数据处理逻辑,跨模块协调
|
||||
- **Repository**:数据持久化,查询优化,事务管理
|
||||
- **Handler**:HTTP协议处理,参数绑定,错误响应
|
||||
|
||||
## 文件组织规范
|
||||
|
||||
### 模块化设计
|
||||
|
||||
按业务领域组织文件,体现清晰的模块边界:
|
||||
|
||||
### 模块分组
|
||||
按业务模块组织文件:
|
||||
```
|
||||
models/
|
||||
├── auth.go # 认证相关模型
|
||||
├── customer.go # 客户相关模型
|
||||
├── license.go # 授权相关模型
|
||||
└── common.go # 通用模型
|
||||
├── auth.go # 认证领域:登录、Token、权限
|
||||
├── customer.go # 客户领域:客户信息、关系管理
|
||||
├── license.go # 授权领域:许可证、授权码
|
||||
└── common.go # 通用结构:响应格式、分页
|
||||
|
||||
service/
|
||||
├── interfaces.go # 服务接口定义
|
||||
├── interfaces.go # 接口契约:定义服务边界
|
||||
├── auth_service.go
|
||||
├── customer_service.go
|
||||
└── license_service.go
|
||||
@@ -40,226 +55,142 @@ api/handlers/
|
||||
└── license_handler.go
|
||||
```
|
||||
|
||||
## 代码结构规范
|
||||
## 代码组织模式
|
||||
|
||||
### 1. 模型定义 (models/)
|
||||
```go
|
||||
// 请求模型
|
||||
type CreateCustomerRequest struct {
|
||||
Name string `json:"name" binding:"required"`
|
||||
Email string `json:"email" binding:"required,email"`
|
||||
Company string `json:"company"`
|
||||
}
|
||||
### 模型层设计要点
|
||||
|
||||
// 响应模型
|
||||
type CustomerResponse struct {
|
||||
ID int `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Email string `json:"email"`
|
||||
Company string `json:"company"`
|
||||
}
|
||||
```
|
||||
- 请求结构体:清晰的字段验证规则,语义化的字段命名
|
||||
- 响应结构体:统一的数据格式,合理的字段层次
|
||||
- 验证标签使用:利用 binding 标签进行输入验证
|
||||
- 文档标签:通过 json 标签明确 API 契约
|
||||
|
||||
### 2. 服务接口 (service/interfaces.go)
|
||||
```go
|
||||
type CustomerService interface {
|
||||
CreateCustomer(req *models.CreateCustomerRequest) (*models.CustomerResponse, error)
|
||||
GetCustomer(id int) (*models.CustomerResponse, error)
|
||||
UpdateCustomer(id int, req *models.UpdateCustomerRequest) error
|
||||
DeleteCustomer(id int) error
|
||||
}
|
||||
```
|
||||
### 服务层设计要点
|
||||
|
||||
### 3. 服务实现 (service/)
|
||||
```go
|
||||
type customerService struct {
|
||||
repo repository.CustomerRepository
|
||||
}
|
||||
- 接口优先:先定义契约,后实现逻辑
|
||||
- 依赖注入:通过构造函数注入依赖
|
||||
- 错误处理:明确的错误类型和信息
|
||||
- 业务封装:核心逻辑集中在服务层
|
||||
|
||||
func NewCustomerService(repo repository.CustomerRepository) CustomerService {
|
||||
return &customerService{repo: repo}
|
||||
}
|
||||
### 处理器层设计要点
|
||||
|
||||
func (s *customerService) CreateCustomer(req *models.CreateCustomerRequest) (*models.CustomerResponse, error) {
|
||||
// 业务逻辑处理
|
||||
// 数据验证
|
||||
// 调用repository
|
||||
}
|
||||
```
|
||||
- 职责单一:只处理 HTTP 协议相关逻辑
|
||||
- 参数绑定:统一使用框架提供的绑定机制
|
||||
- 错误响应:使用统一的错误处理函数
|
||||
- 文档注解:完整的 Swagger 注解信息
|
||||
|
||||
### 4. 处理器实现 (api/handlers/)
|
||||
```go
|
||||
type CustomerHandler struct {
|
||||
service service.CustomerService
|
||||
}
|
||||
## REST API 设计
|
||||
|
||||
func NewCustomerHandler(service service.CustomerService) *CustomerHandler {
|
||||
return &CustomerHandler{service: service}
|
||||
}
|
||||
### 路由设计原则
|
||||
|
||||
// @Summary 创建客户
|
||||
// @Tags 客户管理
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body models.CreateCustomerRequest true "客户信息"
|
||||
// @Success 200 {object} models.APIResponse
|
||||
// @Router /api/v1/customers [post]
|
||||
func (h *CustomerHandler) CreateCustomer(c *gin.Context) {
|
||||
var req models.CreateCustomerRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, models.ErrorResponse{
|
||||
Code: http.StatusBadRequest,
|
||||
Error: "INVALID_REQUEST",
|
||||
Message: "请求参数无效",
|
||||
})
|
||||
return
|
||||
}
|
||||
遵循 RESTful 风格,资源导向的 URL 设计:
|
||||
|
||||
result, err := h.service.CreateCustomer(&req)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.ErrorResponse{
|
||||
Code: http.StatusInternalServerError,
|
||||
Error: "CREATE_FAILED",
|
||||
Message: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
- 使用名词表示资源,动词通过 HTTP 方法体现
|
||||
- 嵌套路由体现资源关系
|
||||
- 版本控制通过路径前缀实现
|
||||
- 统一的路径命名约定
|
||||
|
||||
c.JSON(http.StatusOK, models.APIResponse{
|
||||
Code: http.StatusOK,
|
||||
Message: "创建成功",
|
||||
Data: result,
|
||||
})
|
||||
}
|
||||
```
|
||||
### 响应格式标准化
|
||||
|
||||
## API设计规范
|
||||
**成功响应**:统一的数据结构,包含状态码、消息、数据
|
||||
**错误响应**:详细的错误信息,包含错误码、消息、时间戳
|
||||
**分页响应**:包含分页元信息的数据结构
|
||||
|
||||
### 1. 路由设计
|
||||
```
|
||||
GET /api/v1/customers # 获取客户列表
|
||||
POST /api/v1/customers # 创建客户
|
||||
GET /api/v1/customers/:id # 获取客户详情
|
||||
PUT /api/v1/customers/:id # 更新客户
|
||||
DELETE /api/v1/customers/:id # 删除客户
|
||||
```
|
||||
### HTTP 状态码使用策略
|
||||
|
||||
### 2. 响应格式
|
||||
```go
|
||||
// 成功响应
|
||||
type APIResponse struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Data interface{} `json:"data,omitempty"`
|
||||
}
|
||||
遵循设计理念,大部分业务错误返回 200,通过响应体区分状态:
|
||||
|
||||
// 错误响应
|
||||
type ErrorResponse struct {
|
||||
Code int `json:"code"`
|
||||
Error string `json:"error"`
|
||||
Message string `json:"message"`
|
||||
Timestamp string `json:"timestamp"`
|
||||
}
|
||||
```
|
||||
- **200**:成功(包括业务逻辑失败)
|
||||
- **400**:请求格式错误
|
||||
- **401**:身份认证失败
|
||||
- **403**:权限不足
|
||||
- **404**:资源不存在
|
||||
- **500**:服务器内部错误
|
||||
|
||||
### 3. HTTP状态码
|
||||
- `200` - 请求成功
|
||||
- `201` - 创建成功
|
||||
- `400` - 请求参数错误
|
||||
- `401` - 未认证
|
||||
- `403` - 权限不足
|
||||
- `404` - 资源不存在
|
||||
- `500` - 服务器内部错误
|
||||
## API 文档规范
|
||||
|
||||
## Swagger文档规范
|
||||
### Swagger 注解要求
|
||||
|
||||
### 必需注解
|
||||
```go
|
||||
// @Summary 接口简要描述
|
||||
// @Description 接口详细描述
|
||||
// @Tags 标签分组
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param 参数名 参数位置 参数类型 是否必需 "参数描述"
|
||||
// @Success 状态码 {object} 响应类型 "成功描述"
|
||||
// @Failure 状态码 {object} 错误类型 "失败描述"
|
||||
// @Security BearerAuth (需要认证的接口)
|
||||
// @Router 路由路径 [HTTP方法]
|
||||
```
|
||||
完整的接口文档注解,包含所有必要信息:
|
||||
|
||||
### 标签分组
|
||||
- `认证` - 登录、登出、Token相关
|
||||
- `客户管理` - 客户CRUD操作
|
||||
- `授权管理` - 授权码、许可证相关
|
||||
- `系统` - 系统信息、健康检测
|
||||
- **Summary**:简洁的接口描述
|
||||
- **Description**:详细的功能说明
|
||||
- **Tags**:合理的分组标签
|
||||
- **Parameters**:完整的参数说明
|
||||
- **Responses**:所有可能的响应状态
|
||||
- **Security**:认证要求说明
|
||||
|
||||
## 错误处理规范
|
||||
### 文档分组策略
|
||||
|
||||
### 错误码定义
|
||||
```go
|
||||
const (
|
||||
// 通用错误
|
||||
ErrInvalidRequest = "INVALID_REQUEST"
|
||||
ErrUnauthorized = "UNAUTHORIZED"
|
||||
ErrForbidden = "FORBIDDEN"
|
||||
ErrNotFound = "NOT_FOUND"
|
||||
ErrInternalError = "INTERNAL_ERROR"
|
||||
|
||||
// 业务错误
|
||||
ErrCustomerExists = "CUSTOMER_EXISTS"
|
||||
ErrLicenseExpired = "LICENSE_EXPIRED"
|
||||
ErrInvalidLicense = "INVALID_LICENSE"
|
||||
)
|
||||
```
|
||||
按业务领域组织接口文档:
|
||||
|
||||
### 统一错误处理
|
||||
```go
|
||||
func handleError(c *gin.Context, err error) {
|
||||
var statusCode int
|
||||
var errorCode string
|
||||
|
||||
switch err {
|
||||
case ErrCustomerExists:
|
||||
statusCode = http.StatusConflict
|
||||
errorCode = "CUSTOMER_EXISTS"
|
||||
default:
|
||||
statusCode = http.StatusInternalServerError
|
||||
errorCode = "INTERNAL_ERROR"
|
||||
}
|
||||
|
||||
c.JSON(statusCode, models.ErrorResponse{
|
||||
Code: statusCode,
|
||||
Error: errorCode,
|
||||
Message: err.Error(),
|
||||
Timestamp: time.Now().Format(time.RFC3339),
|
||||
})
|
||||
}
|
||||
```
|
||||
- **认证管理**:登录、登出、Token 刷新
|
||||
- **客户管理**:客户信息 CRUD 操作
|
||||
- **授权管理**:许可证、授权码管理
|
||||
- **系统监控**:健康检测、系统信息
|
||||
|
||||
## 开发流程
|
||||
## 错误处理体系
|
||||
|
||||
### 1. 新增API步骤
|
||||
1. 在 `models/` 定义请求/响应结构体
|
||||
2. 在 `service/interfaces.go` 添加服务接口
|
||||
3. 在 `service/` 实现业务逻辑
|
||||
4. 在 `handlers/` 实现HTTP处理器
|
||||
5. 在 `routes/router.go` 注册路由
|
||||
6. 添加Swagger注解
|
||||
7. 生成文档:`swag init -g cmd/main.go -o ./docs/swagger`
|
||||
### 错误码设计
|
||||
|
||||
### 2. 代码检查清单
|
||||
- [ ] 模型定义是否合理
|
||||
- [ ] 业务逻辑是否在service层
|
||||
- [ ] 错误处理是否完整
|
||||
- [ ] Swagger注解是否完整
|
||||
- [ ] 路由是否正确注册
|
||||
- [ ] HTTP状态码是否合适
|
||||
采用六位数字业务错误码,体现模块化设计:
|
||||
|
||||
## 最佳实践
|
||||
- **前2位**:模块代码(10=认证,20=客户,30=授权,90=系统)
|
||||
- **后4位**:连续错误号,无需分层
|
||||
- **直接映射**:错误码到错误信息的直接对应关系
|
||||
|
||||
1. **职责单一**:每个函数只做一件事
|
||||
2. **接口优先**:先定义接口,再实现
|
||||
3. **错误优雅**:统一错误处理和响应格式
|
||||
4. **文档完整**:所有API都要有Swagger文档
|
||||
5. **命名规范**:使用有意义的命名
|
||||
6. **代码复用**:提取公共逻辑到utils包
|
||||
### 错误响应策略
|
||||
|
||||
**失败快速原则**:早期验证,明确提示
|
||||
**统一处理**:使用 NewErrorResponse 函数统一创建错误响应
|
||||
**分层错误**:Service 层返回业务错误,Handler 层转换为 HTTP 响应
|
||||
|
||||
## 开发工作流
|
||||
|
||||
### API 开发步骤
|
||||
|
||||
遵循分层架构,自下而上的开发顺序:
|
||||
|
||||
1. **模型设计**:定义请求响应结构,明确数据契约
|
||||
2. **接口定义**:在 interfaces.go 中声明服务契约
|
||||
3. **业务实现**:在 service 层实现核心逻辑
|
||||
4. **处理器编写**:在 handlers 层处理 HTTP 协议
|
||||
5. **路由注册**:在 router.go 中注册 API 路由
|
||||
6. **文档完善**:添加完整的 Swagger 注解
|
||||
7. **文档生成**:更新 API 文档
|
||||
|
||||
### 质量检查要点
|
||||
|
||||
**架构层面**:
|
||||
- 分层职责是否清晰
|
||||
- 依赖关系是否合理
|
||||
- 接口设计是否完整
|
||||
|
||||
**实现层面**:
|
||||
- 错误处理是否完整
|
||||
- 输入验证是否充分
|
||||
- 响应格式是否统一
|
||||
|
||||
**文档层面**:
|
||||
- Swagger 注解是否完整
|
||||
- 接口描述是否清晰
|
||||
- 错误码是否正确
|
||||
|
||||
## 实施准则
|
||||
|
||||
### 整洁代码
|
||||
|
||||
- **命名语义化**:函数、变量名体现其用途
|
||||
- **函数职责单一**:每个函数专注一个职责
|
||||
- **避免深层嵌套**:使用早期返回减少复杂度
|
||||
|
||||
### 可维护性
|
||||
|
||||
- **接口优先设计**:先定义契约再实现
|
||||
- **依赖注入模式**:便于测试和扩展
|
||||
- **统一错误处理**:减少重复代码
|
||||
|
||||
### 渐进式复杂度
|
||||
|
||||
- **基础功能优先**:先实现核心功能
|
||||
- **逐步增强**:根据需求添加高级特性
|
||||
- **配置驱动**:通过配置控制功能开关
|
||||
@@ -3,12 +3,13 @@
|
||||
## 技术栈选择
|
||||
|
||||
Web框架: Gin
|
||||
ORM: GORM
|
||||
ORM: GORM,开发环境使用Auto Migration
|
||||
配置: Viper
|
||||
日志: logrus
|
||||
认证: JWT
|
||||
缓存: Redis (可选)
|
||||
数据库: PostgreSQL/MySQL
|
||||
|
||||
|
||||
## 目录结构
|
||||
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
# 系统设计理念
|
||||
|
||||
## 代码即文档(Code as Documentation)
|
||||
|
||||
通过代码本身传达设计意图,减少文档维护负担:
|
||||
|
||||
- **语义化命名**:函数、变量、常量名称体现其用途和含义
|
||||
- **结构化组织**:目录结构和文件组织反映业务模型
|
||||
- **内联说明**:关键逻辑通过简洁注释说明设计思路
|
||||
- **自解释代码**:优先写出无需注释就能理解的代码
|
||||
|
||||
## 分层架构(Layered Architecture)
|
||||
|
||||
明确的职责边界,保持系统的可维护性:
|
||||
|
||||
- **模型层(Models)**:数据结构定义、验证规则、业务实体
|
||||
- **服务层(Service)**:核心业务逻辑、跨领域协调、规则实现
|
||||
- **数据层(Repository)**:数据持久化、查询优化、事务管理
|
||||
- **接口层(Handlers)**:HTTP协议处理、参数绑定、响应格式化
|
||||
|
||||
**设计原则**:上层依赖下层,避免跨层调用,通过接口解耦
|
||||
|
||||
## 整洁代码(Clean Code)
|
||||
|
||||
追求代码的可读性和表达力:
|
||||
|
||||
- **单一职责**:每个函数、类只负责一个明确的职责
|
||||
- **函数简洁**:函数体保持简短,易于理解和测试
|
||||
- **避免重复**:提取公共逻辑,提高代码复用性
|
||||
- **有意义的抽象**:合理使用抽象层次,不过度设计
|
||||
|
||||
## 渐进式复杂度(Progressive Complexity)
|
||||
|
||||
从简单到复杂的演进路径:
|
||||
|
||||
- **开箱即用**:系统默认配置即可运行,降低上手门槛
|
||||
- **按需扩展**:高级功能通过配置启用,不影响基础使用
|
||||
- **平缓学习**:功能按复杂度分层,用户可逐步深入
|
||||
- **向后兼容**:新功能不破坏现有接口和行为
|
||||
|
||||
## 失败快速原则(Fail Fast)
|
||||
|
||||
及早发现问题,提供明确反馈:
|
||||
|
||||
- **启动验证**:系统启动时检查配置完整性和依赖可用性
|
||||
- **输入校验**:在数据处理早期进行格式和业务规则验证
|
||||
- **明确错误**:提供具体的错误信息和解决建议
|
||||
- **优雅降级**:局部故障不影响整体服务可用性
|
||||
|
||||
## 可维护性设计(Maintainable Design)
|
||||
|
||||
面向长期演进的架构设计:
|
||||
|
||||
- **接口优先**:通过接口定义契约,降低模块间耦合
|
||||
- **依赖注入**:便于测试替换和功能扩展
|
||||
- **配置外化**:运行时行为通过配置文件控制
|
||||
- **模块化组织**:按业务领域组织代码,便于团队协作
|
||||
|
||||
@@ -0,0 +1,342 @@
|
||||
# 错误信息多语言设计方案
|
||||
|
||||
## 设计理念
|
||||
|
||||
基于系统设计理念,构建简洁、易用、可维护的多语言错误信息响应系统:
|
||||
|
||||
- **配置驱动**:语言包通过配置文件管理,支持热更新
|
||||
- **代码即文档**:通过结构化的语言文件体现错误信息组织
|
||||
- **渐进式复杂度**:从单语言平滑升级到多语言支持
|
||||
- **失败快速原则**:语言缺失时优雅降级到默认语言
|
||||
|
||||
## 整体架构设计
|
||||
|
||||
### 1. 目录结构
|
||||
|
||||
```
|
||||
backend/
|
||||
├── configs/
|
||||
│ └── i18n/ # 多语言配置目录
|
||||
│ ├── errors/ # 错误信息语言包
|
||||
│ │ ├── zh-CN.yaml # 中文错误信息
|
||||
│ │ ├── en-US.yaml # 英文错误信息
|
||||
│ │ └── ja-JP.yaml # 日文错误信息
|
||||
│ └── messages/ # 其他信息语言包(可选扩展)
|
||||
├── pkg/
|
||||
│ └── i18n/ # 国际化工具包
|
||||
│ ├── manager.go # 多语言管理器
|
||||
│ └── errors.go # 错误信息国际化封装
|
||||
```
|
||||
|
||||
### 2. 核心设计原则
|
||||
|
||||
- **单一职责**:i18n 包专门处理多语言逻辑
|
||||
- **接口解耦**:通过接口与现有错误系统集成
|
||||
- **配置外化**:所有语言文件独立管理
|
||||
- **缓存优化**:语言包加载后内存缓存
|
||||
|
||||
## 语言包配置格式
|
||||
|
||||
### YAML 配置结构
|
||||
|
||||
错误信息按模块组织,保持与现有错误码体系一致:
|
||||
|
||||
```yaml
|
||||
# configs/i18n/errors/zh-CN.yaml
|
||||
system:
|
||||
name: "中文"
|
||||
locale: "zh-CN"
|
||||
|
||||
errors:
|
||||
# 成功状态
|
||||
"000000": "成功"
|
||||
|
||||
# 认证模块 (10xxxx)
|
||||
auth:
|
||||
"100001": "认证已过期"
|
||||
"100002": "认证无效"
|
||||
"100003": "用户名或密码错误"
|
||||
"100004": "缺少认证信息"
|
||||
"100005": "权限不足"
|
||||
|
||||
# 客户模块 (20xxxx) - 预留
|
||||
customer:
|
||||
"200001": "客户不存在"
|
||||
"200002": "客户已存在"
|
||||
|
||||
# 授权模块 (30xxxx) - 预留
|
||||
license:
|
||||
"300001": "授权码无效"
|
||||
"300002": "授权已过期"
|
||||
|
||||
# 系统通用 (90xxxx)
|
||||
system:
|
||||
"900001": "请求参数无效"
|
||||
"900002": "资源不存在"
|
||||
"900003": "资源冲突"
|
||||
"900004": "服务器内部错误"
|
||||
|
||||
# 默认错误信息
|
||||
default_error: "未知错误"
|
||||
```
|
||||
|
||||
```yaml
|
||||
# configs/i18n/errors/en-US.yaml
|
||||
system:
|
||||
name: "English"
|
||||
locale: "en-US"
|
||||
|
||||
errors:
|
||||
"000000": "Success"
|
||||
|
||||
auth:
|
||||
"100001": "Authentication expired"
|
||||
"100002": "Invalid authentication"
|
||||
"100003": "Invalid username or password"
|
||||
"100004": "Authentication required"
|
||||
"100005": "Insufficient permissions"
|
||||
|
||||
customer:
|
||||
"200001": "Customer not found"
|
||||
"200002": "Customer already exists"
|
||||
|
||||
license:
|
||||
"300001": "Invalid license key"
|
||||
"300002": "License expired"
|
||||
|
||||
system:
|
||||
"900001": "Invalid request parameters"
|
||||
"900002": "Resource not found"
|
||||
"900003": "Resource conflict"
|
||||
"900004": "Internal server error"
|
||||
|
||||
default_error: "Unknown error"
|
||||
```
|
||||
|
||||
## 核心组件设计
|
||||
|
||||
### 1. 多语言管理器
|
||||
|
||||
```go
|
||||
// pkg/i18n/manager.go
|
||||
type Manager struct {
|
||||
defaultLang string
|
||||
loadedLangs map[string]*LanguageData
|
||||
configPath string
|
||||
}
|
||||
|
||||
type LanguageData struct {
|
||||
System SystemInfo `yaml:"system"`
|
||||
Errors map[string]string `yaml:"errors"`
|
||||
DefaultError string `yaml:"default_error"`
|
||||
}
|
||||
|
||||
type SystemInfo struct {
|
||||
Name string `yaml:"name"`
|
||||
Locale string `yaml:"locale"`
|
||||
}
|
||||
|
||||
// 核心接口
|
||||
type I18nManager interface {
|
||||
LoadLanguage(lang string) error
|
||||
GetErrorMessage(code, lang string) string
|
||||
SupportedLanguages() []string
|
||||
SetDefaultLanguage(lang string)
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 错误信息国际化封装
|
||||
|
||||
```go
|
||||
// pkg/i18n/errors.go
|
||||
type ErrorI18n struct {
|
||||
manager I18nManager
|
||||
}
|
||||
|
||||
// 扩展现有错误响应函数
|
||||
func NewI18nErrorResponse(code, lang string, customMessage ...string) (int, string, string) {
|
||||
// 获取本地化错误信息
|
||||
message := GetI18nErrorMessage(code, lang)
|
||||
|
||||
// 支持自定义消息覆盖
|
||||
if len(customMessage) > 0 && customMessage[0] != "" {
|
||||
message = customMessage[0]
|
||||
}
|
||||
|
||||
httpStatus := getHTTPStatusByCode(code)
|
||||
return httpStatus, code, message
|
||||
}
|
||||
```
|
||||
|
||||
## 语言检测策略
|
||||
|
||||
### 1. 多层级语言检测
|
||||
|
||||
按优先级顺序检测客户端语言偏好:
|
||||
|
||||
1. **URL 参数**:`?lang=en-US` (最高优先级)
|
||||
2. **HTTP Header**:`Accept-Language: zh-CN,zh;q=0.9,en;q=0.8`
|
||||
3. **用户配置**:数据库存储的用户语言偏好
|
||||
4. **系统默认**:配置文件指定的默认语言
|
||||
|
||||
### 2. 语言检测中间件
|
||||
|
||||
```go
|
||||
// internal/api/middleware/i18n.go
|
||||
func I18nMiddleware() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
lang := detectLanguage(c)
|
||||
c.Set("language", lang)
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
func detectLanguage(c *gin.Context) string {
|
||||
// 1. URL 参数检测
|
||||
if lang := c.Query("lang"); lang != "" {
|
||||
return lang
|
||||
}
|
||||
|
||||
// 2. Accept-Language 头检测
|
||||
if lang := parseAcceptLanguage(c.GetHeader("Accept-Language")); lang != "" {
|
||||
return lang
|
||||
}
|
||||
|
||||
// 3. 返回默认语言
|
||||
return config.DefaultLanguage
|
||||
}
|
||||
```
|
||||
|
||||
## 集成实现方案
|
||||
|
||||
### 1. 配置系统集成
|
||||
|
||||
扩展现有配置结构,添加国际化配置:
|
||||
|
||||
```go
|
||||
// internal/config/config.go
|
||||
type Config struct {
|
||||
// ... 现有配置
|
||||
|
||||
I18n I18nConfig `yaml:"i18n"`
|
||||
}
|
||||
|
||||
type I18nConfig struct {
|
||||
Enable bool `yaml:"enable"` // 是否启用多语言
|
||||
DefaultLang string `yaml:"default_lang"` // 默认语言
|
||||
ConfigPath string `yaml:"config_path"` // 语言包路径
|
||||
SupportLangs []string `yaml:"support_langs"` // 支持的语言列表
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Handler 层集成
|
||||
|
||||
修改现有错误处理流程,支持多语言响应:
|
||||
|
||||
```go
|
||||
// 使用示例
|
||||
func (h *AuthHandler) Login(c *gin.Context) {
|
||||
// ... 业务逻辑
|
||||
|
||||
if err != nil {
|
||||
// 获取客户端语言
|
||||
lang := c.GetString("language")
|
||||
|
||||
// 返回本地化错误响应
|
||||
httpStatus, code, message := i18n.NewI18nErrorResponse("100003", lang)
|
||||
c.JSON(httpStatus, models.ErrorResponse{
|
||||
Code: httpStatus,
|
||||
Error: code,
|
||||
Message: message,
|
||||
Timestamp: time.Now().Format(time.RFC3339),
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 性能优化策略
|
||||
|
||||
### 1. 缓存机制
|
||||
|
||||
- **启动加载**:应用启动时预加载常用语言包
|
||||
- **内存缓存**:语言包加载后常驻内存
|
||||
- **懒加载**:按需加载非常用语言包
|
||||
|
||||
### 2. 降级策略
|
||||
|
||||
- **语言缺失降级**:请求语言不存在时降级到默认语言
|
||||
- **错误码缺失降级**:特定错误码翻译缺失时使用通用错误信息
|
||||
- **完全降级**:多语言系统异常时回退到原有单语言模式
|
||||
|
||||
## 开发和维护流程
|
||||
|
||||
### 1. 语言包维护
|
||||
|
||||
- **版本控制**:语言文件纳入 Git 管理
|
||||
- **格式校验**:CI 流程中校验 YAML 格式正确性
|
||||
- **完整性检查**:确保所有语言包包含相同的错误码
|
||||
|
||||
### 2. 新增错误码流程
|
||||
|
||||
1. 在 `pkg/errors/codes.go` 中添加新错误码
|
||||
2. 在所有语言包中添加对应翻译
|
||||
3. 更新相关文档和测试用例
|
||||
|
||||
### 3. 新增语言支持流程
|
||||
|
||||
1. 创建新的语言包文件(如 `fr-FR.yaml`)
|
||||
2. 翻译所有错误信息
|
||||
3. 在配置中添加新语言到支持列表
|
||||
4. 更新 API 文档
|
||||
|
||||
## 实施建议
|
||||
|
||||
### 第一阶段:基础架构
|
||||
|
||||
1. 实现多语言管理器核心功能
|
||||
2. 创建中英文语言包
|
||||
3. 集成语言检测中间件
|
||||
4. 改造现有错误处理函数
|
||||
|
||||
### 第二阶段:功能完善
|
||||
|
||||
1. 添加更多语言支持
|
||||
2. 实现配置热更新
|
||||
3. 添加性能监控
|
||||
4. 完善错误处理和降级机制
|
||||
|
||||
### 第三阶段:扩展应用
|
||||
|
||||
1. 扩展到其他响应信息的多语言支持
|
||||
2. 添加用户语言偏好存储
|
||||
3. 实现动态语言切换
|
||||
4. 提供语言包管理接口
|
||||
|
||||
## 配置示例
|
||||
|
||||
### 应用配置
|
||||
|
||||
```yaml
|
||||
# configs/config.yaml
|
||||
i18n:
|
||||
enable: true
|
||||
default_lang: "zh-CN"
|
||||
config_path: "configs/i18n/errors"
|
||||
support_langs: ["zh-CN", "en-US", "ja-JP"]
|
||||
```
|
||||
|
||||
### 使用示例
|
||||
|
||||
```go
|
||||
// 初始化
|
||||
i18nManager := i18n.NewManager("configs/i18n/errors", "zh-CN")
|
||||
i18nManager.LoadLanguage("zh-CN")
|
||||
i18nManager.LoadLanguage("en-US")
|
||||
|
||||
// 使用
|
||||
message := i18nManager.GetErrorMessage("100003", "en-US")
|
||||
// 输出:Invalid username or password
|
||||
```
|
||||
|
||||
这个设计方案遵循系统的设计理念,提供了简洁、可维护的多语言错误信息支持,同时保持了与现有架构的良好集成。
|
||||
@@ -0,0 +1,24 @@
|
||||
# MySQL安装
|
||||
|
||||
## Docker安装
|
||||
|
||||
```bash
|
||||
|
||||
docker run --name lm-mysql -d \
|
||||
--restart always \
|
||||
-p 13306:3306 \
|
||||
-e MYSQL_ROOT_PASSWORD=root@123 \
|
||||
-e MYSQL_DATABASE=license_manager \
|
||||
-v mysql-data:/var/lib/mysql \
|
||||
-v mysql-conf:/etc/mysql/conf.d \
|
||||
-v mysql-logs:/var/log/mysql \
|
||||
mysql:8.0 \
|
||||
--default-authentication-plugin=mysql_native_password
|
||||
```
|
||||
|
||||
## 卸载清理
|
||||
|
||||
```bash
|
||||
docker rm -f lm-mysql
|
||||
docker volume rm mysql-data mysql-conf mysql-logs
|
||||
```
|
||||
@@ -38,4 +38,4 @@ swag fmt
|
||||
|
||||
# 验证swagger注释
|
||||
swag init --parseVendor -g cmd/main.go
|
||||
```
|
||||
```
|
||||
@@ -1,273 +0,0 @@
|
||||
# 登录与认证模块设计
|
||||
|
||||
## 概述
|
||||
|
||||
本模块负责系统的用户认证、登录状态管理和权限控制,基于JWT(JSON Web Token)实现无状态认证机制。
|
||||
|
||||
## 技术选型
|
||||
|
||||
- **认证方式**: JWT (JSON Web Token)
|
||||
- **加密算法**: HS256 (HMAC-SHA256)
|
||||
- **密码加密**: bcrypt
|
||||
- **Session存储**: 内存缓存/Redis(可选)
|
||||
|
||||
## 社区版登录设计
|
||||
|
||||
用户名和密码配置在配置文件中,默认用户名和密码为 `admin` / `admin@123`
|
||||
|
||||
## 功能模块
|
||||
|
||||
### 1. 用户登录 (Login)
|
||||
|
||||
#### 1.1 登录流程
|
||||
```
|
||||
1. 用户提交用户名/密码
|
||||
2. 验证用户凭证
|
||||
3. 生成JWT Token
|
||||
4. 返回Token和用户信息
|
||||
5. 前端存储Token(localStorage/sessionStorage)
|
||||
```
|
||||
|
||||
#### 1.2 API接口设计
|
||||
```http
|
||||
POST /api/v1/login
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"username": "admin",
|
||||
"password": "admin@123"
|
||||
}
|
||||
```
|
||||
|
||||
**响应格式:**
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "登录成功",
|
||||
"data": {
|
||||
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
|
||||
"expires_in": 3600,
|
||||
"user_info": {
|
||||
"username": "admin",
|
||||
"role": "administrator"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 1.3 实现细节
|
||||
- 密码使用bcrypt进行哈希验证
|
||||
- JWT包含用户ID、用户名、角色、过期时间
|
||||
- Token默认有效期1小时,可配置
|
||||
- 支持记住我功能(延长Token有效期)
|
||||
|
||||
### 2. 用户登出 (Logout)
|
||||
|
||||
#### 2.1 登出流程
|
||||
```
|
||||
1. 客户端删除本地Token
|
||||
2. 可选:服务端将Token加入黑名单
|
||||
3. 返回登出成功响应
|
||||
```
|
||||
|
||||
#### 2.2 API接口设计
|
||||
```http
|
||||
POST /api/v1/logout
|
||||
Authorization: Bearer <token>
|
||||
```
|
||||
|
||||
**响应格式:**
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "登出成功"
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Token刷新
|
||||
|
||||
#### 3.1 刷新机制
|
||||
- 当Token即将过期时(剩余时间<30分钟),自动刷新
|
||||
- 刷新Token与访问Token分离(可选实现)
|
||||
|
||||
#### 3.2 API接口设计
|
||||
```http
|
||||
POST /api/v1/auth/refresh
|
||||
Authorization: Bearer <token>
|
||||
```
|
||||
|
||||
## JWT Token设计
|
||||
|
||||
### 1. Token结构
|
||||
```json
|
||||
{
|
||||
"header": {
|
||||
"alg": "HS256",
|
||||
"typ": "JWT"
|
||||
},
|
||||
"payload": {
|
||||
"user_id": 1,
|
||||
"username": "admin",
|
||||
"role": "administrator",
|
||||
"iat": 1640995200,
|
||||
"exp": 1641002400
|
||||
},
|
||||
"signature": "signature_string"
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Claims定义
|
||||
- `user_id`: 用户ID
|
||||
- `username`: 用户名
|
||||
- `role`: 用户角色
|
||||
- `iat`: 签发时间
|
||||
- `exp`: 过期时间
|
||||
|
||||
## 中间件认证设计
|
||||
|
||||
### 1. 认证中间件 (AuthMiddleware)
|
||||
|
||||
#### 1.1 功能职责
|
||||
- 验证HTTP请求头中的JWT Token
|
||||
- 解析Token获取用户信息
|
||||
- 处理Token过期和无效情况
|
||||
- 将用户信息注入到请求上下文
|
||||
|
||||
#### 1.2 认证流程
|
||||
```
|
||||
1. 检查Authorization头是否存在
|
||||
2. 提取Bearer Token
|
||||
3. 验证Token签名和有效性
|
||||
4. 解析用户信息
|
||||
5. 将用户信息存储到Context
|
||||
6. 继续处理请求或返回401错误
|
||||
```
|
||||
|
||||
#### 1.3 实现伪代码
|
||||
```go
|
||||
func AuthMiddleware() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
// 1. 获取Token
|
||||
token := extractToken(c)
|
||||
if token == "" {
|
||||
c.JSON(401, gin.H{"error": "Missing token"})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
// 2. 验证Token
|
||||
claims, err := validateToken(token)
|
||||
if err != nil {
|
||||
c.JSON(401, gin.H{"error": "Invalid token"})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
// 3. 设置用户信息到Context
|
||||
c.Set("user_id", claims.UserID)
|
||||
c.Set("username", claims.Username)
|
||||
c.Set("role", claims.Role)
|
||||
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 可选中间件 (OptionalAuth)
|
||||
|
||||
对于某些接口可能需要可选认证(如公开API),提供可选认证中间件:
|
||||
|
||||
```go
|
||||
func OptionalAuthMiddleware() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
token := extractToken(c)
|
||||
if token != "" {
|
||||
if claims, err := validateToken(token); err == nil {
|
||||
c.Set("user_id", claims.UserID)
|
||||
c.Set("username", claims.Username)
|
||||
c.Set("role", claims.Role)
|
||||
}
|
||||
}
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 路由保护策略
|
||||
|
||||
### 1. 路由分组
|
||||
```go
|
||||
// 公开路由
|
||||
public := r.Group("/api/v1")
|
||||
{
|
||||
public.POST("/login", handlers.Login)
|
||||
}
|
||||
|
||||
// 需要认证的路由
|
||||
auth := r.Group("/api/v1")
|
||||
auth.Use(middleware.AuthMiddleware())
|
||||
{
|
||||
auth.POST("/logout", handlers.Logout)
|
||||
auth.GET("/customers", handlers.GetCustomers)
|
||||
auth.POST("/licenses", handlers.CreateLicense)
|
||||
}
|
||||
|
||||
// 管理员路由
|
||||
admin := r.Group("/api/v1/admin")
|
||||
admin.Use(middleware.AuthMiddleware(), middleware.AdminOnly())
|
||||
{
|
||||
admin.GET("/system/info", handlers.GetSystemInfo)
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 权限级别
|
||||
- **Public**: 无需认证
|
||||
- **Authenticated**: 需要有效Token
|
||||
- **Admin**: 需要管理员权限
|
||||
|
||||
## 安全措施
|
||||
|
||||
### 1. Token安全
|
||||
- 使用强密钥签名(至少32字节)
|
||||
- 设置合理的过期时间
|
||||
|
||||
### 2. 密码安全
|
||||
- bcrypt加密,cost factor >= 10
|
||||
- 登录失败限制(防暴力破解)
|
||||
|
||||
|
||||
## 配置参数
|
||||
|
||||
```yaml
|
||||
auth:
|
||||
jwt:
|
||||
secret: "your-256-bit-secret"
|
||||
expire_hours: 1
|
||||
refresh_threshold_minutes: 30
|
||||
admin:
|
||||
username: "admin"
|
||||
password: "admin@123" # 明文密码 部署人员可修改
|
||||
security:
|
||||
max_login_attempts: 5
|
||||
lockout_duration_minutes: 15
|
||||
```
|
||||
|
||||
## 错误处理
|
||||
|
||||
### 1. 认证错误码
|
||||
- `AUTH_001`: Token缺失
|
||||
- `AUTH_002`: Token无效
|
||||
- `AUTH_003`: Token过期
|
||||
- `AUTH_004`: 权限不足
|
||||
- `AUTH_005`: 用户名或密码错误
|
||||
- `AUTH_006`: 账户被锁定
|
||||
|
||||
### 2. 统一错误响应
|
||||
```json
|
||||
{
|
||||
"code": 401,
|
||||
"error": "AUTH_002",
|
||||
"message": "Token无效",
|
||||
"timestamp": "2024-01-01T12:00:00Z"
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,205 @@
|
||||
# 客户管理API设计
|
||||
|
||||
## 1. API概览
|
||||
|
||||
| 方法 | 路径 | 说明 |
|
||||
|------|------|------|
|
||||
| GET | `/api/customers` | 查询客户列表 |
|
||||
| GET | `/api/customers/{id}` | 获取客户详情 |
|
||||
| POST | `/api/customers` | 创建客户 |
|
||||
| PUT | `/api/customers/{id}` | 更新客户信息 |
|
||||
| DELETE | `/api/customers/{id}` | 删除客户(软删除) |
|
||||
| PATCH | `/api/customers/{id}/status` | 修改客户状态 |
|
||||
|
||||
---
|
||||
|
||||
## 2. 数据模型
|
||||
|
||||
### 客户信息结构
|
||||
```json
|
||||
{
|
||||
"id": "uuid",
|
||||
"customer_code": "CUS-2025-0001",
|
||||
"customer_name": "北京科技有限公司",
|
||||
"customer_type": "enterprise",
|
||||
"contact_person": "张三",
|
||||
"contact_title": "技术总监",
|
||||
"email": "[email protected]",
|
||||
"phone": "13800138000",
|
||||
"address": "北京市朝阳区科技园区100号",
|
||||
"company_size": "medium",
|
||||
"customer_level": "vip",
|
||||
"status": "active",
|
||||
"description": "重要客户,需要优先技术支持",
|
||||
"created_at": "2025-01-15T10:30:00Z",
|
||||
"updated_at": "2025-01-20T15:45:00Z",
|
||||
"created_by": "admin_uuid",
|
||||
"updated_by": "admin_uuid"
|
||||
}
|
||||
```
|
||||
|
||||
### 字段说明
|
||||
- `customer_type`: `individual` | `enterprise` | `government` | `education`
|
||||
- `company_size`: `small` | `medium` | `large` | `enterprise`
|
||||
- `customer_level`: `normal` | `vip` | `enterprise` | `strategic`
|
||||
- `status`: `active` | `disabled`
|
||||
|
||||
---
|
||||
|
||||
## 3. API详细设计
|
||||
|
||||
### 3.1 查询客户列表
|
||||
```
|
||||
GET /api/customers
|
||||
```
|
||||
|
||||
**Query参数**
|
||||
- `page`: 页码,默认1
|
||||
- `page_size`: 每页条数,默认20,最大100
|
||||
- `search`: 搜索关键词(支持客户编码、名称、联系人、邮箱)
|
||||
- `customer_type`: 客户类型筛选
|
||||
- `customer_level`: 客户等级筛选
|
||||
- `status`: 状态筛选
|
||||
- `sort`: 排序字段,默认`created_at`
|
||||
- `order`: 排序方向,默认`desc`
|
||||
|
||||
**响应示例**
|
||||
```json
|
||||
{
|
||||
"code": 0,
|
||||
"message": "success",
|
||||
"data": {
|
||||
"list": [
|
||||
{
|
||||
"id": "uuid",
|
||||
"customer_code": "CUS-2025-0001",
|
||||
"customer_name": "北京科技有限公司",
|
||||
"customer_type": "enterprise",
|
||||
"contact_person": "张三",
|
||||
"email": "[email protected]",
|
||||
"customer_level": "vip",
|
||||
"status": "active",
|
||||
"created_at": "2025-01-15T10:30:00Z"
|
||||
}
|
||||
],
|
||||
"total": 100,
|
||||
"page": 1,
|
||||
"page_size": 20,
|
||||
"total_pages": 5
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3.2 获取客户详情
|
||||
```
|
||||
GET /api/customers/{id}
|
||||
```
|
||||
|
||||
**响应示例**
|
||||
```json
|
||||
{
|
||||
"code": 0,
|
||||
"message": "success",
|
||||
"data": {
|
||||
// 完整的客户信息结构
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3.3 创建客户
|
||||
```
|
||||
POST /api/customers
|
||||
```
|
||||
|
||||
**请求体**
|
||||
```json
|
||||
{
|
||||
"customer_name": "北京科技有限公司",
|
||||
"customer_type": "enterprise",
|
||||
"contact_person": "张三",
|
||||
"contact_title": "技术总监",
|
||||
"email": "[email protected]",
|
||||
"phone": "13800138000",
|
||||
"address": "北京市朝阳区科技园区100号",
|
||||
"company_size": "medium",
|
||||
"customer_level": "normal",
|
||||
"description": "新客户",
|
||||
"status": "active"
|
||||
}
|
||||
```
|
||||
|
||||
**必填字段**
|
||||
- `customer_name`: 客户名称
|
||||
- `contact_person`: 联系人姓名
|
||||
- `customer_level`: 客户等级
|
||||
- `customer_type`: 客户类型
|
||||
- `company_size`: 企业规模
|
||||
- `status`: 状态
|
||||
|
||||
|
||||
**响应示例**
|
||||
```json
|
||||
{
|
||||
"code": 0,
|
||||
"message": "客户创建成功",
|
||||
"data": {
|
||||
// 创建后的完整客户信息
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3.4 更新客户信息
|
||||
```
|
||||
PUT /api/customers/{id}
|
||||
```
|
||||
|
||||
**请求体**: 与创建客户相同,但所有字段都是可选的
|
||||
|
||||
**响应示例**
|
||||
```json
|
||||
{
|
||||
"code": 0,
|
||||
"message": "客户信息更新成功",
|
||||
"data": {
|
||||
// 更新后的完整客户信息
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3.5 删除客户
|
||||
```
|
||||
DELETE /api/customers/{id}
|
||||
```
|
||||
|
||||
**响应示例**
|
||||
```json
|
||||
{
|
||||
"code": 0,
|
||||
"message": "客户删除成功",
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
### 3.6 修改客户状态
|
||||
```
|
||||
PATCH /api/customers/{id}/status
|
||||
```
|
||||
|
||||
**请求体**
|
||||
```json
|
||||
{
|
||||
"status": "disabled"
|
||||
}
|
||||
```
|
||||
|
||||
**响应示例**
|
||||
```json
|
||||
{
|
||||
"code": 0,
|
||||
"message": "客户状态更新成功",
|
||||
"data": {
|
||||
"id": "uuid",
|
||||
"status": "disabled"
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,337 @@
|
||||
# 客户属性设计与Web显示方案
|
||||
|
||||
## 1. 核心客户属性
|
||||
|
||||
### 基础标识
|
||||
- `id` - 唯一标识符
|
||||
- **作用**: 数据库主键,系统内部引用
|
||||
- **生成**: UUID自动生成
|
||||
- **必填**: 是
|
||||
|
||||
- `customer_code` - 客户编码 (如: CUS-2025-001)
|
||||
- **作用**: 业务识别码,便于客服沟通和查找
|
||||
- **生成**: CUS-YYYY-NNNN格式,系统自动递增
|
||||
- **必填**: 是
|
||||
- **唯一**: 是
|
||||
- **意义**: 比数据库ID更有业务意义,支持按年度统计
|
||||
|
||||
- `customer_name` - 客户名称/公司名
|
||||
- **作用**: 主要识别信息,合同签署主体
|
||||
- **示例**: "北京科技有限公司"、"张三"
|
||||
- **必填**: 是
|
||||
- **搜索**: 支持模糊搜索
|
||||
|
||||
- `customer_type` - 客户类型 (个人/企业/政府/教育)
|
||||
- **作用**: 区分客户类别,影响服务策略和定价
|
||||
- **枚举**: individual(个人) | enterprise(企业) | government(政府) | education(教育)
|
||||
- **必填**: 是
|
||||
- **默认**: enterprise
|
||||
|
||||
### 联系信息
|
||||
- `contact_person` - 联系人姓名
|
||||
- **作用**: 授权通知和技术支持的对接人
|
||||
- **必填**: 是
|
||||
- **示例**: "张三"
|
||||
|
||||
- `contact_title` - 联系人职务
|
||||
- **作用**: 了解联系人在组织中的角色,便于沟通
|
||||
- **必填**: 否
|
||||
- **示例**: "技术总监"、"IT经理"
|
||||
|
||||
- `email` - 主邮箱
|
||||
- **作用**: 授权激活通知、过期提醒、技术支持的主要渠道
|
||||
- **必填**: 否
|
||||
- **验证**: 邮箱格式验证
|
||||
|
||||
- `phone` - 电话号码
|
||||
- **作用**: 紧急联系、电话技术支持
|
||||
- **必填**: 否
|
||||
- **格式**: 支持手机和固话
|
||||
|
||||
### 地址信息
|
||||
- `address` - 地址
|
||||
- **作用**: 联系地址,邮寄材料或现场服务时使用
|
||||
- **必填**: 否
|
||||
- **示例**: "北京市朝阳区科技园区100号"
|
||||
- **格式**: 自由文本,用户可详细可简略
|
||||
|
||||
### 商业属性
|
||||
- `company_size` - 企业规模 (小型/中型/大型/集团)
|
||||
- **作用**: 影响定价策略和服务等级
|
||||
- **枚举**: small(小型) | medium(中型) | large(大型) | enterprise(集团)
|
||||
- **必填**: 否
|
||||
- **用途**: 制定差异化授权策略
|
||||
|
||||
|
||||
### 客户分级
|
||||
- `customer_level` - 客户等级 (普通/VIP/企业/战略)
|
||||
- **作用**: 影响服务优先级、授权数量限制、技术支持等级
|
||||
- **枚举**: normal(普通) | vip(VIP) | enterprise(企业) | strategic(战略)
|
||||
- **必填**: 否
|
||||
- **默认**: normal
|
||||
- **影响**: 高等级客户可获得更多授权、更快响应
|
||||
|
||||
### 状态管理
|
||||
- `status` - 客户状态 (正常/禁用)
|
||||
- **作用**: 控制是否允许为该客户生成新授权
|
||||
- **枚举**: active(正常) | disabled(禁用)
|
||||
- **必填**: 是
|
||||
- **默认**: active
|
||||
- **影响**: 禁用状态下不能生成新授权,但现有授权仍可使用
|
||||
|
||||
- `description` - 客户描述/备注
|
||||
- **作用**: 记录客户特殊情况、业务背景、注意事项
|
||||
- **必填**: 否
|
||||
- **示例**: "重要客户,IoT平台采购方,需要优先技术支持"
|
||||
|
||||
### 时间字段
|
||||
- `created_at` - 创建时间
|
||||
- **作用**: 记录客户注册时间,用于统计分析
|
||||
- **自动**: 系统自动记录
|
||||
- **不可修改**: 是
|
||||
|
||||
- `updated_at` - 更新时间
|
||||
- **作用**: 记录最后修改时间,追踪数据变更
|
||||
- **自动**: 系统自动更新
|
||||
- **触发**: 任何字段修改都会更新
|
||||
|
||||
- `created_by` - 创建人
|
||||
- **作用**: 记录是哪个管理员创建的客户
|
||||
- **必填**: 是
|
||||
- **关联**: 管理员用户ID
|
||||
|
||||
- `updated_by` - 更新人
|
||||
- **作用**: 记录最后修改人,便于问题追溯
|
||||
- **自动**: 系统自动记录当前操作员
|
||||
|
||||
---
|
||||
|
||||
## 2. Web列表页显示信息
|
||||
|
||||
### 主要显示列
|
||||
```
|
||||
| 客户编码 | 客户名称 | 客户类型 | 联系人 | 邮箱 | 客户等级 | 状态 | 创建时间 | 操作 |
|
||||
```
|
||||
|
||||
### 具体显示内容与作用
|
||||
|
||||
**客户编码** (`customer_code`)
|
||||
- 显示: CUS-2025-001
|
||||
- 作用: 快速识别客户,支持精确搜索,客服沟通时的标准引用
|
||||
- 交互: 点击可复制到剪贴板
|
||||
|
||||
**客户名称** (`customer_name`)
|
||||
- 显示: 北京科技有限公司
|
||||
- 作用: 主要识别信息,最重要的显示内容
|
||||
- 交互: 点击进入客户详情页
|
||||
- 样式: 可截断显示,悬停显示完整名称
|
||||
|
||||
**客户类型** (`customer_type`)
|
||||
- 显示: 🏢企业 | 👤个人 | 🏛️政府 | 🎓教育
|
||||
- 作用: 快速区分客户类别,影响服务策略
|
||||
- 样式: 用图标+文字+颜色区分
|
||||
- 筛选: 支持按类型筛选
|
||||
|
||||
**联系人** (`contact_person`)
|
||||
- 显示: 张三
|
||||
- 作用: 知道具体联系谁,便于沟通对接
|
||||
- 交互: 悬停显示联系人职务信息
|
||||
|
||||
**邮箱** (`email`)
|
||||
- 显示: zhangsan@example.com
|
||||
- 作用: 重要联系方式,授权通知的主要渠道
|
||||
- 交互: 点击可发送邮件
|
||||
- 验证: 显示邮箱验证状态(已验证/未验证)
|
||||
|
||||
**客户等级** (`customer_level`)
|
||||
- 显示: 🏢企业 | 👤普通 | ⭐VIP | 💼战略
|
||||
- 作用: 显示客户重要程度,影响服务优先级
|
||||
- 样式: 不同等级用不同颜色和图标
|
||||
- 排序: 支持按等级排序
|
||||
|
||||
**状态** (`status`)
|
||||
- 显示: ✅正常 | ❌禁用
|
||||
- 作用: 显示客户是否可以生成新授权
|
||||
- 交互: 可直接点击切换状态(需要确认)
|
||||
- 样式: 正常为绿色,禁用为红色
|
||||
|
||||
**创建时间** (`created_at`)
|
||||
- 显示: 2025-01-15
|
||||
- 作用: 了解客户注册时间,便于分析客户生命周期
|
||||
- 格式: YYYY-MM-DD
|
||||
- 排序: 默认按创建时间倒序
|
||||
|
||||
**操作**
|
||||
- 查看详情: 进入客户详情页
|
||||
- 编辑: 修改客户信息
|
||||
- 授权管理: 查看该客户的所有授权
|
||||
- 生成授权: 快速为该客户生成新授权
|
||||
- 更多操作: 禁用/启用、删除等
|
||||
|
||||
---
|
||||
|
||||
## 3. Web详情页显示信息
|
||||
|
||||
### 基本信息卡片
|
||||
```
|
||||
客户编码: CUS-2025-001 客户类型: 🏢企业
|
||||
客户名称: 北京科技有限公司 客户等级: ⭐VIP客户
|
||||
联系人: 张三 (技术总监) 当前状态: ✅正常
|
||||
邮箱: [email protected] 电话: 13800138000
|
||||
```
|
||||
**用途**: 核心信息一览,便于快速了解客户基本情况
|
||||
|
||||
### 地址信息卡片
|
||||
```
|
||||
地址: 北京市朝阳区科技园区100号
|
||||
```
|
||||
**用途**: 联系地址,邮寄材料或现场服务参考
|
||||
|
||||
### 商业信息卡片
|
||||
```
|
||||
企业规模: 中型企业
|
||||
备注描述: 重要客户,IoT平台采购方,需要优先技术支持
|
||||
```
|
||||
**用途**: 业务背景了解,制定针对性服务策略
|
||||
|
||||
### 状态信息卡片
|
||||
```
|
||||
当前状态: ✅正常 创建时间: 2025-01-15 10:30:00
|
||||
创建人: 李四(管理员) 更新时间: 2025-01-20 15:45:00
|
||||
更新人: 王五(客服)
|
||||
```
|
||||
**用途**: 跟踪客户生命周期,了解操作历史
|
||||
|
||||
### 授权统计卡片
|
||||
```
|
||||
总授权数: 3个 活跃授权: 2个
|
||||
过期授权: 1个 最近授权: 2025-01-18
|
||||
即将过期: 1个(7天内) 授权设备总数: 85台
|
||||
```
|
||||
**用途**: 快速了解客户授权使用情况,识别续费机会
|
||||
|
||||
---
|
||||
|
||||
## 4. 列表页功能特性
|
||||
|
||||
### 搜索筛选
|
||||
- **全文搜索框**: 支持客户编码、名称、联系人、邮箱的模糊搜索
|
||||
- **作用**: 快速定位目标客户
|
||||
- **实现**: 实时搜索,输入即显示结果
|
||||
|
||||
- **客户类型筛选**: 全部 | 企业 | 个人 | 政府 | 教育
|
||||
- **作用**: 按业务类型分类查看
|
||||
- **场景**: 针对不同类型客户制定营销策略
|
||||
|
||||
- **客户等级筛选**: 全部 | 普通 | VIP | 企业 | 战略
|
||||
- **作用**: 重点关注高价值客户
|
||||
- **场景**: 优先处理VIP和战略客户的需求
|
||||
|
||||
- **状态筛选**: 全部 | 正常 | 禁用
|
||||
- **作用**: 识别问题客户,及时处理
|
||||
- **场景**: 定期清理禁用客户,分析禁用原因
|
||||
|
||||
### 排序功能
|
||||
- **按创建时间排序** (默认)
|
||||
- **作用**: 了解客户注册趋势,关注新客户
|
||||
|
||||
- **按客户名称排序**
|
||||
- **作用**: 字母序查找,便于浏览
|
||||
|
||||
- **按客户等级排序**
|
||||
- **作用**: 优先显示重要客户
|
||||
|
||||
- **按更新时间排序**
|
||||
- **作用**: 查看最近活跃的客户
|
||||
|
||||
### 批量操作
|
||||
- **批量启用/禁用**
|
||||
- **作用**: 批量处理客户状态变更
|
||||
- **场景**: 批量处理欠费客户、恢复客户服务
|
||||
|
||||
- **批量导出**
|
||||
- **作用**: 导出客户数据进行分析
|
||||
- **格式**: Excel、CSV格式
|
||||
|
||||
- **批量删除**
|
||||
- **作用**: 清理无效客户数据
|
||||
- **安全**: 需要二次确认,软删除机制
|
||||
|
||||
### 分页显示
|
||||
- **每页条数**: 10/20/50/100条可选
|
||||
- **默认**: 20条
|
||||
- **作用**: 平衡加载速度和浏览效率
|
||||
|
||||
- **总数统计**: 显示"共1,234个客户"
|
||||
- **作用**: 了解客户总体规模
|
||||
|
||||
- **页码导航**: 支持跳转到指定页
|
||||
- **作用**: 快速浏览大量数据
|
||||
|
||||
---
|
||||
|
||||
## 5. 显示优化建议
|
||||
|
||||
### 响应式设计
|
||||
- **桌面端(>1200px)**: 显示所有列
|
||||
- **作用**: 完整信息展示,提高工作效率
|
||||
|
||||
- **平板端(768-1200px)**: 隐藏地址、电话等次要信息
|
||||
- **作用**: 保持核心信息,适应中等屏幕
|
||||
|
||||
- **手机端(<768px)**: 卡片式布局,只显示客户名称、等级、状态
|
||||
- **作用**: 移动端快速浏览,支持现场客户查询
|
||||
|
||||
### 数据可视化(仪表盘)
|
||||
- **客户等级分布饼图**
|
||||
- **作用**: 了解客户结构,制定等级提升策略
|
||||
|
||||
- **客户类型统计柱图**
|
||||
- **作用**: 分析目标市场,调整营销重点
|
||||
|
||||
- **月度新增客户趋势图**
|
||||
- **作用**: 监控业务增长,评估营销效果
|
||||
|
||||
### 交互优化
|
||||
- **悬停显示完整信息**
|
||||
- **场景**: 客户名称过长时显示完整名称
|
||||
- **内容**: 联系人悬停显示职务和电话
|
||||
|
||||
- **点击客户名称进入详情页**
|
||||
- **作用**: 快速访问详细信息
|
||||
|
||||
- **状态切换快捷操作**
|
||||
- **实现**: 点击状态图标直接切换,需要确认
|
||||
- **作用**: 快速处理客户状态变更
|
||||
|
||||
- **右键菜单快速操作**
|
||||
- **功能**: 编辑、查看授权、生成授权、禁用等
|
||||
- **作用**: 提高操作效率,减少页面跳转
|
||||
|
||||
### 性能优化
|
||||
- **虚拟滚动**: 大数据量时采用虚拟滚动
|
||||
- **触发**: 客户数>1000时
|
||||
- **作用**: 保证页面流畅性
|
||||
|
||||
- **延迟加载**: 图片和非关键信息延迟加载
|
||||
- **作用**: 提高首屏加载速度
|
||||
|
||||
- **缓存策略**: 客户列表数据缓存5分钟
|
||||
- **作用**: 减少不必要的服务器请求
|
||||
|
||||
## 6. 业务价值总结
|
||||
|
||||
### 提高工作效率
|
||||
- **快速识别**: 通过客户编码和等级快速识别重要客户
|
||||
- **精准搜索**: 多维度搜索快速定位目标客户
|
||||
- **批量操作**: 减少重复性操作,提高处理效率
|
||||
|
||||
### 优化客户服务
|
||||
- **服务分级**: 根据客户等级提供差异化服务
|
||||
- **及时响应**: 通过状态管理及时识别问题客户
|
||||
- **个性化**: 根据客户属性提供合适的解决方案
|
||||
|
||||
### 支撑业务决策
|
||||
- **客户分析**: 通过等级、类型分布了解客户结构
|
||||
- **趋势监控**: 通过创建时间分析客户增长趋势
|
||||
- **风险控制**: 通过状态管理防范业务风险
|
||||
@@ -0,0 +1,145 @@
|
||||
# 登录与认证模块设计
|
||||
|
||||
## 概述
|
||||
|
||||
本模块负责系统的用户认证、登录状态管理和权限控制,基于JWT(JSON Web Token)实现无状态认证机制。
|
||||
|
||||
## 技术选型
|
||||
|
||||
- **认证方式**: JWT (JSON Web Token)
|
||||
- **加密算法**: HS256 (HMAC-SHA256)
|
||||
- **密码加密**: bcrypt
|
||||
- **Session存储**: 内存缓存/Redis(可选)
|
||||
|
||||
## 社区版登录设计
|
||||
|
||||
用户名和密码配置在配置文件中,默认用户名和密码为 `admin` / `admin@123`
|
||||
|
||||
## 功能模块
|
||||
|
||||
### 1. 用户登录 (Login)
|
||||
|
||||
#### 1.1 登录流程
|
||||
```
|
||||
1. 用户提交用户名/密码
|
||||
2. 验证用户凭证
|
||||
3. 生成JWT Token
|
||||
4. 返回Token和用户信息
|
||||
5. 前端存储Token(localStorage/sessionStorage)
|
||||
```
|
||||
|
||||
#### 1.2 API接口设计
|
||||
```http
|
||||
POST /api/v1/login
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"username": "admin",
|
||||
"password": "admin@123"
|
||||
}
|
||||
```
|
||||
|
||||
**响应格式:**
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "登录成功",
|
||||
"data": {
|
||||
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
|
||||
"expires_in": 3600,
|
||||
"user_info": {
|
||||
"username": "admin",
|
||||
"role": "administrator"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 1.3 实现细节
|
||||
- 密码使用bcrypt进行哈希验证
|
||||
- JWT包含用户ID、用户名、角色、过期时间
|
||||
- Token默认有效期1小时,可配置
|
||||
- 支持记住我功能(延长Token有效期)
|
||||
|
||||
### 2. 用户登出 (Logout)
|
||||
|
||||
#### 2.1 登出流程
|
||||
```
|
||||
1. 客户端删除本地Token
|
||||
2. 可选:服务端将Token加入黑名单
|
||||
3. 返回登出成功响应
|
||||
```
|
||||
|
||||
#### 2.2 API接口设计
|
||||
```http
|
||||
POST /api/v1/logout
|
||||
Authorization: Bearer <token>
|
||||
```
|
||||
|
||||
**响应格式:**
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "登出成功"
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Token刷新
|
||||
|
||||
#### 3.1 刷新机制
|
||||
- 当Token即将过期时(剩余时间<30分钟),自动刷新
|
||||
- 刷新Token与访问Token分离(可选实现)
|
||||
|
||||
#### 3.2 API接口设计
|
||||
```http
|
||||
POST /api/v1/auth/refresh
|
||||
Authorization: Bearer <token>
|
||||
```
|
||||
|
||||
## JWT Token设计
|
||||
|
||||
### 1. Token结构
|
||||
```json
|
||||
{
|
||||
"header": {
|
||||
"alg": "HS256",
|
||||
"typ": "JWT"
|
||||
},
|
||||
"payload": {
|
||||
"user_id": 1,
|
||||
"username": "admin",
|
||||
"role": "administrator",
|
||||
"iat": 1640995200,
|
||||
"exp": 1641002400
|
||||
},
|
||||
"signature": "signature_string"
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Claims定义
|
||||
- `user_id`: 用户ID
|
||||
- `username`: 用户名
|
||||
- `role`: 用户角色
|
||||
- `iat`: 签发时间
|
||||
- `exp`: 过期时间
|
||||
|
||||
## 中间件认证设计
|
||||
|
||||
### 1. 认证中间件 (AuthMiddleware)
|
||||
|
||||
#### 1.1 功能职责
|
||||
- 验证HTTP请求头中的JWT Token
|
||||
- 解析Token获取用户信息
|
||||
- 处理Token过期和无效情况
|
||||
- 将用户信息注入到请求上下文
|
||||
|
||||
#### 1.2 认证流程
|
||||
```
|
||||
1. 检查Authorization头是否存在
|
||||
2. 提取Bearer Token
|
||||
3. 验证Token签名和有效性
|
||||
4. 解析用户信息
|
||||
5. 将用户信息存储到Context
|
||||
6. 继续处理请求或返回401错误
|
||||
```
|
||||
|
||||
@@ -234,15 +234,15 @@ const docTemplate = `{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"code": {
|
||||
"type": "integer"
|
||||
},
|
||||
"error": {
|
||||
"description": "业务错误码,如 \"100001\", \"900001\"",
|
||||
"type": "string"
|
||||
},
|
||||
"message": {
|
||||
"description": "错误描述信息",
|
||||
"type": "string"
|
||||
},
|
||||
"timestamp": {
|
||||
"description": "错误发生时间",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
@@ -334,9 +334,9 @@ const docTemplate = `{
|
||||
// SwaggerInfo holds exported Swagger Info so clients can modify it
|
||||
var SwaggerInfo = &swag.Spec{
|
||||
Version: "1.0",
|
||||
Host: "localhost:18888",
|
||||
Host: "",
|
||||
BasePath: "/",
|
||||
Schemes: []string{},
|
||||
Schemes: []string{"http", "https"},
|
||||
Title: "License Manager API",
|
||||
Description: "软件授权管理平台API文档",
|
||||
InfoInstanceName: "swagger",
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
{
|
||||
"schemes": [
|
||||
"http",
|
||||
"https"
|
||||
],
|
||||
"swagger": "2.0",
|
||||
"info": {
|
||||
"description": "软件授权管理平台API文档",
|
||||
@@ -15,7 +19,6 @@
|
||||
},
|
||||
"version": "1.0"
|
||||
},
|
||||
"host": "localhost:18888",
|
||||
"basePath": "/",
|
||||
"paths": {
|
||||
"/api/v1/admin/system/info": {
|
||||
@@ -228,15 +231,15 @@
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"code": {
|
||||
"type": "integer"
|
||||
},
|
||||
"error": {
|
||||
"description": "业务错误码,如 \"100001\", \"900001\"",
|
||||
"type": "string"
|
||||
},
|
||||
"message": {
|
||||
"description": "错误描述信息",
|
||||
"type": "string"
|
||||
},
|
||||
"timestamp": {
|
||||
"description": "错误发生时间",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,12 +13,13 @@ definitions:
|
||||
models.ErrorResponse:
|
||||
properties:
|
||||
code:
|
||||
type: integer
|
||||
error:
|
||||
description: 业务错误码,如 "100001", "900001"
|
||||
type: string
|
||||
message:
|
||||
description: 错误描述信息
|
||||
type: string
|
||||
timestamp:
|
||||
description: 错误发生时间
|
||||
type: string
|
||||
type: object
|
||||
models.HealthResponse:
|
||||
@@ -69,7 +70,6 @@ definitions:
|
||||
version:
|
||||
type: string
|
||||
type: object
|
||||
host: localhost:8080
|
||||
info:
|
||||
contact:
|
||||
email: [email protected]
|
||||
@@ -203,6 +203,9 @@ paths:
|
||||
summary: 健康检测
|
||||
tags:
|
||||
- 系统
|
||||
schemes:
|
||||
- http
|
||||
- https
|
||||
securityDefinitions:
|
||||
BearerAuth:
|
||||
description: Bearer token for authentication
|
||||
|
||||
@@ -11,9 +11,13 @@ require (
|
||||
github.com/swaggo/gin-swagger v1.6.0
|
||||
github.com/swaggo/swag v1.16.3
|
||||
golang.org/x/crypto v0.28.0
|
||||
gopkg.in/yaml.v2 v2.4.0
|
||||
gorm.io/driver/mysql v1.6.0
|
||||
gorm.io/gorm v1.30.1
|
||||
)
|
||||
|
||||
require (
|
||||
filippo.io/edwards25519 v1.1.0 // indirect
|
||||
github.com/KyleBanks/depth v1.2.1 // indirect
|
||||
github.com/PuerkitoBio/purell v1.1.1 // indirect
|
||||
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect
|
||||
@@ -31,8 +35,11 @@ require (
|
||||
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.20.0 // indirect
|
||||
github.com/go-sql-driver/mysql v1.8.1 // indirect
|
||||
github.com/goccy/go-json v0.10.2 // indirect
|
||||
github.com/hashicorp/hcl v1.0.0 // indirect
|
||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||
github.com/jinzhu/now v1.1.5 // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.7 // indirect
|
||||
@@ -59,10 +66,9 @@ require (
|
||||
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect
|
||||
golang.org/x/net v0.25.0 // indirect
|
||||
golang.org/x/sys v0.26.0 // indirect
|
||||
golang.org/x/text v0.19.0 // indirect
|
||||
golang.org/x/text v0.20.0 // indirect
|
||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect
|
||||
google.golang.org/protobuf v1.34.1 // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
@@ -1,3 +1,5 @@
|
||||
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
||||
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
||||
github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
|
||||
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
|
||||
github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI=
|
||||
@@ -47,6 +49,8 @@ github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJn
|
||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||
github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8=
|
||||
github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
|
||||
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
|
||||
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
|
||||
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
|
||||
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=
|
||||
@@ -56,6 +60,10 @@ github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeN
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
|
||||
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||
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/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
|
||||
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
@@ -164,8 +172,8 @@ golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
|
||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
|
||||
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.9.0 h1:fEo0HyrW1GIgZdpbhCRO0PkJajUS5H9IFUztCgEo2jQ=
|
||||
golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
@@ -185,8 +193,8 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM=
|
||||
golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
|
||||
golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug=
|
||||
golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
@@ -208,5 +216,9 @@ gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C
|
||||
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/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.6.0 h1:eNbLmNTpPpTOVZi8MMxCi2aaIm0ZpInbORNXDwyLGvg=
|
||||
gorm.io/driver/mysql v1.6.0/go.mod h1:D/oCC2GWK3M/dqoLxnOlaNKmXz8WNTfcS9y5ovaSqKo=
|
||||
gorm.io/gorm v1.30.1 h1:lSHg33jJTBxs2mgJRfRZeLDG+WZaHYCk3Wtfl6Ngzo4=
|
||||
gorm.io/gorm v1.30.1/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE=
|
||||
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
|
||||
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
|
||||
@@ -3,9 +3,12 @@ package handlers
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"license-manager/internal/api/middleware"
|
||||
"license-manager/internal/models"
|
||||
"license-manager/internal/service"
|
||||
"license-manager/pkg/i18n"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type AuthHandler struct {
|
||||
@@ -34,10 +37,11 @@ func NewAuthHandler(authService service.AuthService) *AuthHandler {
|
||||
func (h *AuthHandler) Login(c *gin.Context) {
|
||||
var req models.LoginRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, models.ErrorResponse{
|
||||
Code: http.StatusBadRequest,
|
||||
Error: "INVALID_REQUEST",
|
||||
Message: "请求参数无效",
|
||||
lang := middleware.GetLanguage(c)
|
||||
status, errCode, message := i18n.NewI18nErrorResponse("900001", lang)
|
||||
c.JSON(status, models.ErrorResponse{
|
||||
Code: errCode,
|
||||
Message: message,
|
||||
Timestamp: getCurrentTimestamp(),
|
||||
})
|
||||
return
|
||||
@@ -45,23 +49,26 @@ func (h *AuthHandler) Login(c *gin.Context) {
|
||||
|
||||
data, err := h.authService.Login(&req)
|
||||
if err != nil {
|
||||
statusCode := http.StatusUnauthorized
|
||||
errorCode := "100003" // 登录失败
|
||||
if err.Error() == "配置未初始化" {
|
||||
statusCode = http.StatusInternalServerError
|
||||
errorCode = "900004" // 服务器内部错误
|
||||
}
|
||||
|
||||
c.JSON(statusCode, models.ErrorResponse{
|
||||
Code: statusCode,
|
||||
Error: "LOGIN_FAILED",
|
||||
Message: err.Error(),
|
||||
|
||||
lang := middleware.GetLanguage(c)
|
||||
status, errCode, message := i18n.NewI18nErrorResponse(errorCode, lang, err.Error())
|
||||
c.JSON(status, models.ErrorResponse{
|
||||
Code: errCode,
|
||||
Message: message,
|
||||
Timestamp: getCurrentTimestamp(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
lang := middleware.GetLanguage(c)
|
||||
successMessage := i18n.GetErrorMessage("000000", lang)
|
||||
c.JSON(http.StatusOK, models.LoginResponse{
|
||||
Code: http.StatusOK,
|
||||
Message: "登录成功",
|
||||
Message: successMessage,
|
||||
Data: data,
|
||||
})
|
||||
}
|
||||
@@ -80,9 +87,11 @@ func (h *AuthHandler) Logout(c *gin.Context) {
|
||||
// 对于JWT无状态认证,客户端删除Token即可
|
||||
// 这里可以记录登出日志或将Token加入黑名单(可选实现)
|
||||
|
||||
lang := middleware.GetLanguage(c)
|
||||
successMessage := i18n.GetErrorMessage("000000", lang)
|
||||
c.JSON(http.StatusOK, models.APIResponse{
|
||||
Code: http.StatusOK,
|
||||
Message: "登出成功",
|
||||
Message: successMessage,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -101,10 +110,11 @@ func (h *AuthHandler) RefreshToken(c *gin.Context) {
|
||||
// 从请求头获取Token
|
||||
token := extractTokenFromHeader(c)
|
||||
if token == "" {
|
||||
c.JSON(http.StatusUnauthorized, models.ErrorResponse{
|
||||
Code: http.StatusUnauthorized,
|
||||
Error: "TOKEN_MISSING",
|
||||
Message: "认证令牌缺失",
|
||||
lang := middleware.GetLanguage(c)
|
||||
status, errCode, message := i18n.NewI18nErrorResponse("100004", lang)
|
||||
c.JSON(status, models.ErrorResponse{
|
||||
Code: errCode,
|
||||
Message: message,
|
||||
Timestamp: getCurrentTimestamp(),
|
||||
})
|
||||
return
|
||||
@@ -112,18 +122,27 @@ func (h *AuthHandler) RefreshToken(c *gin.Context) {
|
||||
|
||||
newToken, err := h.authService.RefreshToken(token)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusUnauthorized, models.ErrorResponse{
|
||||
Code: http.StatusUnauthorized,
|
||||
Error: "TOKEN_REFRESH_FAILED",
|
||||
Message: "刷新令牌失败",
|
||||
// 根据错误类型选择合适的认证错误码
|
||||
errorCode := "100001" // token过期,可以刷新
|
||||
if err.Error() == "token无效" {
|
||||
errorCode = "100002" // token无效,需要重新登录
|
||||
}
|
||||
|
||||
lang := middleware.GetLanguage(c)
|
||||
status, errCode, message := i18n.NewI18nErrorResponse(errorCode, lang, err.Error())
|
||||
c.JSON(status, models.ErrorResponse{
|
||||
Code: errCode,
|
||||
Message: message,
|
||||
Timestamp: getCurrentTimestamp(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
lang := middleware.GetLanguage(c)
|
||||
successMessage := i18n.GetErrorMessage("000000", lang)
|
||||
c.JSON(http.StatusOK, models.APIResponse{
|
||||
Code: http.StatusOK,
|
||||
Message: "刷新成功",
|
||||
Message: successMessage,
|
||||
Data: map[string]interface{}{
|
||||
"token": newToken,
|
||||
},
|
||||
@@ -142,4 +161,4 @@ func extractTokenFromHeader(c *gin.Context) string {
|
||||
return authHeader[7:]
|
||||
}
|
||||
return ""
|
||||
}
|
||||
}
|
||||
@@ -7,10 +7,13 @@ import (
|
||||
// CORSMiddleware 跨域中间件
|
||||
func CORSMiddleware() gin.HandlerFunc {
|
||||
return gin.HandlerFunc(func(c *gin.Context) {
|
||||
// 允许所有域名访问
|
||||
c.Header("Access-Control-Allow-Origin", "*")
|
||||
|
||||
c.Header("Access-Control-Allow-Credentials", "true")
|
||||
c.Header("Access-Control-Allow-Headers", "Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization, accept, origin, Cache-Control, X-Requested-With")
|
||||
c.Header("Access-Control-Allow-Methods", "POST, OPTIONS, GET, PUT, DELETE")
|
||||
c.Header("Access-Control-Allow-Methods", "POST, OPTIONS, GET, PUT, DELETE, PATCH")
|
||||
c.Header("Access-Control-Max-Age", "86400") // 24小时
|
||||
|
||||
if c.Request.Method == "OPTIONS" {
|
||||
c.AbortWithStatus(204)
|
||||
@@ -19,4 +22,4 @@ func CORSMiddleware() gin.HandlerFunc {
|
||||
|
||||
c.Next()
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,212 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"license-manager/pkg/i18n"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
const (
|
||||
// LanguageKey 存储语言信息的上下文键
|
||||
LanguageKey = "language"
|
||||
|
||||
// DefaultLanguage 默认语言
|
||||
DefaultLanguage = "zh-CN"
|
||||
)
|
||||
|
||||
// I18nConfig 多语言中间件配置
|
||||
type I18nConfig struct {
|
||||
Enable bool `yaml:"enable"` // 是否启用多语言
|
||||
DefaultLang string `yaml:"default_lang"` // 默认语言
|
||||
SupportLangs []string `yaml:"support_langs"` // 支持的语言列表
|
||||
}
|
||||
|
||||
// I18nMiddleware 多语言检测中间件
|
||||
func I18nMiddleware(config *I18nConfig) gin.HandlerFunc {
|
||||
// 如果未启用多语言,使用默认语言
|
||||
if config == nil || !config.Enable {
|
||||
return func(c *gin.Context) {
|
||||
c.Set(LanguageKey, config.DefaultLang)
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
return func(c *gin.Context) {
|
||||
lang := detectLanguage(c, config)
|
||||
c.Set(LanguageKey, lang)
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
// detectLanguage 检测客户端语言偏好
|
||||
// 按优先级顺序:URL参数 > Accept-Language头 > 用户配置 > 系统默认
|
||||
func detectLanguage(c *gin.Context, config *I18nConfig) string {
|
||||
// 1. 检查URL参数 ?lang=en-US (最高优先级)
|
||||
if lang := c.Query("lang"); lang != "" {
|
||||
if isLanguageSupported(lang, config.SupportLangs) {
|
||||
return lang
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 检查HTTP头 Accept-Language
|
||||
if acceptLang := c.GetHeader("Accept-Language"); acceptLang != "" {
|
||||
if lang := parseAcceptLanguageHeader(acceptLang, config.SupportLangs); lang != "" {
|
||||
return lang
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 检查用户配置(如果有用户身份验证)
|
||||
// 这里可以从用户数据库或JWT token中获取用户语言偏好
|
||||
if userLang := getUserLanguagePreference(c); userLang != "" {
|
||||
if isLanguageSupported(userLang, config.SupportLangs) {
|
||||
return userLang
|
||||
}
|
||||
}
|
||||
|
||||
// 4. 返回系统默认语言
|
||||
return config.DefaultLang
|
||||
}
|
||||
|
||||
// parseAcceptLanguageHeader 解析Accept-Language头部
|
||||
// 例:zh-CN,zh;q=0.9,en;q=0.8,ja;q=0.7
|
||||
func parseAcceptLanguageHeader(acceptLang string, supportedLangs []string) string {
|
||||
if acceptLang == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
// 解析Accept-Language头部
|
||||
languages := strings.Split(acceptLang, ",")
|
||||
|
||||
// 按权重排序的语言偏好
|
||||
for _, langWithQ := range languages {
|
||||
// 移除权重信息,例如 "zh-CN;q=0.9" -> "zh-CN"
|
||||
lang := strings.TrimSpace(langWithQ)
|
||||
if idx := strings.Index(lang, ";"); idx != -1 {
|
||||
lang = lang[:idx]
|
||||
}
|
||||
|
||||
lang = strings.TrimSpace(lang)
|
||||
|
||||
// 检查是否支持该语言
|
||||
if isLanguageSupported(lang, supportedLangs) {
|
||||
return lang
|
||||
}
|
||||
|
||||
// 尝试匹配语言的主要部分,例如 "zh-CN" -> "zh"
|
||||
if idx := strings.Index(lang, "-"); idx != -1 {
|
||||
primaryLang := lang[:idx]
|
||||
// 查找支持的语言中是否有相同主要语言
|
||||
for _, supported := range supportedLangs {
|
||||
if strings.HasPrefix(supported, primaryLang+"-") {
|
||||
return supported
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
// getUserLanguagePreference 获取用户语言偏好(从用户配置或JWT中)
|
||||
func getUserLanguagePreference(c *gin.Context) string {
|
||||
// 这里可以实现从JWT token或用户数据库中获取用户语言偏好
|
||||
// 示例实现:从JWT Claims中获取语言偏好
|
||||
|
||||
// 1. 从JWT token中获取(如果有认证中间件)
|
||||
if userID, exists := c.Get("user_id"); exists && userID != nil {
|
||||
// 这里可以查询数据库获取用户的语言偏好
|
||||
// userLang := getUserLanguageFromDB(userID.(string))
|
||||
// return userLang
|
||||
_ = userID // 暂时未实现
|
||||
}
|
||||
|
||||
// 2. 从自定义头部获取
|
||||
if userLang := c.GetHeader("X-User-Language"); userLang != "" {
|
||||
return userLang
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
// isLanguageSupported 检查语言是否在支持列表中
|
||||
func isLanguageSupported(lang string, supportedLangs []string) bool {
|
||||
if len(supportedLangs) == 0 {
|
||||
// 如果没有配置支持语言列表,检查i18n管理器
|
||||
return i18n.IsLanguageSupported(lang)
|
||||
}
|
||||
|
||||
for _, supported := range supportedLangs {
|
||||
if supported == lang {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// GetLanguage 从gin.Context中获取语言设置
|
||||
func GetLanguage(c *gin.Context) string {
|
||||
if lang, exists := c.Get(LanguageKey); exists {
|
||||
if langStr, ok := lang.(string); ok {
|
||||
return langStr
|
||||
}
|
||||
}
|
||||
return DefaultLanguage
|
||||
}
|
||||
|
||||
// SetLanguage 设置当前请求的语言
|
||||
func SetLanguage(c *gin.Context, lang string) {
|
||||
c.Set(LanguageKey, lang)
|
||||
}
|
||||
|
||||
// 便利函数:创建不同配置的中间件
|
||||
|
||||
// DefaultI18nMiddleware 使用默认配置的多语言中间件
|
||||
func DefaultI18nMiddleware() gin.HandlerFunc {
|
||||
config := &I18nConfig{
|
||||
Enable: true,
|
||||
DefaultLang: "zh-CN",
|
||||
SupportLangs: []string{"zh-CN", "en-US", "ja-JP"},
|
||||
}
|
||||
return I18nMiddleware(config)
|
||||
}
|
||||
|
||||
// SimpleI18nMiddleware 简化的多语言中间件,仅支持指定语言
|
||||
func SimpleI18nMiddleware(defaultLang string, supportedLangs ...string) gin.HandlerFunc {
|
||||
config := &I18nConfig{
|
||||
Enable: true,
|
||||
DefaultLang: defaultLang,
|
||||
SupportLangs: supportedLangs,
|
||||
}
|
||||
return I18nMiddleware(config)
|
||||
}
|
||||
|
||||
// DisableI18nMiddleware 禁用多语言的中间件,总是使用默认语言
|
||||
func DisableI18nMiddleware(defaultLang string) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
c.Set(LanguageKey, defaultLang)
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
// I18nResponseHelper 多语言响应辅助函数
|
||||
type I18nResponseHelper struct {
|
||||
Context *gin.Context
|
||||
}
|
||||
|
||||
// NewI18nResponseHelper 创建多语言响应辅助器
|
||||
func NewI18nResponseHelper(c *gin.Context) *I18nResponseHelper {
|
||||
return &I18nResponseHelper{Context: c}
|
||||
}
|
||||
|
||||
// GetLang 获取当前请求的语言
|
||||
func (h *I18nResponseHelper) GetLang() string {
|
||||
return GetLanguage(h.Context)
|
||||
}
|
||||
|
||||
// ErrorResponse 创建多语言错误响应
|
||||
func (h *I18nResponseHelper) ErrorResponse(code string, customMessage ...string) (int, string, string) {
|
||||
lang := h.GetLang()
|
||||
return i18n.NewI18nErrorResponse(code, lang, customMessage...)
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
_ "license-manager/docs/swagger" // swagger docs
|
||||
"license-manager/internal/api/handlers"
|
||||
"license-manager/internal/api/middleware"
|
||||
"license-manager/internal/config"
|
||||
"license-manager/internal/service"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
@@ -19,6 +20,17 @@ func SetupRouter() *gin.Engine {
|
||||
router.Use(gin.Recovery())
|
||||
router.Use(middleware.CORSMiddleware())
|
||||
|
||||
// 多语言中间件
|
||||
cfg := config.GetConfig()
|
||||
if cfg.I18n.Enable {
|
||||
i18nConfig := &middleware.I18nConfig{
|
||||
Enable: cfg.I18n.Enable,
|
||||
DefaultLang: cfg.I18n.DefaultLang,
|
||||
SupportLangs: cfg.I18n.SupportLangs,
|
||||
}
|
||||
router.Use(middleware.I18nMiddleware(i18nConfig))
|
||||
}
|
||||
|
||||
// 初始化服务
|
||||
authService := service.NewAuthService()
|
||||
systemService := service.NewSystemService()
|
||||
@@ -31,7 +43,7 @@ func SetupRouter() *gin.Engine {
|
||||
router.GET("/health", systemHandler.HealthCheck)
|
||||
|
||||
// Swagger文档路由
|
||||
router.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))
|
||||
router.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler, ginSwagger.URL("/swagger/doc.json")))
|
||||
|
||||
// API v1 路由组
|
||||
v1 := router.Group("/api/v1")
|
||||
|
||||
@@ -8,9 +8,11 @@ import (
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Server ServerConfig `mapstructure:"server"`
|
||||
Auth AuthConfig `mapstructure:"auth"`
|
||||
Log LogConfig `mapstructure:"log"`
|
||||
Server ServerConfig `mapstructure:"server"`
|
||||
Database DatabaseConfig `mapstructure:"database"`
|
||||
Auth AuthConfig `mapstructure:"auth"`
|
||||
Log LogConfig `mapstructure:"log"`
|
||||
I18n I18nConfig `mapstructure:"i18n"`
|
||||
}
|
||||
|
||||
type ServerConfig struct {
|
||||
@@ -19,6 +21,22 @@ type ServerConfig struct {
|
||||
Mode string `mapstructure:"mode"`
|
||||
}
|
||||
|
||||
type DatabaseConfig struct {
|
||||
Driver string `mapstructure:"driver"`
|
||||
Host string `mapstructure:"host"`
|
||||
Port int `mapstructure:"port"`
|
||||
Username string `mapstructure:"username"`
|
||||
Password string `mapstructure:"password"`
|
||||
Database string `mapstructure:"database"`
|
||||
Charset string `mapstructure:"charset"`
|
||||
ParseTime bool `mapstructure:"parse_time"`
|
||||
Loc string `mapstructure:"loc"`
|
||||
MaxIdleConns int `mapstructure:"max_idle_conns"`
|
||||
MaxOpenConns int `mapstructure:"max_open_conns"`
|
||||
ConnMaxLifetime int `mapstructure:"conn_max_lifetime"`
|
||||
AutoMigrate bool `mapstructure:"auto_migrate"`
|
||||
}
|
||||
|
||||
type AuthConfig struct {
|
||||
JWT JWTConfig `mapstructure:"jwt"`
|
||||
Admin AdminConfig `mapstructure:"admin"`
|
||||
@@ -46,6 +64,13 @@ type LogConfig struct {
|
||||
Format string `mapstructure:"format"`
|
||||
}
|
||||
|
||||
type I18nConfig struct {
|
||||
Enable bool `mapstructure:"enable"` // 是否启用多语言
|
||||
DefaultLang string `mapstructure:"default_lang"` // 默认语言
|
||||
ConfigPath string `mapstructure:"config_path"` // 语言包路径
|
||||
SupportLangs []string `mapstructure:"support_langs"` // 支持的语言列表
|
||||
}
|
||||
|
||||
var AppConfig *Config
|
||||
|
||||
func Load(configPath string) error {
|
||||
@@ -66,11 +91,9 @@ func Load(configPath string) error {
|
||||
viper.AutomaticEnv()
|
||||
|
||||
if err := viper.ReadInConfig(); err != nil {
|
||||
log.Printf("Warning: Config file not found, using defaults: %v", err)
|
||||
log.Printf("Searched paths: %v", viper.ConfigFileUsed())
|
||||
} else {
|
||||
log.Printf("Using config file: %s", viper.ConfigFileUsed())
|
||||
return fmt.Errorf("failed to read config file: %w", err)
|
||||
}
|
||||
log.Printf("Using config file: %s", viper.ConfigFileUsed())
|
||||
|
||||
AppConfig = &Config{}
|
||||
if err := viper.Unmarshal(AppConfig); err != nil {
|
||||
@@ -86,6 +109,21 @@ func setDefaults() {
|
||||
viper.SetDefault("server.port", 18888)
|
||||
viper.SetDefault("server.mode", "debug")
|
||||
|
||||
// Database defaults
|
||||
viper.SetDefault("database.driver", "mysql")
|
||||
viper.SetDefault("database.host", "localhost")
|
||||
viper.SetDefault("database.port", 3306)
|
||||
viper.SetDefault("database.username", "root")
|
||||
viper.SetDefault("database.password", "password")
|
||||
viper.SetDefault("database.database", "license_manager")
|
||||
viper.SetDefault("database.charset", "utf8mb4")
|
||||
viper.SetDefault("database.parse_time", true)
|
||||
viper.SetDefault("database.loc", "Local")
|
||||
viper.SetDefault("database.max_idle_conns", 10)
|
||||
viper.SetDefault("database.max_open_conns", 100)
|
||||
viper.SetDefault("database.conn_max_lifetime", 3600)
|
||||
viper.SetDefault("database.auto_migrate", true)
|
||||
|
||||
// Auth defaults
|
||||
viper.SetDefault("auth.jwt.secret", "license-manager-default-secret-key")
|
||||
viper.SetDefault("auth.jwt.expire_hours", 1)
|
||||
@@ -100,6 +138,12 @@ func setDefaults() {
|
||||
// Log defaults
|
||||
viper.SetDefault("log.level", "info")
|
||||
viper.SetDefault("log.format", "json")
|
||||
|
||||
// I18n defaults
|
||||
viper.SetDefault("i18n.enable", true)
|
||||
viper.SetDefault("i18n.default_lang", "zh-CN")
|
||||
viper.SetDefault("i18n.config_path", "../configs/i18n/errors")
|
||||
viper.SetDefault("i18n.support_langs", []string{"zh-CN", "en-US", "ja-JP"})
|
||||
}
|
||||
|
||||
func GetConfig() *Config {
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"gorm.io/driver/mysql"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/logger"
|
||||
|
||||
"license-manager/internal/config"
|
||||
)
|
||||
|
||||
var DB *gorm.DB
|
||||
|
||||
// InitDatabase 初始化数据库连接
|
||||
func InitDatabase(cfg *config.DatabaseConfig) error {
|
||||
dsn := buildDSN(cfg)
|
||||
|
||||
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
|
||||
Logger: logger.Default.LogMode(getLogLevel()),
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to connect database: %w", err)
|
||||
}
|
||||
|
||||
// 获取底层的 SQL DB 实例
|
||||
sqlDB, err := db.DB()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get underlying sql.DB: %w", err)
|
||||
}
|
||||
|
||||
// 设置连接池参数
|
||||
sqlDB.SetMaxIdleConns(cfg.MaxIdleConns)
|
||||
sqlDB.SetMaxOpenConns(cfg.MaxOpenConns)
|
||||
sqlDB.SetConnMaxLifetime(time.Duration(cfg.ConnMaxLifetime) * time.Second)
|
||||
|
||||
// 测试连接
|
||||
if err := sqlDB.Ping(); err != nil {
|
||||
return fmt.Errorf("failed to ping database: %w", err)
|
||||
}
|
||||
|
||||
DB = db
|
||||
return nil
|
||||
}
|
||||
|
||||
// buildDSN 构建 MySQL 数据源名称
|
||||
func buildDSN(cfg *config.DatabaseConfig) string {
|
||||
return fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=%s&parseTime=%t&loc=%s",
|
||||
cfg.Username,
|
||||
cfg.Password,
|
||||
cfg.Host,
|
||||
cfg.Port,
|
||||
cfg.Database,
|
||||
cfg.Charset,
|
||||
cfg.ParseTime,
|
||||
cfg.Loc,
|
||||
)
|
||||
}
|
||||
|
||||
// getLogLevel 获取 GORM 日志级别
|
||||
func getLogLevel() logger.LogLevel {
|
||||
cfg := config.GetConfig()
|
||||
if cfg != nil && cfg.Server.Mode == "debug" {
|
||||
return logger.Info
|
||||
}
|
||||
return logger.Silent
|
||||
}
|
||||
|
||||
// GetDB 获取数据库实例
|
||||
func GetDB() *gorm.DB {
|
||||
return DB
|
||||
}
|
||||
|
||||
// Close 关闭数据库连接
|
||||
func Close() error {
|
||||
if DB != nil {
|
||||
sqlDB, err := DB.DB()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return sqlDB.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"license-manager/internal/config"
|
||||
"license-manager/internal/models"
|
||||
)
|
||||
|
||||
// AutoMigrate 执行自动数据库迁移
|
||||
func AutoMigrate() error {
|
||||
if DB == nil {
|
||||
return fmt.Errorf("database connection not initialized")
|
||||
}
|
||||
|
||||
log.Println("Starting database auto migration...")
|
||||
|
||||
// 迁移数据库表结构
|
||||
err := DB.AutoMigrate(
|
||||
&models.Customer{},
|
||||
&models.CustomerCodeSequence{},
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to migrate database: %w", err)
|
||||
}
|
||||
|
||||
// 初始化客户编码序列
|
||||
if err := initCustomerCodeSequence(); err != nil {
|
||||
return fmt.Errorf("failed to initialize customer code sequence: %w", err)
|
||||
}
|
||||
|
||||
log.Println("Database auto migration completed successfully")
|
||||
return nil
|
||||
}
|
||||
|
||||
// initCustomerCodeSequence 初始化客户编码序列
|
||||
func initCustomerCodeSequence() error {
|
||||
currentYear := time.Now().Year()
|
||||
|
||||
var sequence models.CustomerCodeSequence
|
||||
result := DB.Where("year = ?", currentYear).First(&sequence)
|
||||
|
||||
if result.Error != nil {
|
||||
// 记录不存在,创建新记录
|
||||
sequence = models.CustomerCodeSequence{
|
||||
Year: currentYear,
|
||||
SequenceNumber: 0,
|
||||
}
|
||||
if err := DB.Create(&sequence).Error; err != nil {
|
||||
return fmt.Errorf("failed to create customer code sequence: %w", err)
|
||||
}
|
||||
log.Printf("Initialized customer code sequence for year %d", currentYear)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// RunMigrationIfEnabled 根据配置决定是否运行迁移
|
||||
func RunMigrationIfEnabled() error {
|
||||
cfg := config.GetConfig()
|
||||
if cfg == nil {
|
||||
return fmt.Errorf("config not initialized")
|
||||
}
|
||||
|
||||
if cfg.Database.AutoMigrate {
|
||||
return AutoMigrate()
|
||||
}
|
||||
|
||||
log.Println("Auto migration is disabled in config")
|
||||
return nil
|
||||
}
|
||||
@@ -10,8 +10,7 @@ type APIResponse struct {
|
||||
|
||||
// ErrorResponse 错误响应结构
|
||||
type ErrorResponse struct {
|
||||
Code int `json:"code"`
|
||||
Error string `json:"error"`
|
||||
Message string `json:"message"`
|
||||
Timestamp string `json:"timestamp"`
|
||||
}
|
||||
Code string `json:"code"` // 业务错误码,如 "100001", "900001"
|
||||
Message string `json:"message"` // 错误描述信息
|
||||
Timestamp string `json:"timestamp"` // 错误发生时间
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// Customer 客户信息模型
|
||||
type Customer struct {
|
||||
ID string `gorm:"type:varchar(36);primaryKey;default:(UUID())" json:"id"`
|
||||
CustomerCode string `gorm:"type:varchar(20);uniqueIndex;not null" json:"customer_code"`
|
||||
CustomerName string `gorm:"type:varchar(200);not null;index" json:"customer_name"`
|
||||
CustomerType string `gorm:"type:varchar(20);not null;default:'enterprise';index" json:"customer_type"`
|
||||
ContactPerson string `gorm:"type:varchar(100);not null" json:"contact_person"`
|
||||
ContactTitle *string `gorm:"type:varchar(100)" json:"contact_title"`
|
||||
Email *string `gorm:"type:varchar(255)" json:"email"`
|
||||
Phone *string `gorm:"type:varchar(20)" json:"phone"`
|
||||
Address *string `gorm:"type:text" json:"address"`
|
||||
CompanySize *string `gorm:"type:varchar(20)" json:"company_size"`
|
||||
CustomerLevel string `gorm:"type:varchar(20);not null;default:'normal';index" json:"customer_level"`
|
||||
Status string `gorm:"type:varchar(20);not null;default:'active';index" json:"status"`
|
||||
Description *string `gorm:"type:text" json:"description"`
|
||||
CreatedAt time.Time `gorm:"not null;index" json:"created_at"`
|
||||
UpdatedAt time.Time `gorm:"not null" json:"updated_at"`
|
||||
CreatedBy string `gorm:"type:varchar(36);not null;index" json:"created_by"`
|
||||
UpdatedBy *string `gorm:"type:varchar(36)" json:"updated_by"`
|
||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
||||
}
|
||||
|
||||
// CustomerCodeSequence 客户编码序列模型
|
||||
type CustomerCodeSequence struct {
|
||||
Year int `gorm:"primaryKey" json:"year"`
|
||||
SequenceNumber int `gorm:"not null;default:0" json:"sequence_number"`
|
||||
}
|
||||
|
||||
// TableName 指定表名
|
||||
func (Customer) TableName() string {
|
||||
return "customers"
|
||||
}
|
||||
|
||||
// BeforeCreate 创建前自动设置时间戳
|
||||
// 确保数据完整性,避免依赖数据库默认值(符合失败快速原则)
|
||||
func (c *Customer) BeforeCreate(tx *gorm.DB) error {
|
||||
now := time.Now()
|
||||
if c.CreatedAt.IsZero() {
|
||||
c.CreatedAt = now
|
||||
}
|
||||
if c.UpdatedAt.IsZero() {
|
||||
c.UpdatedAt = now
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// BeforeUpdate 更新前自动刷新更新时间
|
||||
// 保持数据时间戳的准确性和一致性
|
||||
func (c *Customer) BeforeUpdate(tx *gorm.DB) error {
|
||||
c.UpdatedAt = time.Now()
|
||||
return nil
|
||||
}
|
||||
|
||||
// TableName 指定表名
|
||||
func (CustomerCodeSequence) TableName() string {
|
||||
return "customer_code_sequence"
|
||||
}
|
||||
@@ -67,4 +67,4 @@ func (s *authService) RefreshToken(token string) (string, error) {
|
||||
func (s *authService) ValidateToken(token string) error {
|
||||
_, err := utils.ValidateToken(token)
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
-- 客户表结构
|
||||
-- 基于客户属性设计与Web显示方案创建
|
||||
-- MySQL版本
|
||||
|
||||
-- 创建客户表
|
||||
CREATE TABLE customers (
|
||||
-- 基础标识
|
||||
id VARCHAR(36) PRIMARY KEY DEFAULT (UUID()),
|
||||
customer_code VARCHAR(20) NOT NULL UNIQUE,
|
||||
customer_name VARCHAR(200) NOT NULL,
|
||||
customer_type VARCHAR(20) NOT NULL DEFAULT 'enterprise' COMMENT '客户类型: individual, enterprise, government, education',
|
||||
|
||||
-- 联系信息
|
||||
contact_person VARCHAR(100) NOT NULL,
|
||||
contact_title VARCHAR(100),
|
||||
email VARCHAR(255),
|
||||
phone VARCHAR(20),
|
||||
|
||||
-- 地址信息
|
||||
address TEXT,
|
||||
|
||||
-- 商业属性
|
||||
company_size VARCHAR(20) COMMENT '企业规模: small, medium, large, enterprise',
|
||||
|
||||
-- 客户分级
|
||||
customer_level VARCHAR(20) NOT NULL DEFAULT 'normal' COMMENT '客户等级: normal, vip, enterprise, strategic',
|
||||
|
||||
-- 状态管理
|
||||
status VARCHAR(20) NOT NULL DEFAULT 'active' COMMENT '状态: active, disabled',
|
||||
description TEXT,
|
||||
|
||||
-- 时间字段 (使用DATETIME(3)匹配Go的time.Time和GORM的精度要求)
|
||||
created_at DATETIME(3) NOT NULL,
|
||||
updated_at DATETIME(3) NOT NULL,
|
||||
created_by VARCHAR(36) NOT NULL,
|
||||
updated_by VARCHAR(36),
|
||||
deleted_at DATETIME(3) NULL -- 软删除字段,匹配gorm.DeletedAt
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='客户信息表';
|
||||
|
||||
-- MySQL的列注释已在CREATE TABLE语句中添加
|
||||
|
||||
-- 创建索引
|
||||
CREATE INDEX idx_customers_customer_name ON customers(customer_name);
|
||||
CREATE INDEX idx_customers_customer_type ON customers(customer_type);
|
||||
CREATE INDEX idx_customers_customer_level ON customers(customer_level);
|
||||
CREATE INDEX idx_customers_status ON customers(status);
|
||||
CREATE INDEX idx_customers_created_at ON customers(created_at);
|
||||
CREATE INDEX idx_customers_created_by ON customers(created_by);
|
||||
CREATE INDEX idx_customers_deleted_at ON customers(deleted_at); -- 软删除索引,提高查询性能
|
||||
|
||||
-- 注意:时间戳由Go应用程序管理,不依赖数据库默认值
|
||||
-- 这样可以确保跨数据库兼容性并符合失败快速原则
|
||||
|
||||
-- 创建客户编码序列表
|
||||
CREATE TABLE customer_code_sequence (
|
||||
year INT PRIMARY KEY,
|
||||
sequence_number INT NOT NULL DEFAULT 0
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='客户编码序列表';
|
||||
|
||||
-- 插入当前年份的序列记录
|
||||
INSERT INTO customer_code_sequence (year, sequence_number)
|
||||
VALUES (YEAR(NOW()), 0)
|
||||
ON DUPLICATE KEY UPDATE sequence_number = sequence_number;
|
||||
|
||||
-- 注意:MySQL版本简化了客户编码生成逻辑
|
||||
-- 客户编码将在应用层生成,格式:CUS-YYYY-NNNN
|
||||
-- 可以通过以下SQL获取下一个序列号:
|
||||
-- UPDATE customer_code_sequence SET sequence_number = sequence_number + 1 WHERE year = YEAR(NOW());
|
||||
-- SELECT sequence_number FROM customer_code_sequence WHERE year = YEAR(NOW());
|
||||
@@ -0,0 +1,98 @@
|
||||
-- 示例数据插入
|
||||
-- 基于客户属性设计文档的示例数据
|
||||
-- MySQL版本
|
||||
|
||||
-- 先更新客户编码序列表
|
||||
INSERT INTO customer_code_sequence (year, sequence_number)
|
||||
VALUES (YEAR(NOW()), 5)
|
||||
ON DUPLICATE KEY UPDATE sequence_number = 5;
|
||||
|
||||
-- 插入示例客户数据
|
||||
-- 注意:created_at和updated_at将由应用程序自动设置,这里不需要指定
|
||||
INSERT INTO customers (
|
||||
customer_code, customer_name, customer_type,
|
||||
contact_person, contact_title, email, phone,
|
||||
address, company_size,
|
||||
customer_level, status, description,
|
||||
created_at, updated_at, created_by
|
||||
) VALUES
|
||||
-- 示例1:VIP企业客户
|
||||
(
|
||||
CONCAT('CUS-', YEAR(NOW()), '-0001'),
|
||||
'北京科技有限公司',
|
||||
'enterprise',
|
||||
'张三',
|
||||
'技术总监',
|
||||
'[email protected]',
|
||||
'13800138000',
|
||||
'北京市朝阳区科技园区100号',
|
||||
'medium',
|
||||
'vip',
|
||||
'active',
|
||||
'重要客户,IoT平台采购方,需要优先技术支持',
|
||||
NOW(3), NOW(3), UUID()
|
||||
),
|
||||
-- 示例2:普通个人客户
|
||||
(
|
||||
CONCAT('CUS-', YEAR(NOW()), '-0002'),
|
||||
'李四',
|
||||
'individual',
|
||||
'李四',
|
||||
NULL,
|
||||
'[email protected]',
|
||||
'13900139000',
|
||||
'上海市浦东新区张江高科技园区200号',
|
||||
NULL,
|
||||
'normal',
|
||||
'active',
|
||||
'个人开发者,主要用于学习和小型项目',
|
||||
NOW(3), NOW(3), UUID()
|
||||
),
|
||||
-- 示例3:政府客户
|
||||
(
|
||||
CONCAT('CUS-', YEAR(NOW()), '-0003'),
|
||||
'某市政府信息化中心',
|
||||
'government',
|
||||
'王五',
|
||||
'信息化处处长',
|
||||
'[email protected]',
|
||||
'010-12345678',
|
||||
'某市政府大楼信息化中心',
|
||||
'large',
|
||||
'enterprise',
|
||||
'active',
|
||||
'政府客户,智慧城市项目,需要专项技术支持',
|
||||
NOW(3), NOW(3), UUID()
|
||||
),
|
||||
-- 示例4:教育客户
|
||||
(
|
||||
CONCAT('CUS-', YEAR(NOW()), '-0004'),
|
||||
'清华大学计算机学院',
|
||||
'education',
|
||||
'赵六',
|
||||
'实验室主任',
|
||||
'[email protected]',
|
||||
'010-62785678',
|
||||
'北京市海淀区清华大学计算机学院',
|
||||
'large',
|
||||
'strategic',
|
||||
'active',
|
||||
'重点高校合作伙伴,科研项目合作,享受教育优惠政策',
|
||||
NOW(3), NOW(3), UUID()
|
||||
),
|
||||
-- 示例5:禁用状态的客户
|
||||
(
|
||||
CONCAT('CUS-', YEAR(NOW()), '-0005'),
|
||||
'测试公司',
|
||||
'enterprise',
|
||||
'测试用户',
|
||||
'测试经理',
|
||||
'[email protected]',
|
||||
'13700137000',
|
||||
'测试地址',
|
||||
'small',
|
||||
'normal',
|
||||
'disabled',
|
||||
'测试客户,已禁用,用于演示禁用状态',
|
||||
NOW(3), NOW(3), UUID()
|
||||
);
|
||||
@@ -0,0 +1,79 @@
|
||||
# SQL 数据库迁移文件
|
||||
|
||||
本目录包含许可证管理系统的数据库结构定义和数据迁移文件。
|
||||
|
||||
## 目录结构
|
||||
|
||||
```
|
||||
sql/
|
||||
├── migrations/ # 数据库迁移文件
|
||||
│ ├── 001_create_customers_table.sql # 客户表结构
|
||||
│ └── 002_insert_sample_data.sql # 示例数据
|
||||
└── README.md # 本文件
|
||||
```
|
||||
|
||||
## 数据库支持
|
||||
|
||||
- **MySQL** (推荐版本 8.0+)
|
||||
- 兼容 MariaDB (版本 10.3+)
|
||||
|
||||
## 迁移文件说明
|
||||
|
||||
### 001_create_customers_table.sql
|
||||
创建客户相关的数据库结构,包括:
|
||||
|
||||
- **客户主表** (`customers`):存储客户的所有基本信息,使用VARCHAR类型替代枚举
|
||||
- **客户编码序列表** (`customer_code_sequence`):用于自动生成客户编码
|
||||
- **自动时间戳**:MySQL原生支持自动更新时间戳功能
|
||||
- **索引优化**:为常用查询字段创建索引
|
||||
|
||||
### 002_insert_sample_data.sql
|
||||
插入示例数据,包括:
|
||||
- VIP企业客户
|
||||
- 普通个人客户
|
||||
- 政府客户
|
||||
- 教育客户
|
||||
- 禁用状态客户
|
||||
|
||||
## 执行顺序
|
||||
|
||||
按文件名的数字前缀顺序执行:
|
||||
|
||||
```bash
|
||||
# MySQL
|
||||
mysql -u username -p database_name < migrations/001_create_customers_table.sql
|
||||
mysql -u username -p database_name < migrations/002_insert_sample_data.sql
|
||||
```
|
||||
|
||||
## 客户编码规则
|
||||
|
||||
- 格式:`CUS-YYYY-NNNN`
|
||||
- 示例:`CUS-2025-0001`
|
||||
- 自动递增,按年度重置
|
||||
|
||||
## 主要功能特性
|
||||
|
||||
1. **自动时间戳**:创建和更新时间自动管理(使用MySQL原生功能)
|
||||
2. **UUID主键**:使用UUID作为主键,保证全局唯一性
|
||||
3. **字符串约束**:使用VARCHAR类型替代枚举,通过注释说明取值范围
|
||||
4. **索引优化**:为常用查询字段创建索引
|
||||
5. **简化设计**:移除复杂的触发器和函数,提高可维护性
|
||||
6. **注释完整**:表和字段都有详细的中文注释
|
||||
|
||||
## 数据类型映射
|
||||
|
||||
| 设计文档字段 | MySQL类型 | 说明 |
|
||||
|-------------|-----------|------|
|
||||
| id | VARCHAR(36) | 主键,UUID格式 |
|
||||
| customer_code | VARCHAR(20) | 唯一客户编码 |
|
||||
| customer_type | VARCHAR(20) | 字符串:individual/enterprise/government/education |
|
||||
| email | VARCHAR(255) | 唯一邮箱地址 |
|
||||
| created_at | TIMESTAMP | 时间戳,自动生成 |
|
||||
| status | VARCHAR(20) | 字符串:active/disabled |
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. 执行前请确保MySQL版本支持 `UUID()` 函数(MySQL 8.0+)
|
||||
2. 客户编码生成逻辑简化,建议在应用层实现自动生成
|
||||
3. 字段约束通过应用层验证,确保数据一致性
|
||||
4. 示例数据仅用于开发和测试,生产环境请谨慎使用
|
||||
@@ -0,0 +1,74 @@
|
||||
package errors
|
||||
|
||||
// HTTP 状态码常量
|
||||
const (
|
||||
StatusOK = 200
|
||||
StatusBadRequest = 400
|
||||
StatusUnauthorized = 401
|
||||
StatusForbidden = 403
|
||||
StatusNotFound = 404
|
||||
StatusConflict = 409
|
||||
StatusInternalServerError = 500
|
||||
)
|
||||
|
||||
// ErrorMessages 业务错误码到错误信息的映射
|
||||
// 六位数字格式:前2位模块码 + 后4位错误号
|
||||
var ErrorMessages = map[string]string{
|
||||
"000000": "成功",
|
||||
|
||||
// 10xxxx - 认证模块
|
||||
"100001": "认证已过期",
|
||||
"100002": "认证无效",
|
||||
"100003": "用户名或密码错误",
|
||||
"100004": "缺少认证信息",
|
||||
"100005": "权限不足",
|
||||
|
||||
// 90xxxx - 系统通用错误
|
||||
"900001": "请求参数无效",
|
||||
"900002": "资源不存在",
|
||||
"900003": "资源冲突",
|
||||
"900004": "服务器内部错误",
|
||||
}
|
||||
|
||||
// GetErrorMessage 获取错误码对应的错误信息
|
||||
func GetErrorMessage(code string) string {
|
||||
if msg, ok := ErrorMessages[code]; ok {
|
||||
return msg
|
||||
}
|
||||
return "未知错误"
|
||||
}
|
||||
|
||||
// NewErrorResponse 创建标准错误响应,返回 HTTP 状态码、错误码、错误消息
|
||||
// 大部分业务错误返回 200,只有协议级错误才返回对应 HTTP 状态码
|
||||
func NewErrorResponse(code string, customMessage ...string) (int, string, string) {
|
||||
httpStatus := getHTTPStatusByCode(code)
|
||||
message := GetErrorMessage(code)
|
||||
|
||||
if len(customMessage) > 0 && customMessage[0] != "" {
|
||||
message = customMessage[0]
|
||||
}
|
||||
|
||||
return httpStatus, code, message
|
||||
}
|
||||
|
||||
// getHTTPStatusByCode 根据业务错误码获取对应的 HTTP 状态码
|
||||
func getHTTPStatusByCode(code string) int {
|
||||
switch code {
|
||||
case "000000": // 成功
|
||||
return StatusOK
|
||||
case "900001": // 请求参数无效
|
||||
return StatusBadRequest
|
||||
case "100001", "100002", "100003", "100004": // 认证相关错误
|
||||
return StatusUnauthorized
|
||||
case "100005": // 权限不足
|
||||
return StatusForbidden
|
||||
case "900002": // 资源不存在
|
||||
return StatusNotFound
|
||||
case "900003": // 资源冲突
|
||||
return StatusConflict
|
||||
case "900004": // 服务器内部错误
|
||||
return StatusInternalServerError
|
||||
default:
|
||||
return StatusInternalServerError
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,219 @@
|
||||
package i18n
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// ErrorI18n 错误信息国际化封装
|
||||
type ErrorI18n struct {
|
||||
manager I18nManager
|
||||
}
|
||||
|
||||
// NewErrorI18n 创建新的错误国际化实例
|
||||
func NewErrorI18n(manager I18nManager) *ErrorI18n {
|
||||
return &ErrorI18n{
|
||||
manager: manager,
|
||||
}
|
||||
}
|
||||
|
||||
// NewI18nErrorResponse 创建多语言错误响应
|
||||
// 扩展现有错误响应函数,支持多语言
|
||||
// 返回:HTTP状态码、错误码、本地化错误消息
|
||||
func NewI18nErrorResponse(code, lang string, customMessage ...string) (int, string, string) {
|
||||
// 获取本地化错误信息
|
||||
message := GetI18nErrorMessage(code, lang)
|
||||
|
||||
// 支持自定义消息覆盖
|
||||
if len(customMessage) > 0 && customMessage[0] != "" {
|
||||
message = customMessage[0]
|
||||
}
|
||||
|
||||
// 获取对应的HTTP状态码
|
||||
httpStatus := getHTTPStatusByCode(code)
|
||||
|
||||
return httpStatus, code, message
|
||||
}
|
||||
|
||||
// GetI18nErrorMessage 获取多语言错误信息
|
||||
func GetI18nErrorMessage(code, lang string) string {
|
||||
if globalManager == nil {
|
||||
// 如果多语言系统未初始化,降级到默认错误处理
|
||||
return getDefaultErrorMessage(code)
|
||||
}
|
||||
|
||||
return globalManager.GetErrorMessage(code, lang)
|
||||
}
|
||||
|
||||
// GetI18nErrorMessageWithManager 使用指定管理器获取多语言错误信息
|
||||
func GetI18nErrorMessageWithManager(manager I18nManager, code, lang string) string {
|
||||
if manager == nil {
|
||||
return getDefaultErrorMessage(code)
|
||||
}
|
||||
|
||||
return manager.GetErrorMessage(code, lang)
|
||||
}
|
||||
|
||||
// getDefaultErrorMessage 获取默认错误信息(作为降级方案)
|
||||
func getDefaultErrorMessage(code string) string {
|
||||
// 这里可以集成原有的 ErrorMessages 映射作为降级方案
|
||||
defaultMessages := map[string]string{
|
||||
"000000": "Success",
|
||||
"100001": "Authentication expired",
|
||||
"100002": "Invalid authentication",
|
||||
"100003": "Invalid username or password",
|
||||
"100004": "Authentication required",
|
||||
"100005": "Insufficient permissions",
|
||||
"200001": "Customer not found",
|
||||
"200002": "Customer already exists",
|
||||
"200003": "Customer name is required",
|
||||
"200004": "Invalid customer information format",
|
||||
"300001": "Invalid license key",
|
||||
"300002": "License expired",
|
||||
"300003": "License key is required",
|
||||
"300004": "License key already used",
|
||||
"900001": "Invalid request parameters",
|
||||
"900002": "Resource not found",
|
||||
"900003": "Resource conflict",
|
||||
"900004": "Internal server error",
|
||||
}
|
||||
|
||||
if msg, ok := defaultMessages[code]; ok {
|
||||
return msg
|
||||
}
|
||||
return "Unknown error"
|
||||
}
|
||||
|
||||
// getHTTPStatusByCode 根据业务错误码获取对应的 HTTP 状态码
|
||||
// 保持与原有错误处理逻辑一致
|
||||
func getHTTPStatusByCode(code string) int {
|
||||
const (
|
||||
StatusOK = 200
|
||||
StatusBadRequest = 400
|
||||
StatusUnauthorized = 401
|
||||
StatusForbidden = 403
|
||||
StatusNotFound = 404
|
||||
StatusConflict = 409
|
||||
StatusInternalServerError = 500
|
||||
)
|
||||
|
||||
switch code {
|
||||
case "000000": // 成功
|
||||
return StatusOK
|
||||
case "900001": // 请求参数无效
|
||||
return StatusBadRequest
|
||||
case "100001", "100002", "100003", "100004": // 认证相关错误
|
||||
return StatusUnauthorized
|
||||
case "100005": // 权限不足
|
||||
return StatusForbidden
|
||||
case "900002", "200001", "300001": // 资源不存在
|
||||
return StatusNotFound
|
||||
case "900003", "200002", "300004": // 资源冲突
|
||||
return StatusConflict
|
||||
case "900004": // 服务器内部错误
|
||||
return StatusInternalServerError
|
||||
default:
|
||||
// 根据错误码前缀判断
|
||||
if len(code) >= 2 {
|
||||
prefix := code[:2]
|
||||
switch prefix {
|
||||
case "10": // 认证模块错误,默认401
|
||||
return StatusUnauthorized
|
||||
case "20": // 客户模块错误,默认404
|
||||
return StatusNotFound
|
||||
case "30": // 授权模块错误,默认404
|
||||
return StatusNotFound
|
||||
case "90": // 系统错误,默认500
|
||||
return StatusInternalServerError
|
||||
}
|
||||
}
|
||||
return StatusInternalServerError
|
||||
}
|
||||
}
|
||||
|
||||
// I18nError 多语言错误结构体
|
||||
type I18nError struct {
|
||||
Code string
|
||||
Lang string
|
||||
Message string
|
||||
HttpCode int
|
||||
}
|
||||
|
||||
// Error 实现 error 接口
|
||||
func (e *I18nError) Error() string {
|
||||
return fmt.Sprintf("[%s] %s", e.Code, e.Message)
|
||||
}
|
||||
|
||||
// NewI18nError 创建多语言错误
|
||||
func NewI18nError(code, lang string, customMessage ...string) *I18nError {
|
||||
httpCode, _, message := NewI18nErrorResponse(code, lang, customMessage...)
|
||||
|
||||
return &I18nError{
|
||||
Code: code,
|
||||
Lang: lang,
|
||||
Message: message,
|
||||
HttpCode: httpCode,
|
||||
}
|
||||
}
|
||||
|
||||
// 便利函数:常用错误码的快速创建方法
|
||||
|
||||
// NewAuthError 创建认证错误
|
||||
func NewAuthError(lang string, code ...string) *I18nError {
|
||||
errorCode := "100001" // 默认认证过期
|
||||
if len(code) > 0 && code[0] != "" {
|
||||
errorCode = code[0]
|
||||
}
|
||||
return NewI18nError(errorCode, lang)
|
||||
}
|
||||
|
||||
// NewValidationError 创建验证错误
|
||||
func NewValidationError(lang string) *I18nError {
|
||||
return NewI18nError("900001", lang)
|
||||
}
|
||||
|
||||
// NewNotFoundError 创建资源不存在错误
|
||||
func NewNotFoundError(lang string) *I18nError {
|
||||
return NewI18nError("900002", lang)
|
||||
}
|
||||
|
||||
// NewConflictError 创建资源冲突错误
|
||||
func NewConflictError(lang string) *I18nError {
|
||||
return NewI18nError("900003", lang)
|
||||
}
|
||||
|
||||
// NewInternalError 创建内部服务器错误
|
||||
func NewInternalError(lang string) *I18nError {
|
||||
return NewI18nError("900004", lang)
|
||||
}
|
||||
|
||||
// 批量错误处理函数
|
||||
|
||||
// ValidateI18nSupport 验证多语言支持状态
|
||||
func ValidateI18nSupport() error {
|
||||
if globalManager == nil {
|
||||
return fmt.Errorf("i18n manager not initialized")
|
||||
}
|
||||
|
||||
supportedLangs := globalManager.SupportedLanguages()
|
||||
if len(supportedLangs) == 0 {
|
||||
return fmt.Errorf("no languages loaded")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetSupportedLanguages 获取支持的语言列表
|
||||
func GetSupportedLanguages() []string {
|
||||
if globalManager == nil {
|
||||
return []string{}
|
||||
}
|
||||
return globalManager.SupportedLanguages()
|
||||
}
|
||||
|
||||
// IsLanguageSupported 检查语言是否支持
|
||||
func IsLanguageSupported(lang string) bool {
|
||||
if globalManager == nil {
|
||||
return false
|
||||
}
|
||||
return globalManager.IsLanguageSupported(lang)
|
||||
}
|
||||
@@ -0,0 +1,235 @@
|
||||
package i18n
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
// SystemInfo 语言包系统信息
|
||||
type SystemInfo struct {
|
||||
Name string `yaml:"name"`
|
||||
Locale string `yaml:"locale"`
|
||||
}
|
||||
|
||||
// LanguageData 语言包数据结构
|
||||
type LanguageData struct {
|
||||
System SystemInfo `yaml:"system"`
|
||||
Errors map[string]interface{} `yaml:"errors"` // 灵活结构:支持直接字符串和嵌套对象
|
||||
DefaultError string `yaml:"default_error"`
|
||||
flatErrors map[string]string // 扁平化的错误码映射(内部使用)
|
||||
}
|
||||
|
||||
// Manager 多语言管理器
|
||||
type Manager struct {
|
||||
defaultLang string
|
||||
loadedLangs map[string]*LanguageData
|
||||
configPath string
|
||||
mutex sync.RWMutex
|
||||
}
|
||||
|
||||
// I18nManager 多语言管理器接口
|
||||
type I18nManager interface {
|
||||
LoadLanguage(lang string) error
|
||||
GetErrorMessage(code, lang string) string
|
||||
SupportedLanguages() []string
|
||||
SetDefaultLanguage(lang string)
|
||||
IsLanguageSupported(lang string) bool
|
||||
}
|
||||
|
||||
// NewManager 创建新的多语言管理器
|
||||
func NewManager(configPath, defaultLang string) *Manager {
|
||||
return &Manager{
|
||||
defaultLang: defaultLang,
|
||||
loadedLangs: make(map[string]*LanguageData),
|
||||
configPath: configPath,
|
||||
}
|
||||
}
|
||||
|
||||
// LoadLanguage 加载指定语言包
|
||||
func (m *Manager) LoadLanguage(lang string) error {
|
||||
m.mutex.Lock()
|
||||
defer m.mutex.Unlock()
|
||||
|
||||
// 检查是否已加载
|
||||
if _, exists := m.loadedLangs[lang]; exists {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 构建语言文件路径
|
||||
filePath := filepath.Join(m.configPath, fmt.Sprintf("%s.yaml", lang))
|
||||
|
||||
// 读取文件内容
|
||||
data, err := ioutil.ReadFile(filePath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read language file %s: %v", filePath, err)
|
||||
}
|
||||
|
||||
// 解析YAML
|
||||
var langData LanguageData
|
||||
if err := yaml.Unmarshal(data, &langData); err != nil {
|
||||
return fmt.Errorf("failed to parse language file %s: %v", filePath, err)
|
||||
}
|
||||
|
||||
// 扁平化错误码映射,便于快速查找
|
||||
langData.flatErrors = make(map[string]string)
|
||||
|
||||
// 处理错误码映射
|
||||
for key, value := range langData.Errors {
|
||||
switch v := value.(type) {
|
||||
case string:
|
||||
// 直接定义的错误码(如 "000000": "成功")
|
||||
langData.flatErrors[key] = v
|
||||
case map[interface{}]interface{}:
|
||||
// 嵌套模块的错误码(如 auth: {"100001": "xxx"})
|
||||
for subKey, subValue := range v {
|
||||
if code, ok := subKey.(string); ok {
|
||||
if msg, ok := subValue.(string); ok {
|
||||
langData.flatErrors[code] = msg
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 存储加载的语言数据
|
||||
m.loadedLangs[lang] = &langData
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetErrorMessage 获取指定语言的错误信息
|
||||
func (m *Manager) GetErrorMessage(code, lang string) string {
|
||||
m.mutex.RLock()
|
||||
defer m.mutex.RUnlock()
|
||||
|
||||
// 首先尝试获取请求语言的错误信息
|
||||
if langData, exists := m.loadedLangs[lang]; exists {
|
||||
if message, found := langData.flatErrors[code]; found {
|
||||
return message
|
||||
}
|
||||
// 如果错误码不存在,返回该语言的默认错误信息
|
||||
if langData.DefaultError != "" {
|
||||
return langData.DefaultError
|
||||
}
|
||||
}
|
||||
|
||||
// 如果请求语言不存在或错误码不存在,降级到默认语言
|
||||
if lang != m.defaultLang {
|
||||
if langData, exists := m.loadedLangs[m.defaultLang]; exists {
|
||||
if message, found := langData.flatErrors[code]; found {
|
||||
return message
|
||||
}
|
||||
// 返回默认语言的默认错误信息
|
||||
if langData.DefaultError != "" {
|
||||
return langData.DefaultError
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 最终降级:返回硬编码的默认错误信息
|
||||
return "Unknown error"
|
||||
}
|
||||
|
||||
// SupportedLanguages 返回已加载的语言列表
|
||||
func (m *Manager) SupportedLanguages() []string {
|
||||
m.mutex.RLock()
|
||||
defer m.mutex.RUnlock()
|
||||
|
||||
languages := make([]string, 0, len(m.loadedLangs))
|
||||
for lang := range m.loadedLangs {
|
||||
languages = append(languages, lang)
|
||||
}
|
||||
return languages
|
||||
}
|
||||
|
||||
// SetDefaultLanguage 设置默认语言
|
||||
func (m *Manager) SetDefaultLanguage(lang string) {
|
||||
m.mutex.Lock()
|
||||
defer m.mutex.Unlock()
|
||||
m.defaultLang = lang
|
||||
}
|
||||
|
||||
// IsLanguageSupported 检查语言是否被支持
|
||||
func (m *Manager) IsLanguageSupported(lang string) bool {
|
||||
m.mutex.RLock()
|
||||
defer m.mutex.RUnlock()
|
||||
_, exists := m.loadedLangs[lang]
|
||||
return exists
|
||||
}
|
||||
|
||||
// GetLanguageInfo 获取语言包信息
|
||||
func (m *Manager) GetLanguageInfo(lang string) *SystemInfo {
|
||||
m.mutex.RLock()
|
||||
defer m.mutex.RUnlock()
|
||||
|
||||
if langData, exists := m.loadedLangs[lang]; exists {
|
||||
return &langData.System
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// parseAcceptLanguage 解析 Accept-Language 头部
|
||||
func ParseAcceptLanguage(acceptLang string) string {
|
||||
if acceptLang == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
// 简单解析 Accept-Language 头部,获取权重最高的语言
|
||||
// 例:zh-CN,zh;q=0.9,en;q=0.8
|
||||
languages := strings.Split(acceptLang, ",")
|
||||
if len(languages) > 0 {
|
||||
// 取第一个语言(权重最高)
|
||||
firstLang := strings.TrimSpace(languages[0])
|
||||
// 移除权重信息
|
||||
if idx := strings.Index(firstLang, ";"); idx != -1 {
|
||||
firstLang = firstLang[:idx]
|
||||
}
|
||||
return strings.TrimSpace(firstLang)
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
// 全局管理器实例(单例模式)
|
||||
var globalManager *Manager
|
||||
var once sync.Once
|
||||
|
||||
// InitGlobalManager 初始化全局管理器
|
||||
func InitGlobalManager(configPath, defaultLang string) error {
|
||||
var initErr error
|
||||
once.Do(func() {
|
||||
globalManager = NewManager(configPath, defaultLang)
|
||||
|
||||
// 预加载默认语言
|
||||
if err := globalManager.LoadLanguage(defaultLang); err != nil {
|
||||
initErr = fmt.Errorf("failed to load default language %s: %v", defaultLang, err)
|
||||
}
|
||||
})
|
||||
return initErr
|
||||
}
|
||||
|
||||
// GetGlobalManager 获取全局管理器实例
|
||||
func GetGlobalManager() *Manager {
|
||||
return globalManager
|
||||
}
|
||||
|
||||
// GetErrorMessage 全局函数:获取错误信息
|
||||
func GetErrorMessage(code, lang string) string {
|
||||
if globalManager == nil {
|
||||
return "I18n not initialized"
|
||||
}
|
||||
return globalManager.GetErrorMessage(code, lang)
|
||||
}
|
||||
|
||||
// LoadLanguage 全局函数:加载语言
|
||||
func LoadLanguage(lang string) error {
|
||||
if globalManager == nil {
|
||||
return fmt.Errorf("I18n not initialized")
|
||||
}
|
||||
return globalManager.LoadLanguage(lang)
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
# MySQL 8数据库 - 生产环境优化配置
|
||||
mysql:
|
||||
image: mysql:8.0
|
||||
container_name: license-manager-mysql-prod
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD:-root@123}
|
||||
MYSQL_DATABASE: license_manager
|
||||
MYSQL_USER: license_user
|
||||
MYSQL_PASSWORD: ${MYSQL_PASSWORD:-license@123}
|
||||
TZ: Asia/Shanghai
|
||||
ports:
|
||||
- "${MYSQL_PORT:-3306}:3306"
|
||||
volumes:
|
||||
- mysql_data:/var/lib/mysql
|
||||
- ./mysql/conf.d:/etc/mysql/conf.d
|
||||
command: >
|
||||
--default-authentication-plugin=mysql_native_password
|
||||
--character-set-server=utf8mb4
|
||||
--collation-server=utf8mb4_unicode_ci
|
||||
--sql_mode=STRICT_TRANS_TABLES,NO_ZERO_DATE,NO_ZERO_IN_DATE,ERROR_FOR_DIVISION_BY_ZERO
|
||||
--max_connections=2000
|
||||
--innodb_buffer_pool_size=512M
|
||||
--innodb_log_file_size=256M
|
||||
--slow_query_log=1
|
||||
--slow_query_log_file=/var/lib/mysql/mysql-slow.log
|
||||
--long_query_time=2
|
||||
healthcheck:
|
||||
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 5
|
||||
start_period: 60s
|
||||
networks:
|
||||
- license-manager-network
|
||||
logging:
|
||||
driver: "json-file"
|
||||
options:
|
||||
max-size: "10m"
|
||||
max-file: "3"
|
||||
|
||||
# 后端服务 - 生产环境
|
||||
backend:
|
||||
image: ${BACKEND_IMAGE:-license-manager-backend:latest}
|
||||
container_name: license-manager-backend-prod
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- TZ=Asia/Shanghai
|
||||
- GIN_MODE=release
|
||||
ports:
|
||||
- "${BACKEND_PORT:-18888}:18888"
|
||||
volumes:
|
||||
- ./backend/configs:/app/backend/configs:ro
|
||||
- ./backend/configs/config.prod.yaml:/app/backend/cmd/config.yaml:ro
|
||||
- ./logs:/app/logs
|
||||
- ./data:/app/data
|
||||
depends_on:
|
||||
mysql:
|
||||
condition: service_healthy
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "curl -fsS http://localhost:18888/health || exit 1"]
|
||||
interval: 10s
|
||||
timeout: 10s
|
||||
retries: 6
|
||||
start_period: 60s
|
||||
networks:
|
||||
- license-manager-network
|
||||
logging:
|
||||
driver: "json-file"
|
||||
options:
|
||||
max-size: "10m"
|
||||
max-file: "3"
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
memory: 512M
|
||||
reservations:
|
||||
memory: 256M
|
||||
|
||||
# 前端服务 - 生产环境
|
||||
frontend:
|
||||
image: ${FRONTEND_IMAGE:-license-manager-frontend:latest}
|
||||
container_name: license-manager-frontend-prod
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- TZ=Asia/Shanghai
|
||||
ports:
|
||||
- "${FRONTEND_PORT:-80}:80"
|
||||
depends_on:
|
||||
backend:
|
||||
condition: service_healthy
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "curl -fsS http://localhost/ || exit 1"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 30s
|
||||
networks:
|
||||
- license-manager-network
|
||||
logging:
|
||||
driver: "json-file"
|
||||
options:
|
||||
max-size: "10m"
|
||||
max-file: "3"
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
memory: 256M
|
||||
reservations:
|
||||
memory: 128M
|
||||
|
||||
# Redis缓存 (可选,用于session存储和缓存)
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
container_name: license-manager-redis-prod
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "${REDIS_PORT:-6379}:6379"
|
||||
volumes:
|
||||
- redis_data:/data
|
||||
command: redis-server --appendonly yes --requirepass ${REDIS_PASSWORD:-redis@123}
|
||||
healthcheck:
|
||||
test: ["CMD", "redis-cli", "--raw", "incr", "ping"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 30s
|
||||
networks:
|
||||
- license-manager-network
|
||||
logging:
|
||||
driver: "json-file"
|
||||
options:
|
||||
max-size: "10m"
|
||||
max-file: "3"
|
||||
|
||||
volumes:
|
||||
mysql_data:
|
||||
driver: local
|
||||
redis_data:
|
||||
driver: local
|
||||
|
||||
networks:
|
||||
license-manager-network:
|
||||
driver: bridge
|
||||
ipam:
|
||||
config:
|
||||
- subnet: 172.20.0.0/16
|
||||
@@ -0,0 +1,91 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
# MySQL 8数据库
|
||||
mysql:
|
||||
image: mysql:8.0
|
||||
container_name: license-manager-mysql
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
MYSQL_ROOT_PASSWORD: root@123
|
||||
MYSQL_DATABASE: license_manager
|
||||
MYSQL_USER: license_user
|
||||
MYSQL_PASSWORD: license@123
|
||||
TZ: Asia/Shanghai
|
||||
ports:
|
||||
- "13306:3306"
|
||||
volumes:
|
||||
- mysql_data:/var/lib/mysql
|
||||
command: >
|
||||
--default-authentication-plugin=mysql_native_password
|
||||
--character-set-server=utf8mb4
|
||||
--collation-server=utf8mb4_unicode_ci
|
||||
--sql_mode=STRICT_TRANS_TABLES,NO_ZERO_DATE,NO_ZERO_IN_DATE,ERROR_FOR_DIVISION_BY_ZERO
|
||||
--max_connections=1000
|
||||
--innodb_buffer_pool_size=256M
|
||||
healthcheck:
|
||||
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 5
|
||||
start_period: 30s
|
||||
networks:
|
||||
- license-manager-network
|
||||
|
||||
# 后端服务
|
||||
backend:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile.backend
|
||||
container_name: license-manager-backend
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- TZ=Asia/Shanghai
|
||||
ports:
|
||||
- "18888:18888"
|
||||
volumes:
|
||||
- ./backend/configs:/app/backend/configs
|
||||
- ./logs:/app/logs
|
||||
- ./backend/configs/config.dev.yaml:/app/backend/cmd/config.yaml:ro
|
||||
depends_on:
|
||||
mysql:
|
||||
condition: service_healthy
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "curl -fsS http://localhost:18888/health || exit 1"]
|
||||
interval: 10s
|
||||
timeout: 10s
|
||||
retries: 6
|
||||
start_period: 60s
|
||||
networks:
|
||||
- license-manager-network
|
||||
|
||||
# 前端服务
|
||||
frontend:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile.frontend
|
||||
container_name: license-manager-frontend
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- TZ=Asia/Shanghai
|
||||
ports:
|
||||
- "18080:80"
|
||||
depends_on:
|
||||
backend:
|
||||
condition: service_healthy
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "curl -fsS http://localhost/ || exit 1"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 10s
|
||||
networks:
|
||||
- license-manager-network
|
||||
|
||||
volumes:
|
||||
mysql_data:
|
||||
driver: local
|
||||
|
||||
networks:
|
||||
license-manager-network:
|
||||
driver: bridge
|
||||
@@ -0,0 +1,72 @@
|
||||
# Release v0.1.0 - Initial Framework Release
|
||||
|
||||
**发布日期**: 2024-08-13
|
||||
**版本类型**: 初始框架版本
|
||||
|
||||
---
|
||||
|
||||
## 🎉 首个框架版本发布
|
||||
|
||||
这是License Manager项目的第一个发布版本,包含了基础的系统框架和核心功能。
|
||||
|
||||
## ✨ 本版本新增
|
||||
|
||||
- 🔐 JWT用户认证系统
|
||||
- 🌐 多语言支持(中/英/日)
|
||||
- 📊 响应式管理界面
|
||||
- 🛠️ 完整的前后端分离架构
|
||||
- 🐳 Docker容器化部署支持
|
||||
- 📝 API基础框架
|
||||
|
||||
## 📦 快速体验
|
||||
|
||||
```bash
|
||||
git clone https://github.com/cedar-v/license-manager.git
|
||||
cd license-manager
|
||||
docker compose up -d --build
|
||||
```
|
||||
|
||||
访问: http://localhost:18080
|
||||
|
||||
## 🔮 下一步计划
|
||||
|
||||
### v0.2.0 - 客户管理模块
|
||||
- [ ] 客户信息增删改查
|
||||
- [ ] 客户状态管理
|
||||
- [ ] 批量操作功能
|
||||
|
||||
### v0.3.0 - 授权管理核心
|
||||
- [ ] 授权码生成算法
|
||||
- [ ] 硬件指纹绑定
|
||||
- [ ] 在线/离线授权模式
|
||||
|
||||
### v0.4.0 - API完善
|
||||
- [ ] 授权验证API
|
||||
- [ ] 激活与心跳API
|
||||
- [ ] API限流和安全增强
|
||||
|
||||
## ⚠️ 重要说明
|
||||
|
||||
**这是早期开发版本,主要用于:**
|
||||
- 展示系统架构和技术选型
|
||||
- 收集用户反馈和建议
|
||||
- 建立开发基线和版本管理
|
||||
|
||||
**当前版本限制:**
|
||||
- 仅包含基础框架和登录功能
|
||||
- 数据为模拟数据,非完整业务逻辑
|
||||
- API接口尚未完全实现
|
||||
- 未进行完整的安全测试
|
||||
|
||||
## 🤝 贡献
|
||||
|
||||
欢迎提交Issue和Pull Request!
|
||||
|
||||
## 📄 许可证
|
||||
|
||||
本项目采用GPL-3.0开源许可证。
|
||||
|
||||
---
|
||||
|
||||
**项目地址**: https://github.com/cedar-v/license-manager
|
||||
**问题反馈**: https://github.com/cedar-v/license-manager/issues
|
||||
@@ -0,0 +1,345 @@
|
||||
# 客户属性设计与Web显示方案
|
||||
|
||||
## 1. 核心客户属性
|
||||
|
||||
### 基础标识
|
||||
- `id` - 唯一标识符
|
||||
- **作用**: 数据库主键,系统内部引用
|
||||
- **生成**: UUID自动生成
|
||||
- **必填**: 是
|
||||
|
||||
- `customer_code` - 客户编码 (如: CUS-2025-001)
|
||||
- **作用**: 业务识别码,便于客服沟通和查找
|
||||
- **生成**: CUS-YYYY-NNNN格式,系统自动递增
|
||||
- **必填**: 是
|
||||
- **唯一**: 是
|
||||
- **意义**: 比数据库ID更有业务意义,支持按年度统计
|
||||
|
||||
- `customer_name` - 客户名称/公司名
|
||||
- **作用**: 主要识别信息,合同签署主体
|
||||
- **示例**: "北京科技有限公司"、"张三"
|
||||
- **必填**: 是
|
||||
- **搜索**: 支持模糊搜索
|
||||
|
||||
- `customer_type` - 客户类型 (个人/企业/政府/教育)
|
||||
- **作用**: 区分客户类别,影响服务策略和定价
|
||||
- **枚举**: individual(个人) | enterprise(企业) | government(政府) | education(教育)
|
||||
- **必填**: 是
|
||||
- **默认**: enterprise
|
||||
|
||||
### 联系信息
|
||||
- `contact_person` - 联系人姓名
|
||||
- **作用**: 授权通知和技术支持的对接人
|
||||
- **必填**: 是
|
||||
- **示例**: "张三"
|
||||
|
||||
- `contact_title` - 联系人职务
|
||||
- **作用**: 了解联系人在组织中的角色,便于沟通
|
||||
- **必填**: 否
|
||||
- **示例**: "技术总监"、"IT经理"
|
||||
|
||||
- `email` - 主邮箱
|
||||
- **作用**: 授权激活通知、过期提醒、技术支持的主要渠道
|
||||
- **必填**: 是
|
||||
- **唯一**: 建议唯一
|
||||
- **验证**: 邮箱格式验证
|
||||
|
||||
- `phone` - 电话号码
|
||||
- **作用**: 紧急联系、电话技术支持
|
||||
- **必填**: 否
|
||||
- **格式**: 支持手机和固话
|
||||
|
||||
### 地址信息
|
||||
- `address` - 地址
|
||||
- **作用**: 联系地址,邮寄材料或现场服务时使用
|
||||
- **必填**: 否
|
||||
- **示例**: "北京市朝阳区科技园区100号"
|
||||
- **格式**: 自由文本,用户可详细可简略
|
||||
|
||||
### 商业属性
|
||||
- `company_size` - 企业规模 (小型/中型/大型/集团)
|
||||
- **作用**: 影响定价策略和服务等级
|
||||
- **枚举**: small(小型) | medium(中型) | large(大型) | enterprise(集团)
|
||||
- **必填**: 否
|
||||
- **用途**: 制定差异化授权策略
|
||||
|
||||
### 授权配置
|
||||
- `preferred_license_type` - 首选授权类型 (在线/离线/混合)
|
||||
- **作用**: 影响授权码生成策略和验证方式
|
||||
- **枚举**: online(在线) | offline(离线) | hybrid(混合)
|
||||
- **必填**: 否
|
||||
- **默认**: online
|
||||
- **意义**: 在线需要联网验证,离线可本地验证
|
||||
|
||||
### 客户分级
|
||||
- `customer_level` - 客户等级 (普通/VIP/企业/战略)
|
||||
- **作用**: 影响服务优先级、授权数量限制、技术支持等级
|
||||
- **枚举**: normal(普通) | vip(VIP) | enterprise(企业) | strategic(战略)
|
||||
- **必填**: 否
|
||||
- **默认**: normal
|
||||
- **影响**: 高等级客户可获得更多授权、更快响应
|
||||
|
||||
### 状态管理
|
||||
- `status` - 客户状态 (正常/禁用)
|
||||
- **作用**: 控制是否允许为该客户生成新授权
|
||||
- **枚举**: active(正常) | disabled(禁用)
|
||||
- **必填**: 是
|
||||
- **默认**: active
|
||||
- **影响**: 禁用状态下不能生成新授权,但现有授权仍可使用
|
||||
|
||||
- `description` - 客户描述/备注
|
||||
- **作用**: 记录客户特殊情况、业务背景、注意事项
|
||||
- **必填**: 否
|
||||
- **示例**: "重要客户,IoT平台采购方,需要优先技术支持"
|
||||
|
||||
### 时间字段
|
||||
- `created_at` - 创建时间
|
||||
- **作用**: 记录客户注册时间,用于统计分析
|
||||
- **自动**: 系统自动记录
|
||||
- **不可修改**: 是
|
||||
|
||||
- `updated_at` - 更新时间
|
||||
- **作用**: 记录最后修改时间,追踪数据变更
|
||||
- **自动**: 系统自动更新
|
||||
- **触发**: 任何字段修改都会更新
|
||||
|
||||
- `created_by` - 创建人
|
||||
- **作用**: 记录是哪个管理员创建的客户
|
||||
- **必填**: 是
|
||||
- **关联**: 管理员用户ID
|
||||
|
||||
- `updated_by` - 更新人
|
||||
- **作用**: 记录最后修改人,便于问题追溯
|
||||
- **自动**: 系统自动记录当前操作员
|
||||
|
||||
---
|
||||
|
||||
## 2. Web列表页显示信息
|
||||
|
||||
### 主要显示列
|
||||
```
|
||||
| 客户编码 | 客户名称 | 客户类型 | 联系人 | 邮箱 | 客户等级 | 状态 | 创建时间 | 操作 |
|
||||
```
|
||||
|
||||
### 具体显示内容与作用
|
||||
|
||||
**客户编码** (`customer_code`)
|
||||
- 显示: CUS-2025-001
|
||||
- 作用: 快速识别客户,支持精确搜索,客服沟通时的标准引用
|
||||
- 交互: 点击可复制到剪贴板
|
||||
|
||||
**客户名称** (`customer_name`)
|
||||
- 显示: 北京科技有限公司
|
||||
- 作用: 主要识别信息,最重要的显示内容
|
||||
- 交互: 点击进入客户详情页
|
||||
- 样式: 可截断显示,悬停显示完整名称
|
||||
|
||||
**客户类型** (`customer_type`)
|
||||
- 显示: 🏢企业 | 👤个人 | 🏛️政府 | 🎓教育
|
||||
- 作用: 快速区分客户类别,影响服务策略
|
||||
- 样式: 用图标+文字+颜色区分
|
||||
- 筛选: 支持按类型筛选
|
||||
|
||||
**联系人** (`contact_person`)
|
||||
- 显示: 张三
|
||||
- 作用: 知道具体联系谁,便于沟通对接
|
||||
- 交互: 悬停显示联系人职务信息
|
||||
|
||||
**邮箱** (`email`)
|
||||
- 显示: zhangsan@example.com
|
||||
- 作用: 重要联系方式,授权通知的主要渠道
|
||||
- 交互: 点击可发送邮件
|
||||
- 验证: 显示邮箱验证状态(已验证/未验证)
|
||||
|
||||
**客户等级** (`customer_level`)
|
||||
- 显示: 🏢企业 | 👤普通 | ⭐VIP | 💼战略
|
||||
- 作用: 显示客户重要程度,影响服务优先级
|
||||
- 样式: 不同等级用不同颜色和图标
|
||||
- 排序: 支持按等级排序
|
||||
|
||||
**状态** (`status`)
|
||||
- 显示: ✅正常 | ❌禁用
|
||||
- 作用: 显示客户是否可以生成新授权
|
||||
- 交互: 可直接点击切换状态(需要确认)
|
||||
- 样式: 正常为绿色,禁用为红色
|
||||
|
||||
**创建时间** (`created_at`)
|
||||
- 显示: 2025-01-15
|
||||
- 作用: 了解客户注册时间,便于分析客户生命周期
|
||||
- 格式: YYYY-MM-DD
|
||||
- 排序: 默认按创建时间倒序
|
||||
|
||||
**操作**
|
||||
- 查看详情: 进入客户详情页
|
||||
- 编辑: 修改客户信息
|
||||
- 授权管理: 查看该客户的所有授权
|
||||
- 生成授权: 快速为该客户生成新授权
|
||||
- 更多操作: 禁用/启用、删除等
|
||||
|
||||
---
|
||||
|
||||
## 3. Web详情页显示信息
|
||||
|
||||
### 基本信息卡片
|
||||
```
|
||||
客户编码: CUS-2025-001 客户类型: 🏢企业
|
||||
客户名称: 北京科技有限公司 客户等级: ⭐VIP客户
|
||||
联系人: 张三 (技术总监) 当前状态: ✅正常
|
||||
邮箱: zhangsan@example.com 电话: 13800138000
|
||||
```
|
||||
**用途**: 核心信息一览,便于快速了解客户基本情况
|
||||
|
||||
### 地址信息卡片
|
||||
```
|
||||
地址: 北京市朝阳区科技园区100号
|
||||
```
|
||||
**用途**: 联系地址,邮寄材料或现场服务参考
|
||||
|
||||
### 商业信息卡片
|
||||
```
|
||||
企业规模: 中型企业 首选授权: 在线授权
|
||||
备注描述: 重要客户,IoT平台采购方,需要优先技术支持
|
||||
```
|
||||
**用途**: 业务背景了解,制定针对性服务策略
|
||||
|
||||
### 状态信息卡片
|
||||
```
|
||||
当前状态: ✅正常 创建时间: 2025-01-15 10:30:00
|
||||
创建人: 李四(管理员) 更新时间: 2025-01-20 15:45:00
|
||||
更新人: 王五(客服)
|
||||
```
|
||||
**用途**: 跟踪客户生命周期,了解操作历史
|
||||
|
||||
### 授权统计卡片
|
||||
```
|
||||
总授权数: 3个 活跃授权: 2个
|
||||
过期授权: 1个 最近授权: 2025-01-18
|
||||
即将过期: 1个(7天内) 授权设备总数: 85台
|
||||
```
|
||||
**用途**: 快速了解客户授权使用情况,识别续费机会
|
||||
|
||||
---
|
||||
|
||||
## 4. 列表页功能特性
|
||||
|
||||
### 搜索筛选
|
||||
- **全文搜索框**: 支持客户编码、名称、联系人、邮箱的模糊搜索
|
||||
- **作用**: 快速定位目标客户
|
||||
- **实现**: 实时搜索,输入即显示结果
|
||||
|
||||
- **客户类型筛选**: 全部 | 企业 | 个人 | 政府 | 教育
|
||||
- **作用**: 按业务类型分类查看
|
||||
- **场景**: 针对不同类型客户制定营销策略
|
||||
|
||||
- **客户等级筛选**: 全部 | 普通 | VIP | 企业 | 战略
|
||||
- **作用**: 重点关注高价值客户
|
||||
- **场景**: 优先处理VIP和战略客户的需求
|
||||
|
||||
- **状态筛选**: 全部 | 正常 | 禁用
|
||||
- **作用**: 识别问题客户,及时处理
|
||||
- **场景**: 定期清理禁用客户,分析禁用原因
|
||||
|
||||
### 排序功能
|
||||
- **按创建时间排序** (默认)
|
||||
- **作用**: 了解客户注册趋势,关注新客户
|
||||
|
||||
- **按客户名称排序**
|
||||
- **作用**: 字母序查找,便于浏览
|
||||
|
||||
- **按客户等级排序**
|
||||
- **作用**: 优先显示重要客户
|
||||
|
||||
- **按更新时间排序**
|
||||
- **作用**: 查看最近活跃的客户
|
||||
|
||||
### 批量操作
|
||||
- **批量启用/禁用**
|
||||
- **作用**: 批量处理客户状态变更
|
||||
- **场景**: 批量处理欠费客户、恢复客户服务
|
||||
|
||||
- **批量导出**
|
||||
- **作用**: 导出客户数据进行分析
|
||||
- **格式**: Excel、CSV格式
|
||||
|
||||
- **批量删除**
|
||||
- **作用**: 清理无效客户数据
|
||||
- **安全**: 需要二次确认,软删除机制
|
||||
|
||||
### 分页显示
|
||||
- **每页条数**: 10/20/50/100条可选
|
||||
- **默认**: 20条
|
||||
- **作用**: 平衡加载速度和浏览效率
|
||||
|
||||
- **总数统计**: 显示"共1,234个客户"
|
||||
- **作用**: 了解客户总体规模
|
||||
|
||||
- **页码导航**: 支持跳转到指定页
|
||||
- **作用**: 快速浏览大量数据
|
||||
|
||||
---
|
||||
|
||||
## 5. 显示优化建议
|
||||
|
||||
### 响应式设计
|
||||
- **桌面端(>1200px)**: 显示所有列
|
||||
- **作用**: 完整信息展示,提高工作效率
|
||||
|
||||
- **平板端(768-1200px)**: 隐藏地址、电话等次要信息
|
||||
- **作用**: 保持核心信息,适应中等屏幕
|
||||
|
||||
- **手机端(<768px)**: 卡片式布局,只显示客户名称、等级、状态
|
||||
- **作用**: 移动端快速浏览,支持现场客户查询
|
||||
|
||||
### 数据可视化(仪表盘)
|
||||
- **客户等级分布饼图**
|
||||
- **作用**: 了解客户结构,制定等级提升策略
|
||||
|
||||
- **客户类型统计柱图**
|
||||
- **作用**: 分析目标市场,调整营销重点
|
||||
|
||||
- **月度新增客户趋势图**
|
||||
- **作用**: 监控业务增长,评估营销效果
|
||||
|
||||
### 交互优化
|
||||
- **悬停显示完整信息**
|
||||
- **场景**: 客户名称过长时显示完整名称
|
||||
- **内容**: 联系人悬停显示职务和电话
|
||||
|
||||
- **点击客户名称进入详情页**
|
||||
- **作用**: 快速访问详细信息
|
||||
|
||||
- **状态切换快捷操作**
|
||||
- **实现**: 点击状态图标直接切换,需要确认
|
||||
- **作用**: 快速处理客户状态变更
|
||||
|
||||
- **右键菜单快速操作**
|
||||
- **功能**: 编辑、查看授权、生成授权、禁用等
|
||||
- **作用**: 提高操作效率,减少页面跳转
|
||||
|
||||
### 性能优化
|
||||
- **虚拟滚动**: 大数据量时采用虚拟滚动
|
||||
- **触发**: 客户数>1000时
|
||||
- **作用**: 保证页面流畅性
|
||||
|
||||
- **延迟加载**: 图片和非关键信息延迟加载
|
||||
- **作用**: 提高首屏加载速度
|
||||
|
||||
- **缓存策略**: 客户列表数据缓存5分钟
|
||||
- **作用**: 减少不必要的服务器请求
|
||||
|
||||
## 6. 业务价值总结
|
||||
|
||||
### 提高工作效率
|
||||
- **快速识别**: 通过客户编码和等级快速识别重要客户
|
||||
- **精准搜索**: 多维度搜索快速定位目标客户
|
||||
- **批量操作**: 减少重复性操作,提高处理效率
|
||||
|
||||
### 优化客户服务
|
||||
- **服务分级**: 根据客户等级提供差异化服务
|
||||
- **及时响应**: 通过状态管理及时识别问题客户
|
||||
- **个性化**: 根据客户偏好(授权类型)提供合适的解决方案
|
||||
|
||||
### 支撑业务决策
|
||||
- **客户分析**: 通过等级、类型分布了解客户结构
|
||||
- **趋势监控**: 通过创建时间分析客户增长趋势
|
||||
- **风险控制**: 通过状态管理防范业务风险
|
||||
@@ -19,30 +19,6 @@ license-manager/
|
||||
└── bin/ # 编译产物目录
|
||||
```
|
||||
|
||||
## 后端目录 (backend/)
|
||||
|
||||
```
|
||||
backend/
|
||||
├── cmd/ # 应用程序入口
|
||||
│ └── main.go # 主程序入口文件
|
||||
├── internal/ # 内部包(不对外暴露)
|
||||
│ ├── api/ # API路由和控制器
|
||||
│ ├── config/ # 配置管理
|
||||
│ ├── models/ # 数据模型定义
|
||||
│ ├── service/ # 业务逻辑层
|
||||
│ ├── repository/ # 数据访问层
|
||||
│ └── middleware/ # HTTP中间件
|
||||
├── pkg/ # 可重用的公共包
|
||||
├── configs/ # 配置文件目录
|
||||
│ ├── config.yaml # 生产环境配置
|
||||
│ └── config.example.yaml # 配置模板文件
|
||||
├── migrations/ # 数据库迁移脚本
|
||||
├── scripts/ # 后端专用脚本
|
||||
├── tools/ # 硬件信息获取工具
|
||||
├── go.mod # Go模块依赖
|
||||
└── go.sum # Go依赖校验
|
||||
```
|
||||
|
||||
## 前端目录 (frontend/)
|
||||
|
||||
```
|
||||
|
||||
@@ -119,47 +119,8 @@
|
||||
- 客户管理、授权生成、授权列表和详情页面
|
||||
- 系统仪表盘、用户登录页面
|
||||
|
||||
### 6.2 RESTful API接口
|
||||
- `POST /api/validate` - 授权验证
|
||||
- `POST /api/activate` - 授权激活
|
||||
- `POST /api/heartbeat` - 心跳上报
|
||||
- `GET /api/license/{code}` - 授权信息查询
|
||||
- `GET /api/customers` - 客户列表API
|
||||
- `GET /tools/{tool}` - 工具下载
|
||||
|
||||
### 6.3 数据格式
|
||||
- 统一JSON格式数据交换
|
||||
- 统一错误响应格式
|
||||
- 支持数据分页和筛选
|
||||
|
||||
## 7. 项目交付物
|
||||
|
||||
### 7.1 软件交付
|
||||
- 完整源代码(前后端)
|
||||
- 编译后的可执行文件
|
||||
- Docker镜像文件
|
||||
- 数据库初始化脚本
|
||||
|
||||
### 7.2 文档交付
|
||||
- 部署安装文档
|
||||
- API接口文档
|
||||
- 用户操作手册
|
||||
- 系统架构文档
|
||||
|
||||
### 7.3 工具交付
|
||||
- 硬件信息获取工具(Windows/Linux)
|
||||
- 客户端SDK示例
|
||||
- 配置文件模板
|
||||
|
||||
## 8. 开发计划
|
||||
|
||||
### 8.1 开发阶段
|
||||
1. **架构设计**(1周):技术选型、数据库设计、API设计
|
||||
2. **核心功能**(3周):客户管理、授权生成、验证API
|
||||
3. **Web界面**(2周):前端页面开发、交互逻辑
|
||||
4. **测试部署**(1周):功能测试、性能测试、部署验证
|
||||
|
||||
### 8.2 质量保证
|
||||
- 单元测试覆盖率 > 80%
|
||||
- 接口测试和性能测试
|
||||
- 安全性测试和代码审查
|
||||
|
After Width: | Height: | Size: 267 KiB |
|
After Width: | Height: | Size: 1.1 MiB |
@@ -0,0 +1,78 @@
|
||||
## 使用 CI 产物镜像替换本地构建的部署方案
|
||||
|
||||
本文说明如何使用 GitHub Actions 构建并推送到 GHCR 的镜像,直接替换本地 `docker compose up -d --build` 的构建流程,以实现更一致、更快的部署。
|
||||
|
||||
### 前置条件
|
||||
- 已通过仓库内工作流发布镜像:
|
||||
- 后端:`.github/workflows/release_backend.yml`
|
||||
- 前端:`.github/workflows/release_frontend.yml`
|
||||
- 镜像仓库(GHCR)可访问:`ghcr.io/<owner>/<repo>-backend:<tag>`、`ghcr.io/<owner>/<repo>-frontend:<tag>`
|
||||
- 已在部署机登录 GHCR:
|
||||
```bash
|
||||
docker login ghcr.io -u <YOUR_GITHUB_USER> -p <YOUR_GITHUB_TOKEN>
|
||||
```
|
||||
|
||||
### 方式 A(推荐):直接使用生产编排(支持镜像变量)
|
||||
生产编排文件 `docker-compose.prod.yml` 已支持通过环境变量替换镜像。
|
||||
|
||||
1) 在项目根目录创建 `.env`:
|
||||
```env
|
||||
# 指向 GHCR 镜像(可用具体版本号,如 v1.0.0)
|
||||
BACKEND_IMAGE=ghcr.io/<owner>/<repo>-backend:latest
|
||||
FRONTEND_IMAGE=ghcr.io/<owner>/<repo>-frontend:latest
|
||||
|
||||
# 可选:本地端口映射(如用于开发环境)
|
||||
MYSQL_PORT=13306
|
||||
BACKEND_PORT=18888
|
||||
FRONTEND_PORT=18080
|
||||
```
|
||||
|
||||
2) 启动:
|
||||
```bash
|
||||
docker compose -f docker-compose.prod.yml up -d
|
||||
```
|
||||
|
||||
3) 验证:
|
||||
```bash
|
||||
curl http://localhost:18888/health
|
||||
```
|
||||
|
||||
说明:
|
||||
- 该方式同时适用于生产/准生产/本地测试场景,只需通过 `.env` 控制端口与镜像标签即可。
|
||||
- 若镜像私有,请确保已成功 `docker login ghcr.io`。
|
||||
|
||||
### 方式 B:临时覆盖镜像(不创建 .env)
|
||||
直接在一次性命令中覆盖镜像变量:
|
||||
```bash
|
||||
BACKEND_IMAGE=ghcr.io/<owner>/<repo>-backend:v1.0.0 \
|
||||
FRONTEND_IMAGE=ghcr.io/<owner>/<repo>-frontend:v1.0.0 \
|
||||
docker compose -f docker-compose.prod.yml up -d
|
||||
```
|
||||
|
||||
### 常用操作
|
||||
- 拉取新版本并滚动更新:
|
||||
```bash
|
||||
docker compose -f docker-compose.prod.yml pull
|
||||
docker compose -f docker-compose.prod.yml up -d
|
||||
```
|
||||
|
||||
- 回滚到上一版本:
|
||||
```bash
|
||||
# 修改 .env 中的镜像标签为上一个版本
|
||||
docker compose -f docker-compose.prod.yml up -d
|
||||
```
|
||||
|
||||
### 与开发编排的关系
|
||||
- 当前开发编排 `docker-compose.yml` 使用 `build` 本地构建。如果希望开发环境也使用远端镜像,推荐直接沿用方式 A(使用 `docker-compose.prod.yml`,并通过 `.env` 调整端口为开发习惯)。
|
||||
- 也可以手动修改 `docker-compose.yml`:将 `backend`、`frontend` 的 `build` 段替换为 `image: ${BACKEND_IMAGE}` / `image: ${FRONTEND_IMAGE}`,并在 `.env` 中声明对应镜像。
|
||||
|
||||
### 标签与架构
|
||||
- CI 工作流默认推送 `latest` 与手动输入的版本号(如 `v1.0.0`),并构建多架构镜像:`linux/amd64, linux/arm64`。
|
||||
- 生产环境建议使用显式版本标签部署,避免 `latest` 带来的不可预期变更。
|
||||
|
||||
### 故障排查
|
||||
- 无法拉取镜像:检查 GHCR 登录权限、镜像是否设为公开或部署机是否具备读取私有包权限。
|
||||
- 拉取缓慢或超时:配置镜像加速器或重试。
|
||||
- 启动后访问异常:参考 `README-Docker.md` 的故障排查章节(健康检查、反向代理、容器间网络连通性等)。
|
||||
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
# 开发环境
|
||||
NODE_ENV = "development"
|
||||
|
||||
###
|
||||
# @Author: 13895237362 [email protected]
|
||||
# @Date: 2025-07-31 14:30:20
|
||||
# @LastEditors: 13895237362 [email protected]
|
||||
# @LastEditTime: 2025-08-08 09:18:54
|
||||
# @FilePath: /frontend/.env.development
|
||||
# @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
|
||||
###
|
||||
# 开发环境接口地址
|
||||
VITE_API_BASE_URL = '/devApi'
|
||||
VITE_API_BASE_URL = 'http://104.156.140.42:18888'
|
||||
|
||||
# # 开发环境打包名称
|
||||
# VITE_OUTDIR='dev'
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
# 正式环境
|
||||
NODE_ENV = "production"
|
||||
|
||||
# 正式环境接口地址
|
||||
VITE_API_BASE_URL = '/proApi'
|
||||
VITE_API_BASE_URL = 'http://104.156.140.42:18888'
|
||||
|
||||
# 正式环境打包名称
|
||||
VITE_OUTDIR='production'
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
# 测试环境
|
||||
NODE_ENV = "test"
|
||||
|
||||
###
|
||||
# @Author: 13895237362 [email protected]
|
||||
# @Date: 2025-07-31 14:30:20
|
||||
# @LastEditors: 13895237362 [email protected]
|
||||
# @LastEditTime: 2025-08-08 10:07:33
|
||||
# @FilePath: /frontend/.env.test
|
||||
# @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
|
||||
###
|
||||
# 测试环境接口地址
|
||||
VITE_API_BASE_URL = '/testApi'
|
||||
VITE_API_BASE_URL = ''
|
||||
|
||||
# 测试环境打包名称
|
||||
VITE_OUTDIR='test'
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"semi": false,
|
||||
"singleQuote": true,
|
||||
"quoteProps": "as-needed",
|
||||
"trailingComma": "none",
|
||||
"bracketSpacing": true,
|
||||
"bracketSameLine": false,
|
||||
"arrowParens": "avoid",
|
||||
"printWidth": 100,
|
||||
"tabWidth": 2,
|
||||
"useTabs": false,
|
||||
"endOfLine": "lf",
|
||||
"vueIndentScriptAndStyle": false
|
||||
}
|
||||
@@ -0,0 +1,506 @@
|
||||
<!--
|
||||
* @Author: 13895237362 2205451508@qq.com
|
||||
* @Date: 2025-08-11 09:07:12
|
||||
* @LastEditors: 13895237362 2205451508@qq.com
|
||||
* @LastEditTime: 2025-08-11 11:07:32
|
||||
* @FilePath: /frontend/docs/Claude + Figma 开发 Vue 3 + TypeScript + Element Plus 应用指南.md
|
||||
* @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
|
||||
-->
|
||||
# Claude + Figma 开发 Vue 3 + TypeScript + Element Plus 应用指南
|
||||
|
||||
## 1. 准备工作
|
||||
|
||||
### 1.1 工具安装
|
||||
|
||||
**必需工具:**
|
||||
- **Figma**: 设计工具,用于获取UI设计资源和导出图标、图片资源
|
||||
- **Node.js**: 确保安装最新LTS版本 (推荐18.x或更高版本)
|
||||
- **包管理器**: npm、yarn 或 pnpm (推荐使用 pnpm)
|
||||
- **Vue CLI 或 Vite**: 项目脚手架工具 (推荐使用 Vite,构建速度更快)
|
||||
- **Git**: 版本控制工具
|
||||
- **Claude**: AI助手,用于代码生成和开发协助
|
||||
|
||||
**推荐工具:**
|
||||
- **VS Code**: 代码编辑器
|
||||
- **Vue DevTools**: 浏览器调试插件
|
||||
- **Figma Desktop**: 桌面版 Figma 应用
|
||||
- **Element Plus Helper**: VS Code 插件,提供 Element Plus 组件智能提示
|
||||
|
||||
### 1.2 环境配置
|
||||
|
||||
**Node.js 环境检查:**
|
||||
```bash
|
||||
node --version # 应显示 v18.x.x 或更高
|
||||
npm --version # 检查 npm 版本
|
||||
```
|
||||
|
||||
## 2. 项目结构说明
|
||||
|
||||
### 2.1 完整项目目录结构
|
||||
|
||||
```
|
||||
license-manager-frontend/
|
||||
├── .claude/ # Claude Code 配置
|
||||
│ └── settings.local.json # 本地设置
|
||||
├── .vscode/ # VS Code 配置
|
||||
│ └── extensions.json # 推荐插件
|
||||
├── docs/ # 项目文档
|
||||
│ ├── Claude + Figma 开发 Vue 3 + TypeScript + Element Plus 应用指南.md
|
||||
│ └── 多语言框架设计方案.md
|
||||
├── public/ # 静态资源
|
||||
│ └── favicon.ico # 网站图标
|
||||
├── src/ # 源码目录
|
||||
│ ├── api/ # API 接口层
|
||||
│ │ ├── https/ # HTTP 请求工具
|
||||
│ │ │ ├── errorCodeType.ts # 错误码类型定义
|
||||
│ │ │ └── index.ts # HTTP 请求封装
|
||||
│ │ └── user.ts # 用户相关 API
|
||||
│ ├── assets/ # 静态资源
|
||||
│ │ ├── icons/ # SVG 图标
|
||||
│ │ ├── images/ # 图片资源
|
||||
│ │ │ ├── login-background.png # 登录背景图
|
||||
│ │ │ └── login-background-m.png # 移动端登录背景
|
||||
│ │ └── styles/ # 样式文件系统
|
||||
│ │ ├── global.scss # 全局样式重置和基础样式
|
||||
│ │ ├── variables.scss # SCSS 变量(颜色、尺寸等)
|
||||
│ │ ├── element-theme.scss # Element Plus 主题定制
|
||||
│ │ └── global.css # 旧版全局样式(待清理)
|
||||
│ ├── components/ # 组件目录
|
||||
│ │ ├── common/ # 通用组件
|
||||
│ │ │ └── layout/ # 布局组件
|
||||
│ │ │ ├── Layout.vue # 主布局组件
|
||||
│ │ │ ├── Sidebar.vue # 侧边栏组件
|
||||
│ │ │ ├── NavContent.vue # 顶部导航组件
|
||||
│ │ │ └── index.ts # 类型定义和导出
|
||||
│ │ └── business/ # 业务相关组件
|
||||
│ │ └── cusCard/ # 自定义卡片组件
|
||||
│ ├── i18n/ # 国际化配置
|
||||
│ │ ├── index.ts # i18n 初始化配置
|
||||
│ │ └── locales/ # 语言包
|
||||
│ │ ├── zh.json # 中文语言包
|
||||
│ │ ├── en.json # 英文语言包
|
||||
│ │ └── ja.json # 日文语言包
|
||||
│ ├── router/ # 路由配置
|
||||
│ │ └── index.ts # Vue Router 配置
|
||||
│ ├── store/ # 状态管理 (Pinia)
|
||||
│ │ ├── index.ts # Pinia 配置和导出
|
||||
│ │ └── modules/ # 状态模块
|
||||
│ │ ├── app.ts # 应用全局状态
|
||||
│ │ └── user.ts # 用户状态管理
|
||||
│ ├── utils/ # 工具函数库
|
||||
│ ├── views/ # 页面组件
|
||||
│ │ ├── Dashboard.vue # 仪表盘页面
|
||||
│ │ └── Login.vue # 登录页面
|
||||
│ ├── App.vue # 根组件
|
||||
│ ├── main.ts # 应用入口文件
|
||||
│ └── vite-env.d.ts # Vite 环境类型声明
|
||||
├── .env.development # 开发环境变量
|
||||
├── .env.production # 生产环境变量
|
||||
├── .env.test # 测试环境变量
|
||||
├── .eslintrc.cjs # ESLint 配置
|
||||
├── .prettierrc # Prettier 代码格式化配置
|
||||
├── index.html # HTML 入口文件
|
||||
├── package.json # 项目依赖和脚本配置
|
||||
├── tsconfig.json # TypeScript 配置
|
||||
├── tsconfig.node.json # Node.js TypeScript 配置
|
||||
└── vite.config.ts # Vite 构建配置
|
||||
```
|
||||
|
||||
### 2.2 核心目录详解
|
||||
|
||||
#### 📁 src/assets/styles/ - SCSS 样式系统
|
||||
- **variables.scss**: 定义主题色彩、字体大小、间距等设计变量
|
||||
- **mixins.scss**: 包含常用的样式混合器(清除浮动、文本省略、响应式等)
|
||||
- **global.scss**: 全局样式重置和基础样式定义
|
||||
- **element-theme.scss**: Element Plus 组件深度样式定制
|
||||
|
||||
#### 📁 src/components/ - 组件分层架构
|
||||
- **common/**: 跨项目可复用的通用组件(如布局、表单、表格等)
|
||||
- **business/**: 特定业务场景的组件(与许可证管理相关)
|
||||
|
||||
#### 📁 src/store/modules/ - Pinia 状态管理
|
||||
- **app.ts**: 应用级状态(主题、语言、侧边栏状态等)
|
||||
- **user.ts**: 用户状态(登录信息、权限、个人设置等)
|
||||
|
||||
#### 📁 配置文件说明
|
||||
- **.eslintrc.cjs**: ESLint 代码规范检查配置
|
||||
- **.prettierrc**: 代码格式化规则配置
|
||||
- **vite.config.ts**: Vite 构建工具配置(包含性能优化设置)
|
||||
|
||||
### 2.3 样式文件组织最佳实践
|
||||
|
||||
#### SCSS 样式系统架构
|
||||
```scss
|
||||
// 1. 在 variables.scss 中定义设计变量
|
||||
$primary-color: #019C7C;
|
||||
$border-radius: 6px;
|
||||
|
||||
// 2. 在 mixins.scss 中创建可复用的样式片段
|
||||
@mixin button-style($color: $primary-color) {
|
||||
background-color: $color;
|
||||
border-radius: $border-radius;
|
||||
// ...
|
||||
}
|
||||
|
||||
// 3. 在组件中导入和使用
|
||||
@import '@/assets/styles/variables.scss';
|
||||
@import '@/assets/styles/mixins.scss';
|
||||
|
||||
.my-button {
|
||||
@include button-style($success-color);
|
||||
}
|
||||
```
|
||||
|
||||
#### 组件样式规范
|
||||
- **优先使用** `<style lang="scss" scoped>` 避免样式污染
|
||||
- **导入变量**: 在组件中导入 variables.scss 和 mixins.scss
|
||||
- **复用样式**: 将常用样式抽取到 mixins 中
|
||||
- **主题支持**: 使用 SCSS 变量配合 Element Plus 主题定制
|
||||
|
||||
#### 响应式设计
|
||||
```scss
|
||||
// 使用 mixins.scss 中的响应式断点
|
||||
@include tablet {
|
||||
// 平板样式
|
||||
}
|
||||
|
||||
@include mobile {
|
||||
// 手机样式
|
||||
}
|
||||
```
|
||||
|
||||
## 3. Figma设计转代码
|
||||
|
||||
### 3.1 设计标记获取流程
|
||||
|
||||
**步骤1:获取设计标记**
|
||||
1. 在 Figma 中选择设计稿的根节点
|
||||
2. 查看右侧面板的"设计"标签获取:
|
||||
- 颜色值 (Color)
|
||||
- 字体规格 (Typography)
|
||||
- 间距系统 (Spacing)
|
||||
- 圆角值 (Border radius)
|
||||
- 阴影效果 (Drop shadow)
|
||||
|
||||
**步骤2:导出设计资源**
|
||||
```bash
|
||||
# Figma 资源导出建议
|
||||
1. 图标导出为 SVG 格式
|
||||
2. 图片导出为 PNG/WebP 格式 (2x 倍图)
|
||||
3. 获取精确的间距、字号、颜色值
|
||||
```
|
||||
|
||||
### 3.3 Element Plus 主题定制
|
||||
|
||||
**主题配置 (`/src/assets/styles/element-theme.scss`):**
|
||||
```scss
|
||||
// 覆盖 Element Plus CSS 变量
|
||||
:root {
|
||||
--el-color-primary: #{$primary-color};
|
||||
--el-border-radius-base: #{$border-radius-base};
|
||||
--el-font-size-base: #{$font-size-base};
|
||||
// 更多变量覆盖...
|
||||
}
|
||||
|
||||
// 深度选择器示例
|
||||
:deep(.el-button) {
|
||||
&--primary {
|
||||
background-color: $primary-color;
|
||||
border-color: $primary-color;
|
||||
|
||||
&:hover {
|
||||
background-color: lighten($primary-color, 10%);
|
||||
border-color: lighten($primary-color, 10%);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3.4 组件样式实现示例
|
||||
|
||||
**按钮组件定制:**
|
||||
```vue
|
||||
<template>
|
||||
<el-button
|
||||
:type="type"
|
||||
:size="size"
|
||||
:loading="loading"
|
||||
class="custom-button"
|
||||
>
|
||||
<slot></slot>
|
||||
</el-button>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.custom-button {
|
||||
border-radius: $border-radius-base;
|
||||
font-weight: $font-weight-primary;
|
||||
|
||||
// 根据 Figma 设计稿调整
|
||||
&:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
```
|
||||
### 3.5 图标处理方案
|
||||
**优先使用element-plus的icon图标库,可以在element-plus的官方文档中找到所有的图标,并将其引入到项目中**
|
||||
```bash
|
||||
|
||||
**自定义 SVG 图标:在还原设计图的情况下如果element-plus的icon图标库不满足需求,可以将设计图中的icon下载为svg,**
|
||||
```vue
|
||||
<template>
|
||||
<svg-icon name="custom-icon" class="icon" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
// 创建全局 SVG 图标组件
|
||||
defineProps<{
|
||||
name: string
|
||||
size?: string
|
||||
}>()
|
||||
</script>
|
||||
```
|
||||
|
||||
### 3.6 公共样式抽取
|
||||
|
||||
**工具类样式 (`/src/assets/styles/global.scss`):**
|
||||
|
||||
|
||||
## 4. 开发流程和最佳实践
|
||||
|
||||
### 4.1 Claude 协作开发流程
|
||||
|
||||
**步骤1:需求分析**
|
||||
```
|
||||
1. 向 Claude 描述功能需求
|
||||
2. 提供 Figma 设计稿链接或截图
|
||||
3. 说明技术栈要求(Vue 3 + TypeScript + Element Plus)
|
||||
```
|
||||
|
||||
**步骤2:代码生成**
|
||||
```
|
||||
提问示例:
|
||||
"请根据这个 Figma 设计稿生成一个用户管理页面,包含:
|
||||
- 用户列表表格
|
||||
- 搜索和筛选功能
|
||||
- 新增/编辑用户弹窗
|
||||
- 使用 Element Plus 组件库"
|
||||
```
|
||||
|
||||
**步骤3:代码审查与优化**
|
||||
```
|
||||
代码审查要点:
|
||||
1. TypeScript 类型定义是否完整
|
||||
2. 组件设计是否合理
|
||||
3. 样式是否符合设计规范
|
||||
4. 性能优化是否到位
|
||||
```
|
||||
|
||||
### 4.2 组件开发最佳实践
|
||||
|
||||
**组件设计原则:**
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
// 1. 定义清晰的 Props 接口
|
||||
interface Props {
|
||||
title: string
|
||||
data: any[]
|
||||
loading?: boolean
|
||||
}
|
||||
|
||||
// 2. 使用 withDefaults 设置默认值
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
loading: false
|
||||
})
|
||||
|
||||
// 3. 定义明确的 Emit 事件
|
||||
const emit = defineEmits<{
|
||||
update: [value: any]
|
||||
delete: [id: string]
|
||||
}>()
|
||||
|
||||
// 4. 使用 Composition API 组织逻辑
|
||||
const { data, loading, error } = useUserData()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- 5. 合理的模板结构 -->
|
||||
<div class="user-table">
|
||||
<el-table :data="data" :loading="loading">
|
||||
<!-- ... -->
|
||||
</el-table>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
// 6. 作用域样式
|
||||
.user-table {
|
||||
// 样式定义
|
||||
}
|
||||
</style>
|
||||
```
|
||||
### 4.3 样式开发最佳实践
|
||||
|
||||
**响应式设计:**
|
||||
```scss
|
||||
// 响应式断点
|
||||
$breakpoint-mobile: 768px;
|
||||
$breakpoint-tablet: 992px;
|
||||
$breakpoint-desktop: 1200px;
|
||||
|
||||
// 响应式混入
|
||||
@mixin mobile {
|
||||
@media (max-width: #{$breakpoint-mobile - 1px}) {
|
||||
@content;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin tablet {
|
||||
@media (min-width: #{$breakpoint-mobile}) and (max-width: #{$breakpoint-tablet - 1px}) {
|
||||
@content;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin desktop {
|
||||
@media (min-width: #{$breakpoint-desktop}) {
|
||||
@content;
|
||||
}
|
||||
}
|
||||
|
||||
// 使用示例
|
||||
.user-card {
|
||||
padding: 16px;
|
||||
|
||||
@include mobile {
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
@include desktop {
|
||||
padding: 24px;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**主题切换支持:**
|
||||
```scss
|
||||
// 支持暗色模式
|
||||
:root {
|
||||
--bg-color: #ffffff;
|
||||
--text-color: #333333;
|
||||
}
|
||||
|
||||
[data-theme="dark"] {
|
||||
--bg-color: #1a1a1a;
|
||||
--text-color: #ffffff;
|
||||
}
|
||||
|
||||
.app-container {
|
||||
background-color: var(--bg-color);
|
||||
color: var(--text-color);
|
||||
}
|
||||
```
|
||||
|
||||
### 4.5 性能优化建议
|
||||
|
||||
**组件懒加载:**
|
||||
```typescript
|
||||
// router/index.ts
|
||||
const routes = [
|
||||
{
|
||||
path: '/users',
|
||||
name: 'UserManagement',
|
||||
component: () => import('@/views/UserManagement.vue')
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
**图片懒加载和优化:**
|
||||
```vue
|
||||
<template>
|
||||
<el-image
|
||||
:src="imageUrl"
|
||||
fit="cover"
|
||||
lazy
|
||||
:preview-src-list="[imageUrl]"
|
||||
:placeholder="placeholderImage"
|
||||
/>
|
||||
</template>
|
||||
```
|
||||
|
||||
## 5. 常见问题和解决方案
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
**问题3:SCSS 变量未定义**
|
||||
```scss
|
||||
// 解决方案:在 vite.config.ts 中配置全局 SCSS 变量
|
||||
export default defineConfig({
|
||||
css: {
|
||||
preprocessorOptions: {
|
||||
scss: {
|
||||
additionalData: `@import "@/assets/styles/variables.scss";`
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
```
|
||||
### 5.3 样式开发问题
|
||||
|
||||
**问题1:移动端适配问题**
|
||||
```scss
|
||||
// 解决方案:使用视口单位和媒体查询
|
||||
.container {
|
||||
width: 100vw;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 0 4vw;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
padding: 0 5vw;
|
||||
}
|
||||
}
|
||||
|
||||
// 字体大小适配
|
||||
.title {
|
||||
font-size: clamp(1.5rem, 4vw, 2.5rem);
|
||||
}
|
||||
```
|
||||
|
||||
**问题2:Figma导出的颜色值与实际不符**
|
||||
```scss
|
||||
// 解决方案:检查色彩空间和透明度
|
||||
// Figma 中显示 rgba(255, 0, 0, 0.5)
|
||||
// 可能需要转换为
|
||||
.element {
|
||||
background-color: rgba(255, 0, 0, 0.5);
|
||||
/* 或者使用 HSL */
|
||||
background-color: hsla(0, 100%, 50%, 0.5);
|
||||
}
|
||||
```
|
||||
|
||||
**问题3:Element Plus 主题定制不生效**
|
||||
```scss
|
||||
// 错误写法:直接覆盖 CSS 变量可能不生效
|
||||
:root {
|
||||
--el-color-primary: #ff0000;
|
||||
}
|
||||
|
||||
// 正确写法:按照 Element Plus 规范
|
||||
// 1. 使用 SCSS 变量
|
||||
$--color-primary: #ff0000;
|
||||
|
||||
// 2. 或在根元素上设置
|
||||
html {
|
||||
--el-color-primary: #ff0000;
|
||||
}
|
||||
|
||||
// 3. 使用 Element Plus 主题配置
|
||||
import { ElConfigProvider } from 'element-plus'
|
||||
|
||||
这个完善的指南应该能帮助你更好地使用 Claude 和 Figma 进行 Vue 3 + TypeScript + Element Plus 应用的开发。如果在开发过程中遇到其他问题,可以随时参考这个文档或向 Claude 寻求帮助。
|
||||
@@ -0,0 +1,229 @@
|
||||
# 软件授权管理平台多语言框架设计方案
|
||||
|
||||
## 1. 需求分析
|
||||
|
||||
根据需求说明书,软件授权管理平台需要支持国际化,主要体现在:
|
||||
|
||||
- **用户界面多语言**:Web管理界面需要支持中文、英文、日文等多种语言
|
||||
- **错误信息多语言**:API接口返回的错误信息需要本地化
|
||||
- **文档多语言**:用户操作手册、API接口文档需要多语言版本
|
||||
- **全球化部署**:支持不同国家和地区的客户使用
|
||||
|
||||
## 2. 技术方案选型
|
||||
|
||||
### 2.1 前端多语言方案
|
||||
|
||||
**推荐使用 Vue I18n**
|
||||
|
||||
**选择理由:**
|
||||
- 与Vue.js 3完美集成,是Vue生态官方推荐方案
|
||||
- 支持Composition API,符合现代Vue开发模式
|
||||
- 提供完整的国际化功能:消息格式化、复数形式、日期时间格式化
|
||||
- 与Element Plus等UI组件库良好兼容
|
||||
- 支持动态语言切换和懒加载
|
||||
- 生态成熟,文档完善,社区活跃
|
||||
|
||||
**对比其他方案:**
|
||||
- **react-i18next**:专为React设计,不适用于Vue项目
|
||||
- **Fluent.js**:Mozilla开发,功能强大但学习成本较高
|
||||
- **FormatJS**:功能全面但配置复杂,适合大型企业级应用
|
||||
|
||||
### 2.2 后端多语言方案
|
||||
|
||||
**推荐使用 Go-i18n**
|
||||
|
||||
**选择理由:**
|
||||
- 专为Go语言设计,API简洁易用
|
||||
- 支持复数形式和消息模板
|
||||
- 与现有Go技术栈无缝集成
|
||||
- 支持多种消息格式(JSON、TOML、YAML)
|
||||
- 轻量级实现,性能优秀
|
||||
|
||||
## 3. 架构设计
|
||||
|
||||
### 3.1 语言支持范围
|
||||
|
||||
**初期支持语言:**
|
||||
- 中文(zh-CN):简体中文,默认语言
|
||||
- 英文(en-US):美式英语,国际通用语言
|
||||
- 日文(ja-JP):日本语,重要亚洲市场
|
||||
|
||||
**后期扩展语言:**
|
||||
- 韩文(ko-KR)
|
||||
- 德文(de-DE)
|
||||
- 法文(fr-FR)
|
||||
|
||||
### 3.2 前端国际化架构
|
||||
|
||||
**语言包组织结构:**
|
||||
- 采用模块化语言包设计
|
||||
- 按功能模块分离翻译文件
|
||||
- 支持命名空间隔离,避免键值冲突
|
||||
- 集成Element Plus组件库语言包
|
||||
|
||||
**语言切换机制:**
|
||||
- 支持实时语言切换,无需刷新页面
|
||||
- 语言偏好存储在localStorage中持久化
|
||||
- 根据浏览器语言自动检测默认语言
|
||||
- 提供语言选择器组件
|
||||
|
||||
**文本键值设计原则:**
|
||||
- 使用语义化命名,便于维护
|
||||
- 按页面和功能模块分组织
|
||||
- 支持插值变量和复数形式
|
||||
- 提供默认文本兜底机制
|
||||
|
||||
### 3.3 后端国际化架构
|
||||
|
||||
**错误信息国际化:**
|
||||
- 统一错误码映射多语言消息
|
||||
- 支持动态参数注入
|
||||
- 根据Accept-Language请求头自动选择语言
|
||||
- 提供错误信息翻译中间件
|
||||
|
||||
**API响应国际化:**
|
||||
- 状态描述信息支持多语言
|
||||
- 数据验证错误消息本地化
|
||||
- 系统提示信息多语言支持
|
||||
|
||||
### 3.4 数据存储设计
|
||||
|
||||
**语言包存储:**
|
||||
- 前端语言包采用JSON格式静态文件
|
||||
- 后端语言包采用JSON格式,支持热更新
|
||||
- 考虑语言包缓存机制提升性能
|
||||
|
||||
**用户语言偏好:**
|
||||
- 在用户会话中记录语言偏好
|
||||
- 支持API请求携带语言参数覆盖默认设置
|
||||
|
||||
## 4. 实施策略
|
||||
|
||||
### 4.1 开发阶段规划
|
||||
|
||||
**第一阶段:基础框架搭建**
|
||||
- 配置Vue I18n和Go-i18n基础框架
|
||||
- 建立语言包文件结构和命名规范
|
||||
- 实现语言切换基础功能
|
||||
|
||||
**第二阶段:核心功能国际化**
|
||||
- 完成客户管理模块多语言适配
|
||||
- 实现授权管理模块界面国际化
|
||||
- 适配系统管理相关功能
|
||||
|
||||
**第三阶段:完善优化**
|
||||
- 补充所有界面文本的多语言版本
|
||||
- 优化语言包加载性能
|
||||
- 完善错误处理和兜底机制
|
||||
|
||||
### 4.2 翻译管理策略
|
||||
|
||||
**翻译流程:**
|
||||
- 开发阶段使用中文作为主要语言
|
||||
- 功能稳定后进行专业翻译
|
||||
- 建立翻译审核和更新机制
|
||||
|
||||
**翻译质量保证:**
|
||||
- 关键术语建立统一词汇表
|
||||
- 界面文本保持简洁明了
|
||||
- 考虑不同语言的文化差异
|
||||
|
||||
## 5. Element Plus集成最佳实践
|
||||
|
||||
|
||||
### 5.2 正确的初始化配置
|
||||
|
||||
**Element Plus配置:**
|
||||
```typescript
|
||||
// main.ts中正确配置
|
||||
app.use(ElementPlus, {
|
||||
locale: getCurrentElementLocale() // 传递正确的locale
|
||||
})
|
||||
```
|
||||
|
||||
**浏览器语言自动检测:**
|
||||
```typescript
|
||||
function detectBrowserLanguage(): SupportedLocale {
|
||||
// 优先级:localStorage > 浏览器语言 > 默认中文
|
||||
const savedLanguage = localStorage.getItem('userLanguage')
|
||||
if (savedLanguage && supportedLanguages.includes(savedLanguage)) {
|
||||
return savedLanguage
|
||||
}
|
||||
|
||||
const browserLanguage = navigator.language.toLowerCase()
|
||||
if (browserLanguage.startsWith('en')) return 'en'
|
||||
if (browserLanguage.startsWith('ja')) return 'ja'
|
||||
return 'zh' // 默认回退
|
||||
}
|
||||
```
|
||||
|
||||
### 5.3 使用场景划分
|
||||
|
||||
**Vue I18n (`t()`) 负责:**
|
||||
- 页面标题、表单标签、按钮文本
|
||||
- 错误信息、提示消息
|
||||
- 业务相关的自定义文本
|
||||
|
||||
**Element Plus 自动处理:**
|
||||
- 组件内部文本:日期选择器、分页器、表格等
|
||||
- 系统级提示:Loading、Empty、确认对话框等
|
||||
- 表单验证提示信息
|
||||
|
||||
## 6. 技术实现要点
|
||||
|
||||
### 6.1 前端实现关键点
|
||||
|
||||
**语言包格式统一:**
|
||||
- 使用JSON格式存储翻译文件,便于维护
|
||||
- 按模块组织语言包结构,避免冲突
|
||||
|
||||
**浏览器语言检测:**
|
||||
- 支持自动检测浏览器语言偏好
|
||||
- 实现智能回退机制和用户偏好记忆
|
||||
|
||||
**响应式设计适配:**
|
||||
- 考虑不同语言文本长度差异对布局的影响
|
||||
- 设计弹性布局应对文本长度变化
|
||||
|
||||
### 6.2 后端实现关键点
|
||||
|
||||
**性能优化:**
|
||||
- 语言包预加载和内存缓存
|
||||
- 避免每次请求都进行文件读取
|
||||
|
||||
**扩展性设计:**
|
||||
- 支持运行时动态添加新语言
|
||||
- 提供语言包热更新机制
|
||||
|
||||
## 7. 实际改进记录
|
||||
|
||||
### 7.1 已完成的优化
|
||||
|
||||
**文件格式标准化:**
|
||||
- ✅ 将语言包从TypeScript格式改为JSON格式
|
||||
- ✅ 统一文件命名:`zh.json`、`en.json`、`ja.json`
|
||||
|
||||
**浏览器语言自动检测:**
|
||||
- ✅ 实现`detectBrowserLanguage()`函数
|
||||
- ✅ 支持localStorage偏好 > 浏览器语言 > 默认回退的优先级
|
||||
|
||||
**Element Plus冲突解决:**
|
||||
- ✅ 分离Vue I18n和Element Plus语言包管理
|
||||
- ✅ 正确配置Element Plus初始化参数
|
||||
- ✅ 避免语言包合并导致的键值覆盖问题
|
||||
|
||||
## 8. 优势总结
|
||||
|
||||
**Vue I18n方案优势:**
|
||||
- 成熟稳定的技术方案,风险可控
|
||||
- 与现有技术栈完美契合,学习成本低
|
||||
- 功能完整,满足项目所有国际化需求
|
||||
- 社区支持良好,问题解决效率高
|
||||
- 支持渐进式国际化,可分阶段实施
|
||||
|
||||
**整体架构优势:**
|
||||
- 前后端统一的国际化策略
|
||||
- 模块化设计便于维护和扩展
|
||||
- 性能优化保证用户体验
|
||||
- 支持未来业务国际化扩展需求
|
||||
- 正确处理Vue I18n与Element Plus的集成关系
|
||||
@@ -1,31 +1,55 @@
|
||||
{
|
||||
"name": "vue3_vite",
|
||||
"name": "license-manager-frontend",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"description": "License Manager Frontend built with Vue 3 + TypeScript + Element Plus",
|
||||
"author": "License Manager Team",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"serve": "vite",
|
||||
"test": "vue-tsc && vite build --mode test",
|
||||
"prod": "vue-tsc && vite build --mode production",
|
||||
"preview": "vite preview"
|
||||
"build": "vue-tsc --noEmit && vite build",
|
||||
"build:test": "vue-tsc --noEmit && vite build --mode test",
|
||||
"build:prod": "vue-tsc --noEmit && vite build --mode production",
|
||||
"preview": "vite preview",
|
||||
"type-check": "vue-tsc --noEmit",
|
||||
"lint": "echo 'ESLint temporarily disabled due to config migration'",
|
||||
"test": "vitest",
|
||||
"test:coverage": "vitest --coverage"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^1.3.6",
|
||||
"element-plus": "^2.3.4",
|
||||
"rollup-plugin-visualizer": "^6.0.3",
|
||||
"sass": "^1.89.2",
|
||||
"vite-plugin-eslint": "^1.8.1",
|
||||
"vue": "^3.2.47",
|
||||
"@element-plus/icons-vue": "^2.3.2",
|
||||
"axios": "^1.7.0",
|
||||
"echarts": "^5.6.0",
|
||||
"element-plus": "^2.10.5",
|
||||
"pinia": "^2.3.1",
|
||||
"vue": "^3.5.0",
|
||||
"vue-echarts": "^7.0.3",
|
||||
"vue-i18n": "^9.14.5",
|
||||
"vue-router": "^4.1.6"
|
||||
"vue-router": "^4.4.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^18.16.16",
|
||||
"@vitejs/plugin-vue": "^4.1.0",
|
||||
"@types/node": "^22.0.0",
|
||||
"@typescript-eslint/eslint-plugin": "^8.0.0",
|
||||
"@typescript-eslint/parser": "^8.0.0",
|
||||
"@vitejs/plugin-vue": "^5.0.0",
|
||||
"@vue/eslint-config-prettier": "^9.0.0",
|
||||
"@vue/eslint-config-typescript": "^13.0.0",
|
||||
"eslint": "^8.57.1",
|
||||
"eslint-plugin-vue": "^9.0.0",
|
||||
"prettier": "^3.0.0",
|
||||
"rollup-plugin-visualizer": "^5.12.0",
|
||||
"sass": "^1.89.2",
|
||||
"terser": "^5.43.1",
|
||||
"typescript": "^5.8.3",
|
||||
"vite": "^4.3.0",
|
||||
"vite": "^5.0.0",
|
||||
"vite-plugin-compression": "^0.5.1",
|
||||
"vue-tsc": "^3.0.4"
|
||||
"vitest": "^2.0.0",
|
||||
"vue-eslint-parser": "^10.0.0",
|
||||
"vue-tsc": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0",
|
||||
"npm": ">=8.0.0"
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,34 @@
|
||||
<!--
|
||||
* @Author: 13895237362 2205451508@qq.com
|
||||
* @Date: 2025-08-05 14:46:50
|
||||
* @LastEditors: 13895237362 2205451508@qq.com
|
||||
* @LastEditTime: 2025-08-05 15:31:22
|
||||
* @FilePath: /frontend/src/App.vue
|
||||
* @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
|
||||
-->
|
||||
<template>
|
||||
<div id="app">
|
||||
<el-config-provider :locale="elementLocale">
|
||||
<router-view />
|
||||
</div>
|
||||
</el-config-provider>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
// 根组件,只负责渲染路由视图
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import zhCn from 'element-plus/es/locale/lang/zh-cn'
|
||||
import en from 'element-plus/es/locale/lang/en'
|
||||
import ja from 'element-plus/es/locale/lang/ja'
|
||||
|
||||
const { locale } = useI18n()
|
||||
|
||||
const localeMap = {
|
||||
zh: zhCn,
|
||||
en: en,
|
||||
ja: ja
|
||||
}
|
||||
const elementLocale = computed(() => {
|
||||
return localeMap[locale.value as keyof typeof localeMap] || zhCn
|
||||
})
|
||||
</script>
|
||||
|
||||
<style>
|
||||
|
||||
@@ -1,3 +1,11 @@
|
||||
/*
|
||||
* @Author: 13895237362 2205451508@qq.com
|
||||
* @Date: 2025-07-31 14:30:20
|
||||
* @LastEditors: 13895237362 2205451508@qq.com
|
||||
* @LastEditTime: 2025-08-13 09:38:31
|
||||
* @FilePath: /frontend/src/api/https/errorCodeType.ts
|
||||
* @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
|
||||
*/
|
||||
export const errorCodeType = function(status:Number):string{
|
||||
let errMessage:string = "未知错误"
|
||||
switch (status) {
|
||||
|
||||
@@ -2,8 +2,9 @@ import axios from 'axios'
|
||||
|
||||
// http状态码
|
||||
import { errorCodeType } from './errorCodeType'
|
||||
//element-plus提示消息组件
|
||||
import { ElMessage } from "element-plus"
|
||||
// 语言管理器
|
||||
import { generateAcceptLanguageHeader } from '@/utils/language'
|
||||
|
||||
const envUrl = import.meta.env.VITE_API_BASE_URL
|
||||
|
||||
//使用create方法创建axios实例
|
||||
@@ -13,8 +14,8 @@ const Axios = axios.create({
|
||||
headers: {
|
||||
'Content-Type': 'application/json;charset=UTF-8',
|
||||
},
|
||||
// 跨域时候允许携带凭证
|
||||
withCredentials: true
|
||||
// 跨域时候允许携带凭证 (开发环境不启用,避免CORS问题)
|
||||
withCredentials: false
|
||||
})
|
||||
// 添加请求拦截器
|
||||
Axios.interceptors.request.use(config => {
|
||||
@@ -23,6 +24,11 @@ Axios.interceptors.request.use(config => {
|
||||
if(token){
|
||||
config.headers['Authorization'] = `Bearer ${token}`
|
||||
}
|
||||
|
||||
// 设置 Accept-Language 头,符合 RFC 7231 规范
|
||||
// 优先级:用户选择;q=1.0 > 备用语言;q=0.8 > 通配符;q=0.5
|
||||
config.headers['Accept-Language'] = generateAcceptLanguageHeader()
|
||||
|
||||
return config
|
||||
})
|
||||
// 添加响应拦截器
|
||||
@@ -33,7 +39,7 @@ Axios.interceptors.response.use(response => {
|
||||
const code = apiData.code
|
||||
// 如果没有 Code, 代表这不是项目后端开发的 API
|
||||
if (code === undefined) {
|
||||
ElMessage.error("非本系统的接口")
|
||||
// 不在拦截器中显示错误,让组件自己处理
|
||||
return Promise.reject(new Error("非本系统的接口"))
|
||||
} else {
|
||||
switch (code) {
|
||||
@@ -44,30 +50,35 @@ Axios.interceptors.response.use(response => {
|
||||
// code === 0 也代表成功 (兼容)
|
||||
return apiData
|
||||
default:
|
||||
// 不是正确的 Code
|
||||
ElMessage.error(apiData.message || "Error")
|
||||
return Promise.reject(new Error("Error"))
|
||||
// 不在拦截器中显示错误,让组件自己处理
|
||||
// 将错误信息附加到 Error 对象上,供组件使用
|
||||
const error = new Error(apiData.message || "Error")
|
||||
;(error as any).response = { data: apiData }
|
||||
return Promise.reject(error)
|
||||
}
|
||||
}
|
||||
}, error => {
|
||||
console.log('Response的error', error)
|
||||
showErrMessage(errorCodeType(error.response.status))
|
||||
// 不在拦截器中显示错误消息,让具体的页面组件处理
|
||||
// showErrMessage(errorCodeType(error.response?.status))
|
||||
|
||||
// 为错误对象添加更多信息,便于组件处理
|
||||
if (error.response) {
|
||||
const status = error.response.status
|
||||
const data = error.response.data
|
||||
|
||||
// 如果后端返回了错误消息,优先使用后端消息
|
||||
if (data && data.message) {
|
||||
;(error as any).backendMessage = data.message
|
||||
} else {
|
||||
// 否则使用状态码对应的消息
|
||||
;(error as any).backendMessage = errorCodeType(status)
|
||||
}
|
||||
}
|
||||
|
||||
return Promise.reject(error)
|
||||
})
|
||||
|
||||
/**
|
||||
* @description 显示错误消息
|
||||
* opt 传入参数
|
||||
* err 错误信息
|
||||
* type 消息类型
|
||||
* duration 消息持续时间
|
||||
*/
|
||||
function showErrMessage (errMessage: string, type:any= 'error', duration:number = 5000){
|
||||
ElMessage({
|
||||
message: errMessage,
|
||||
type:type,
|
||||
duration: duration
|
||||
})
|
||||
}
|
||||
// 错误处理函数已移除,由页面组件统一处理错误显示
|
||||
|
||||
export default Axios;
|
||||
@@ -0,0 +1,3 @@
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M10.0518 11.6455C11.567 11.6455 13.0337 11.9493 14.4092 12.5527C15.7381 13.134 16.9208 13.9635 17.9258 15.0195L18.3809 15.499L17.9834 16.0244C17.0716 17.2313 15.8779 18.2315 14.5312 18.917C13.1203 19.6358 11.5961 20 10.001 20C8.41942 20 6.90875 19.6408 5.50684 18.9355C4.17128 18.2633 2.98142 17.278 2.06738 16.0889L1.66602 15.5654L2.11621 15.084C3.12342 14.0102 4.31528 13.1627 5.65527 12.5703C7.04169 11.9559 8.52117 11.6456 10.0518 11.6455ZM10.0518 13.1982C7.67586 13.1983 5.4571 14.07 3.7334 15.665C5.33514 17.442 7.58283 18.4472 10.001 18.4473C12.4458 18.4473 14.7107 17.4216 16.3125 15.6113C14.5976 14.0497 12.3988 13.1982 10.0518 13.1982ZM10.001 0C11.3498 0 12.6591 0.263803 13.8926 0.785156C15.0839 1.28876 16.1528 2.01026 17.0713 2.92871C17.9897 3.84716 18.7112 4.91611 19.2148 6.10742C19.7362 7.34092 20 8.65016 20 9.99902C20 11.0905 19.8246 12.1665 19.4785 13.1914L18.0078 12.6963C18.2984 11.8312 18.4473 10.9238 18.4473 10.001C18.4472 5.3444 14.6556 1.55574 9.99902 1.55566C5.34238 1.55566 1.55281 5.34435 1.55273 10.001C1.55273 10.9482 1.70857 11.8776 2.01465 12.7627L0.547852 13.2715C0.184014 12.2221 0 11.1213 0 10.001C1.71756e-05 8.64994 0.26382 7.34088 0.785156 6.10742C1.28876 4.91611 2.01026 3.84716 2.92871 2.92871C3.84716 2.01026 4.91611 1.28876 6.10742 0.785156C7.34088 0.26382 8.65216 1.73133e-05 10.001 0ZM10.0518 4.89648C11.7578 4.89648 13.1475 6.28517 13.1475 7.99121C13.1474 9.69723 11.7578 11.0859 10.0518 11.0859C8.34591 11.0857 6.95707 9.6971 6.95703 7.99121C6.95703 6.28529 8.34589 4.89669 10.0518 4.89648ZM10.0518 6.44922C9.20224 6.44942 8.51074 7.14164 8.51074 7.99121C8.51078 8.84075 9.20226 9.533 10.0518 9.5332C10.9014 9.5332 11.5937 8.84087 11.5938 7.99121C11.5938 7.14152 10.9015 6.44922 10.0518 6.44922Z" fill="#B2B8C2"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.8 KiB |
@@ -0,0 +1,3 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M6.52954 1.85626C3.75062 2.51889 1.68419 5.01808 1.68419 7.99992C1.68419 11.488 4.51184 14.3156 7.99992 14.3156C10.9818 14.3156 13.4809 12.2492 14.1436 9.4703C13.1961 10.2948 11.958 10.7941 10.6028 10.7941C7.62202 10.7941 5.2057 8.37781 5.2057 5.39708C5.2057 4.04179 5.70503 2.80374 6.52954 1.85626ZM0 7.99992C0 3.58168 3.58168 0 7.99992 0C8.5542 0 9.09619 0.0564954 9.62011 0.164329C10.0035 0.24324 10.282 0.575925 10.2922 0.967222C10.3023 1.35852 10.0416 1.70524 9.66279 1.80399C8.06687 2.22008 6.8899 3.67218 6.8899 5.39708C6.8899 7.44766 8.55218 9.10994 10.6028 9.10994C12.3277 9.10994 13.7798 7.93297 14.1958 6.33706C14.2946 5.95829 14.6413 5.6975 15.0326 5.70768C15.4239 5.71786 15.7566 5.99632 15.8355 6.3797C15.9433 6.90363 15.9998 7.44564 15.9998 7.99992C15.9998 12.4182 12.4182 15.9998 7.99992 15.9998C3.58168 15.9998 0 12.4182 0 7.99992Z" fill="#1D1D1D"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1019 B |
@@ -0,0 +1,3 @@
|
||||
<svg width="19" height="20" viewBox="0 0 19 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M10.7397 0.30622L17.8809 4.06812C18.2187 4.24611 18.5012 4.51164 18.6984 4.83633C18.8955 5.16101 18.9997 5.53264 19 5.91145V14.103C19 14.8779 18.5678 15.5889 17.8771 15.9501L10.735 19.696C9.96265 20.1013 9.03735 20.1013 8.265 19.696L1.1229 15.952C0.783836 15.7742 0.50013 15.5083 0.302282 15.1828C0.104435 14.8574 -7.08312e-05 14.4847 3.60183e-08 14.1049V5.91333C3.60183e-08 5.14026 0.43035 4.4302 1.1191 4.06718L8.26025 0.30622C8.64192 0.105155 9.06765 0 9.5 0C9.93235 0 10.3581 0.105155 10.7397 0.30622ZM8.6507 2.00002L8.4911 2.07807L2.7816 5.15343C2.53785 5.2849 2.33059 5.47382 2.17814 5.70347C2.0257 5.93312 1.93278 6.19643 1.9076 6.47009L1.9 6.62245V13.392C1.9 13.9582 2.1888 14.4829 2.66285 14.7895L2.78445 14.862L8.49395 17.9241C8.77644 18.0757 9.09015 18.1615 9.41119 18.1748C9.73222 18.1882 10.0521 18.1288 10.3464 18.0013L10.5051 17.9241L16.2155 14.862C16.719 14.592 17.0468 14.0908 17.0943 13.5321L17.1 13.391V6.62245C17.1 6.06099 16.815 5.5409 16.3495 5.23243L16.2184 5.15343L10.5089 2.07807C10.2258 1.92545 9.91106 1.83906 9.58895 1.82553C9.26684 1.812 8.94589 1.87168 8.6507 2.00002ZM13.4862 6.78985C13.5943 6.72713 13.7138 6.68618 13.838 6.66938C13.9621 6.65258 14.0884 6.66025 14.2095 6.69195C14.3306 6.72365 14.4442 6.77876 14.5437 6.8541C14.6432 6.92944 14.7267 7.02353 14.7894 7.13094C14.852 7.23835 14.8926 7.35697 14.9087 7.47996C14.9249 7.60296 14.9163 7.7279 14.8835 7.8476C14.8506 7.9673 14.7942 8.07938 14.7175 8.17741C14.6407 8.27543 14.5451 8.35746 14.4362 8.41876L10.45 10.6966V14.7011C10.45 14.9506 10.3499 15.1898 10.1718 15.3661C9.99359 15.5425 9.75196 15.6416 9.5 15.6416C9.24804 15.6416 9.00641 15.5425 8.82825 15.3661C8.65009 15.1898 8.55 14.9506 8.55 14.7011V10.6975L4.5638 8.41876C4.34738 8.29316 4.18994 8.08791 4.12581 7.84781C4.06169 7.60771 4.09611 7.35225 4.22154 7.13718C4.34697 6.9221 4.55325 6.76487 4.79535 6.69981C5.03745 6.63474 5.29572 6.66711 5.5138 6.78985L9.4848 9.05828H9.5133L13.4862 6.78985Z" fill="#019C7C"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.0 KiB |
@@ -0,0 +1,3 @@
|
||||
<svg width="19" height="19" viewBox="0 0 19 19" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M9.49992 18.9998C4.25311 18.9998 0 14.7467 0 9.49992C0 4.25311 4.25311 0 9.49992 0C14.7467 0 18.9998 4.25311 18.9998 9.49992C18.9998 14.7467 14.7467 18.9998 9.49992 18.9998ZM7.37975 16.9504C6.4425 14.9623 5.99966 12.5698 5.88104 10.3751H1.72726C1.91211 11.837 2.62962 13.4055 3.58213 14.5298C4.53464 15.6542 5.96807 16.5277 7.37975 16.9504ZM7.62843 10.3751C7.77188 12.6921 8.43402 14.9434 9.49992 16.8642C10.5946 14.8926 11.2341 12.6261 11.3714 10.3751H7.62843ZM17.2489 10.3751H13.0961C12.9775 12.5698 12.5607 14.9623 11.6235 16.9504C13.0351 16.5277 14.4709 15.6542 15.4234 14.5298C16.3759 13.4055 17.064 11.837 17.2489 10.3751ZM1.72631 8.61799H5.88104C5.99966 6.4233 6.44345 4.04156 7.3807 2.05352C5.96902 2.47615 4.53464 3.34291 3.58213 4.46726C2.62962 5.59162 1.91116 7.15605 1.72631 8.61799ZM7.62843 8.61799H11.3695C11.2325 6.36708 10.5943 4.10732 9.49992 2.13558C8.40522 4.10724 7.76481 6.367 7.62748 8.61799H7.62843ZM11.6235 2.05352C12.5607 4.04156 12.9765 6.4233 13.0952 8.61799H17.2479C17.0631 7.15605 16.3759 5.58593 15.4234 4.46157C14.4709 3.33722 13.0351 2.47615 11.6235 2.05352Z" fill="#1D1D1D"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
@@ -0,0 +1,3 @@
|
||||
<svg width="20" height="17" viewBox="0 0 20 17" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M9.37266 9.52207H3.75014C3.40733 9.52207 3.12473 9.83257 3.12473 10.2102V11.567H3.75014C4.4381 11.567 5.00097 12.1735 5.00097 12.9247V15.6407C5.00097 16.3944 4.4381 16.9976 3.75014 16.9976H1.25082C0.562097 17 0 16.3944 0 15.6407V12.9263C0 12.1727 0.562097 11.5694 1.25082 11.5694H1.87623V10.2126C1.87623 9.083 2.7186 8.16686 3.75014 8.16686H9.37498V6.81003H7.50029C6.46952 6.81003 5.62638 5.89469 5.62638 4.76508V2.04495C5.62638 0.916144 6.46952 0 7.50029 0H12.4997C13.5305 0 14.3736 0.916144 14.3736 2.04576V4.76104C14.3736 5.89065 13.5305 6.80679 12.4997 6.80679H10.6258V8.16362H16.2499C17.2814 8.16362 18.1238 9.07896 18.1238 10.2086V11.5654H18.7492C19.4379 11.5654 20 12.171 20 12.9222V15.6391C20 16.3919 19.4379 16.996 18.7492 16.996H16.2475C15.5604 16.996 14.9967 16.3895 14.9967 15.6383V12.9263C14.9967 12.1727 15.5604 11.5694 16.2475 11.5694H16.8729V10.2126C16.8729 9.83662 16.5927 9.5245 16.2475 9.5245H10.6235V11.5694H11.2489C11.9368 11.5694 12.4997 12.1751 12.4997 12.9263V15.6432C12.4997 16.396 11.9368 17 11.2489 17H8.74957C8.06084 17 7.49874 16.3936 7.49874 15.6424V12.9263C7.49874 12.1727 8.06084 11.5694 8.74957 11.5694H9.37498V9.52288H9.37266V9.52207ZM7.49874 1.36006C7.15516 1.36006 6.87334 1.67057 6.87334 2.04737V4.76427C6.87334 5.14027 7.15361 5.45158 7.49874 5.45158H12.4974C12.841 5.45158 13.1228 5.14189 13.1228 4.76427V2.04495C13.1228 1.66976 12.8425 1.35764 12.4974 1.35764H7.49874V1.35926V1.36006ZM1.25005 12.9263V15.6432H3.74937V12.9263H1.25082H1.25005ZM8.74879 12.9263V15.6432H11.2481V12.9263H8.74879ZM16.2475 12.9263V15.6432H18.7469V12.9263H16.2468H16.2475Z" fill="#B2B8C2"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.7 KiB |
@@ -0,0 +1,4 @@
|
||||
<svg width="41" height="40" viewBox="0 0 41 40" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M26.3125 11.4814L22.25 19.5947V22.7148L27.1191 13.0576L29.7393 18.1777L18.7988 40H0L13.5938 22.8037H14L13.8125 23.1201L7.46875 33.541H17.8438V16.9111L7.1875 25.6475L18.0312 10.1406L11.625 14.4463V14.2588L20.4375 0L26.3125 11.4814Z" fill="#019C7C"/>
|
||||
<path d="M34.5498 39.9996H28.75L24.5938 32.8864L27.125 27.6246L34.5498 39.9996ZM41 39.9996H36.2705L27.9346 25.941L30.7188 20.1559L41 39.9996Z" fill="#146B59"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 521 B |
@@ -0,0 +1,3 @@
|
||||
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M15.919 6.92282V13.6726H17.127C17.605 13.6726 17.9924 14.06 17.9924 14.5379C17.9924 15.0158 17.605 15.4033 17.127 15.4033H11.9944C11.7847 16.8714 10.5222 18 8.9962 18C7.47015 18 6.2077 16.8714 5.99804 15.4033H0.865352C0.387431 15.4033 0 15.0158 0 14.5379C0 14.06 0.387431 13.6726 0.865352 13.6726H2.07339V6.92282C2.07339 3.09945 5.17283 0 8.99621 0C12.8196 0 15.919 3.09945 15.919 6.92282ZM3.80409 6.92282C3.80409 4.05529 6.12868 1.7307 8.99621 1.7307C11.8637 1.7307 14.1883 4.05529 14.1883 6.92282V13.6726H3.80409V6.92282ZM8.9962 16.2693C8.43104 16.2693 7.95023 15.9081 7.77203 15.4039H10.2204C10.0422 15.9081 9.56136 16.2693 8.9962 16.2693Z" fill="#1D1D1D"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 813 B |
@@ -0,0 +1,3 @@
|
||||
<svg width="19" height="20" viewBox="0 0 19 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12.2364 9.00488C13.0863 8.8125 13.9705 8.81285 14.8203 9.00586C14.9005 9.02409 14.9536 9.09659 14.9561 9.17676C14.9641 9.43807 15.0352 9.694 15.1631 9.92383C15.3138 10.1945 15.5388 10.4191 15.8125 10.5732C16.0654 10.7155 16.3516 10.7927 16.6436 10.7969C16.9354 10.801 17.2236 10.7325 17.4805 10.5977C17.5532 10.5595 17.6449 10.5681 17.7012 10.627C17.9977 10.9369 18.2568 11.2793 18.4737 11.6465C18.6915 12.0133 18.8654 12.4032 18.9922 12.8086C19.0162 12.8853 18.9783 12.9666 18.9082 13.0088C18.6802 13.1462 18.4887 13.334 18.3487 13.5566C18.1116 13.9335 18.0378 14.3865 18.1426 14.8164C18.2474 15.2463 18.5226 15.6186 18.9082 15.8516C18.9778 15.8938 19.0149 15.9743 18.9912 16.0508C18.738 16.8649 18.2971 17.6123 17.7012 18.2344C17.6449 18.2932 17.5531 18.3018 17.4805 18.2637C17.2235 18.1289 16.9355 18.0602 16.6436 18.0645C16.3516 18.0687 16.0653 18.1457 15.8125 18.2881C15.5388 18.4423 15.3137 18.6677 15.1631 18.9385C15.0354 19.1683 14.965 19.4244 14.9571 19.6855C14.9545 19.7656 14.9013 19.8383 14.8213 19.8564C13.9713 20.0488 13.0871 20.0475 12.2373 19.8545C12.1573 19.8363 12.1042 19.7646 12.1016 19.6846C12.0936 19.4232 12.0226 19.1664 11.8946 18.9365C11.7438 18.6659 11.5189 18.4412 11.2451 18.2871C10.9923 18.1449 10.706 18.0677 10.4141 18.0635C10.1221 18.0593 9.83417 18.1288 9.57717 18.2637C9.50437 18.3019 9.4128 18.2923 9.35647 18.2334C9.06018 17.9235 8.80091 17.5819 8.58401 17.2148C8.36633 16.8477 8.19245 16.4573 8.06545 16.0518C8.0414 15.975 8.07934 15.8938 8.14944 15.8516C8.37736 15.7142 8.56897 15.5263 8.70901 15.3037C8.94583 14.927 9.0197 14.4747 8.91506 14.0449C8.81027 13.615 8.53507 13.2427 8.14944 13.0098C8.07948 12.9676 8.04252 12.8862 8.06643 12.8096C8.31962 11.9956 8.76071 11.2489 9.35647 10.627C9.4128 10.5681 9.50446 10.5595 9.57717 10.5977C9.83414 10.7324 10.1223 10.8011 10.4141 10.7969C10.7061 10.7926 10.9923 10.7156 11.2451 10.5732C11.519 10.419 11.7439 10.1937 11.8946 9.92285C12.0224 9.69303 12.0927 9.43703 12.1006 9.17578C12.103 9.09561 12.1562 9.02312 12.2364 9.00488ZM13.9268 10.2295C13.6625 10.2046 13.3962 10.2049 13.1319 10.2295C12.9285 10.846 12.5028 11.3664 11.9375 11.6904C11.3752 12.0148 10.7075 12.1213 10.0684 11.9883C9.91487 12.2037 9.78194 12.4325 9.6719 12.6719C10.107 13.1552 10.3481 13.782 10.3477 14.4307C10.3476 15.095 10.0976 15.7155 9.6719 16.1885C9.78279 16.4279 9.91664 16.6572 10.0703 16.873C10.709 16.7403 11.3756 16.846 11.9375 17.1699C12.5025 17.4939 12.9277 18.0145 13.1309 18.6309C13.3954 18.6556 13.6613 18.6566 13.9258 18.6318C14.1291 18.0153 14.5548 17.4949 15.1201 17.1709C15.6824 16.8465 16.3492 16.7402 16.9883 16.873C17.1422 16.6574 17.2758 16.4282 17.3858 16.1885C16.9507 15.7053 16.7096 15.0793 16.71 14.4307C16.71 13.7663 16.96 13.1449 17.3858 12.6719C17.2749 12.4325 17.1409 12.2041 16.9873 11.9883C16.3487 12.1211 15.6821 12.0144 15.1201 11.6904C14.555 11.3664 14.1299 10.8459 13.9268 10.2295ZM7.43069 10.7314C7.45168 10.7396 7.4712 10.7512 7.48733 10.7666C7.50351 10.7821 7.51667 10.8009 7.52541 10.8213C7.53411 10.8416 7.53815 10.8637 7.53811 10.8857V12.0674C7.53811 12.1606 7.46005 12.2365 7.36428 12.2393C4.21497 12.3297 1.64857 14.8691 1.56057 18.0586C1.55797 18.1518 1.48055 18.2275 1.38479 18.2275H0.171898C0.149216 18.2276 0.126437 18.2233 0.105491 18.2148C0.0845105 18.2063 0.0648109 18.1945 0.0488507 18.1787C0.0328861 18.1629 0.0211636 18.1436 0.0127179 18.123C0.0042783 18.1025 -0.000365797 18.0807 2.25475e-05 18.0586C0.0889423 14.0632 3.31982 10.8106 7.36428 10.7197C7.38699 10.7193 7.40959 10.7233 7.43069 10.7314ZM13.5293 12.7422C14.5104 12.7423 15.3057 13.5173 15.3057 14.4727C15.3056 15.428 14.5104 16.202 13.5293 16.2021C12.5481 16.2021 11.752 15.4281 11.752 14.4727C11.752 13.5172 12.5481 12.7422 13.5293 12.7422ZM13.5293 13.7549C13.334 13.7549 13.146 13.8304 13.0078 13.9648C12.8697 14.0994 12.792 14.2824 12.792 14.4727C12.792 14.5668 12.8117 14.6601 12.8487 14.7471C12.8856 14.8339 12.9396 14.913 13.0078 14.9795C13.0762 15.0461 13.1577 15.0997 13.2471 15.1357C13.3364 15.1717 13.4327 15.1894 13.5293 15.1895C13.6258 15.1894 13.7214 15.1716 13.8106 15.1357C13.9 15.0997 13.9814 15.0461 14.0498 14.9795C14.1182 14.9129 14.173 14.834 14.21 14.7471C14.247 14.6601 14.2656 14.5668 14.2656 14.4727C14.2657 14.2824 14.188 14.0994 14.0498 13.9648C13.9118 13.8306 13.7245 13.755 13.5293 13.7549ZM8.751 0C11.3836 0 13.5186 2.07823 13.5186 4.6416C13.5184 7.20486 11.3835 9.28223 8.751 9.28223C6.11871 9.28199 3.98454 7.20472 3.9844 4.6416C3.9844 2.07837 6.11863 0.000236777 8.751 0ZM8.751 1.51855C6.98018 1.51879 5.54494 2.91736 5.54494 4.6416C5.54508 6.36573 6.98026 7.76343 8.751 7.76367C10.5219 7.76367 11.9579 6.36587 11.958 4.6416C11.958 2.91721 10.522 1.51855 8.751 1.51855Z" fill="#B2B8C2"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.7 KiB |
@@ -0,0 +1,3 @@
|
||||
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.20006 0C3.67131 0 0 3.67131 0 8.20006C0 12.7288 3.67131 16.4001 8.20006 16.4001C10.1534 16.4001 11.9472 15.7171 13.3558 14.5769L16.5265 17.7476C16.8636 18.0847 17.4101 18.0847 17.7472 17.7476C18.0843 17.4105 18.0843 16.864 17.7472 16.5269L14.5765 13.3562C15.717 11.9476 16.4001 10.1536 16.4001 8.20006C16.4001 3.67131 12.7288 0 8.20006 0ZM1.72633 8.20006C1.72633 4.62473 4.62473 1.72633 8.20006 1.72633C11.7754 1.72633 14.6738 4.62473 14.6738 8.20006C14.6738 11.7754 11.7754 14.6738 8.20006 14.6738C4.62473 14.6738 1.72633 11.7754 1.72633 8.20006Z" fill="#1D1D1D"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 721 B |
@@ -0,0 +1,4 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M8.25 4.5V19.5H3.75C3.55109 19.5 3.36032 19.421 3.21967 19.2803C3.07902 19.1397 3 18.9489 3 18.75V5.25C3 5.05109 3.07902 4.86032 3.21967 4.71967C3.36032 4.57902 3.55109 4.5 3.75 4.5H8.25Z" fill="#1D1D1D" fill-opacity="0.12"/>
|
||||
<path d="M20.25 3.75H3.75C3.35218 3.75 2.97064 3.90804 2.68934 4.18934C2.40804 4.47064 2.25 4.85218 2.25 5.25V18.75C2.25 19.1478 2.40804 19.5294 2.68934 19.8107C2.97064 20.092 3.35218 20.25 3.75 20.25H20.25C20.6478 20.25 21.0294 20.092 21.3107 19.8107C21.592 19.5294 21.75 19.1478 21.75 18.75V5.25C21.75 4.85218 21.592 4.47064 21.3107 4.18934C21.0294 3.90804 20.6478 3.75 20.25 3.75ZM3.75 14.25H5.25C5.44891 14.25 5.63968 14.171 5.78033 14.0303C5.92098 13.8897 6 13.6989 6 13.5C6 13.3011 5.92098 13.1103 5.78033 12.9697C5.63968 12.829 5.44891 12.75 5.25 12.75H3.75V11.25H5.25C5.44891 11.25 5.63968 11.171 5.78033 11.0303C5.92098 10.8897 6 10.6989 6 10.5C6 10.3011 5.92098 10.1103 5.78033 9.96967C5.63968 9.82902 5.44891 9.75 5.25 9.75H3.75V8.25H5.25C5.44891 8.25 5.63968 8.17098 5.78033 8.03033C5.92098 7.88968 6 7.69891 6 7.5C6 7.30109 5.92098 7.11032 5.78033 6.96967C5.63968 6.82902 5.44891 6.75 5.25 6.75H3.75V5.25H7.5V18.75H3.75V14.25ZM20.25 18.75H9V5.25H20.25V18.75Z" fill="#1D1D1D"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
@@ -0,0 +1,5 @@
|
||||
<svg width="19" height="19" viewBox="0 0 19 19" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M0 9.49992C0 4.25313 4.25313 0 9.49992 0C14.7467 0 18.9998 4.25313 18.9998 9.49992C18.9998 14.7467 14.7467 18.9998 9.49992 18.9998C4.25313 18.9998 0 14.7467 0 9.49992ZM9.49992 1.72726C5.20706 1.72726 1.72726 5.20706 1.72726 9.49992C1.72726 13.7928 5.20706 17.2726 9.49992 17.2726C13.7928 17.2726 17.2726 13.7928 17.2726 9.49992C17.2726 5.20706 13.7928 1.72726 9.49992 1.72726Z" fill="#1D1D1D"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.36254 4.77166C7.92941 4.20479 8.69824 3.88633 9.49992 3.88633C10.3016 3.88633 11.0704 4.20479 11.6373 4.77166C12.2042 5.33852 12.5226 6.10736 12.5226 6.90903C12.5226 7.7107 12.2042 8.47953 11.6373 9.0464C11.0704 9.61327 10.3016 9.93173 9.49992 9.93173C8.69824 9.93173 7.92941 9.61327 7.36254 9.0464C6.79568 8.47953 6.47721 7.7107 6.47721 6.90903C6.47721 6.10736 6.79568 5.33852 7.36254 4.77166ZM9.49992 5.61359C9.15634 5.61359 8.82684 5.75007 8.5839 5.99301C8.34096 6.23595 8.20447 6.56546 8.20447 6.90903C8.20447 7.2526 8.34096 7.5821 8.5839 7.82505C8.82684 8.06799 9.15634 8.20447 9.49992 8.20447C9.84349 8.20447 10.173 8.06799 10.4159 7.82505C10.6589 7.5821 10.7954 7.2526 10.7954 6.90903C10.7954 6.56546 10.6589 6.23595 10.4159 5.99301C10.173 5.75007 9.84349 5.61359 9.49992 5.61359Z" fill="#1D1D1D"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.77244 12.5226C5.94266 12.5226 4.44433 13.9462 4.32555 15.7456C4.29413 16.2215 3.88284 16.5818 3.4069 16.5504C2.93097 16.519 2.57062 16.1077 2.60204 15.6318C2.78034 12.9308 5.02671 10.7954 7.77244 10.7954H11.2269C13.969 10.7954 16.2133 12.925 16.3968 15.621C16.4292 16.0968 16.0697 16.5089 15.5938 16.5412C15.1179 16.5736 14.7059 16.2141 14.6735 15.7383C14.5513 13.9425 13.0544 12.5226 11.2269 12.5226H7.77244Z" fill="#1D1D1D"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.8 KiB |
@@ -0,0 +1,3 @@
|
||||
<svg width="20" height="17" viewBox="0 0 20 17" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M16 14.0267H20V12.0267H16V14.0267ZM14 4.02669V6.02669H20V4.02669H14ZM15 10.0267H20V8.02669H15V10.0267ZM10.23 8.81669C11.0119 8.15498 11.5717 7.26911 11.8337 6.27887C12.0957 5.28864 12.0472 4.24183 11.6949 3.28003C11.3425 2.31824 10.7033 1.48787 9.86358 0.90123C9.0239 0.314594 8.02431 0 7 0C5.97569 0 4.9761 0.314594 4.13642 0.90123C3.29674 1.48787 2.65749 2.31824 2.30514 3.28003C1.95278 4.24183 1.90433 5.28864 2.16632 6.27887C2.42831 7.26911 2.98811 8.15498 3.77 8.81669C2.63355 9.40776 1.68108 10.2993 1.01633 11.3943C0.351585 12.4893 4.42014e-05 13.7457 0 15.0267C0.00137363 15.3616 0.0281168 15.6959 0.08 16.0267H2.08C2.02276 15.6965 1.99598 15.3618 2 15.0267C2 13.7006 2.52678 12.4288 3.46447 11.4912C4.40215 10.5535 5.67392 10.0267 7 10.0267C8.32608 10.0267 9.59785 10.5535 10.5355 11.4912C11.4732 12.4288 12 13.7006 12 15.0267C11.9974 15.3624 11.9639 15.6971 11.9 16.0267H13.9C13.9586 15.6964 13.992 15.3621 14 15.0267C14 13.7457 13.6484 12.4893 12.9837 11.3943C12.3189 10.2993 11.3664 9.40776 10.23 8.81669ZM7 8.02669C6.40666 8.02669 5.82664 7.85074 5.33329 7.5211C4.83994 7.19145 4.45542 6.72292 4.22836 6.17474C4.0013 5.62656 3.94189 5.02336 4.05764 4.44142C4.1734 3.85947 4.45912 3.32492 4.87868 2.90537C5.29824 2.48581 5.83279 2.20009 6.41473 2.08433C6.99667 1.96858 7.59987 2.02799 8.14805 2.25505C8.69623 2.48211 9.16476 2.86663 9.49441 3.35998C9.82405 3.85332 10 4.43334 10 5.02669C10 5.82234 9.68393 6.5854 9.12132 7.14801C8.55871 7.71062 7.79565 8.02669 7 8.02669Z" fill="#B2B8C2"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 249 KiB After Width: | Height: | Size: 1.1 MiB |
@@ -0,0 +1,105 @@
|
||||
@use './variables.scss' as *;
|
||||
@use 'sass:color';
|
||||
|
||||
// Element Plus 主题定制
|
||||
.el-button {
|
||||
border-radius: $border-radius-base;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&--primary {
|
||||
background-color: $primary-color;
|
||||
border-color: $primary-color;
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
background-color: color.adjust($primary-color, $lightness: 10%);
|
||||
border-color: color.adjust($primary-color, $lightness: 10%);
|
||||
}
|
||||
|
||||
&:active {
|
||||
background-color: color.adjust($primary-color, $lightness: -10%);
|
||||
border-color: color.adjust($primary-color, $lightness: -10%);
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
}
|
||||
|
||||
// 输入框样式
|
||||
.el-input {
|
||||
.el-input__wrapper {
|
||||
border-radius: $border-radius-base;
|
||||
|
||||
&:hover {
|
||||
border-color: $primary-color;
|
||||
}
|
||||
|
||||
&.is-focus {
|
||||
border-color: $primary-color;
|
||||
box-shadow: 0 0 0 2px rgba(1, 156, 124, 0.2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 表格样式
|
||||
.el-table {
|
||||
border-radius: $border-radius-base;
|
||||
overflow: hidden;
|
||||
|
||||
.el-table__header {
|
||||
background-color: $background-color-base;
|
||||
}
|
||||
|
||||
.el-table__row:hover > td {
|
||||
background-color: color.adjust($primary-color, $lightness: 45%);
|
||||
}
|
||||
|
||||
th.el-table__cell {
|
||||
background-color: $background-color-base;
|
||||
color: $text-color-primary;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
// 卡片样式
|
||||
.el-card {
|
||||
border-radius: $border-radius-base;
|
||||
box-shadow: $box-shadow-base;
|
||||
|
||||
.el-card__header {
|
||||
font-weight: 500;
|
||||
color: $text-color-primary;
|
||||
}
|
||||
}
|
||||
|
||||
// 菜单样式
|
||||
.el-menu {
|
||||
.el-menu-item {
|
||||
&:hover {
|
||||
background-color: color.adjust($primary-color, $lightness: 45%);
|
||||
color: $primary-color;
|
||||
}
|
||||
|
||||
&.is-active {
|
||||
background-color: color.adjust($primary-color, $lightness: 40%);
|
||||
color: $primary-color;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 对话框样式
|
||||
.el-dialog {
|
||||
border-radius: $border-radius-base;
|
||||
|
||||
.el-dialog__header {
|
||||
padding: $spacing-medium $spacing-large;
|
||||
}
|
||||
|
||||
.el-dialog__body {
|
||||
padding: $spacing-large;
|
||||
}
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
/* 全局样式重置 */
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html, body {
|
||||
height: 100%;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
#app {
|
||||
height: 100%;
|
||||
}
|
||||
@@ -0,0 +1,341 @@
|
||||
@use './variables.scss' as *;
|
||||
|
||||
/* 全局样式重置 */
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html, body {
|
||||
height: 100%;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
font-size: 14px;
|
||||
color: #303133;
|
||||
background-color: #F5F7FA;
|
||||
}
|
||||
|
||||
#app {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* Element Plus 主题色自定义 */
|
||||
:root {
|
||||
--el-color-primary: #019C7C !important;
|
||||
--el-color-primary-light-3: rgba(1, 156, 124, 0.7) !important;
|
||||
--el-color-primary-light-5: rgba(1, 156, 124, 0.5) !important;
|
||||
--el-color-primary-light-7: rgba(1, 156, 124, 0.3) !important;
|
||||
--el-color-primary-light-8: rgba(1, 156, 124, 0.2) !important;
|
||||
--el-color-primary-light-9: rgba(1, 156, 124, 0.1) !important;
|
||||
--el-color-primary-dark-2: #0e8a71 !important;
|
||||
--el-border-radius-base: 4px;
|
||||
--el-font-size-base: 14px;
|
||||
}
|
||||
|
||||
/* 工具类样式 */
|
||||
// 间距工具类
|
||||
.m-0 { margin: 0; }
|
||||
.m-1 { margin: 8px; }
|
||||
.m-2 { margin: 12px; }
|
||||
.m-3 { margin: 16px; }
|
||||
.m-4 { margin: 24px; }
|
||||
.m-5 { margin: 32px; }
|
||||
|
||||
.p-0 { padding: 0; }
|
||||
.p-1 { padding: 8px; }
|
||||
.p-2 { padding: 12px; }
|
||||
.p-3 { padding: 16px; }
|
||||
.p-4 { padding: 24px; }
|
||||
.p-5 { padding: 32px; }
|
||||
|
||||
// 文本工具类
|
||||
.text-left { text-align: left; }
|
||||
.text-center { text-align: center; }
|
||||
.text-right { text-align: right; }
|
||||
|
||||
.text-primary { color: #019C7C; }
|
||||
.text-success { color: #67C23A; }
|
||||
.text-warning { color: #E6A23C; }
|
||||
.text-danger { color: #F56C6C; }
|
||||
.text-info { color: #909399; }
|
||||
|
||||
// 显示工具类
|
||||
.d-none { display: none; }
|
||||
.d-block { display: block; }
|
||||
.d-flex { display: flex; }
|
||||
.d-inline { display: inline; }
|
||||
.d-inline-block { display: inline-block; }
|
||||
|
||||
// Flex 布局工具类
|
||||
.flex-center {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.flex-between {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.flex-start {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.flex-end {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
// 文本省略
|
||||
.ellipsis {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.ellipsis-2 {
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.ellipsis-3 {
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 3;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
// 卡片样式
|
||||
.card {
|
||||
background: #FFFFFF;
|
||||
border: 1px solid #EBEEF5;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
// 滚动条样式
|
||||
.scrollbar {
|
||||
&::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
background: #F5F7FA;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: #c1c1c1;
|
||||
border-radius: 3px;
|
||||
|
||||
&:hover {
|
||||
background: #a8a8a8;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 响应式工具类
|
||||
@media (max-width: 768px) {
|
||||
.hide-mobile { display: none !important; }
|
||||
.show-mobile { display: block !important; }
|
||||
}
|
||||
|
||||
@media (min-width: 769px) {
|
||||
.hide-desktop { display: none !important; }
|
||||
.show-desktop { display: block !important; }
|
||||
}
|
||||
|
||||
// 动画过渡
|
||||
.transition {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
// 悬停阴影
|
||||
.hover-shadow {
|
||||
transition: box-shadow 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
/* 移动端日期选择器弹出层优化 */
|
||||
.mobile-date-picker {
|
||||
@media (max-width: 768px) {
|
||||
/* 限制最大宽度,防止超出屏幕 */
|
||||
max-width: calc(100vw - 32px) !important;
|
||||
/* 确保在屏幕范围内 */
|
||||
left: 16px !important;
|
||||
right: 16px !important;
|
||||
/* 使用较小的字体 */
|
||||
font-size: 13px !important;
|
||||
/* 添加z-index确保在最上层 */
|
||||
z-index: 9999 !important;
|
||||
|
||||
/* 日期面板样式调整 */
|
||||
.el-picker-panel {
|
||||
width: auto !important;
|
||||
max-width: 100% !important;
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
/* 日期范围选择器的两个面板调整为上下布局 */
|
||||
.el-picker-panel__content {
|
||||
display: flex !important;
|
||||
flex-direction: column !important;
|
||||
gap: 8px !important;
|
||||
overflow-x: auto !important;
|
||||
}
|
||||
|
||||
/* 日期表格调整 */
|
||||
.el-date-table {
|
||||
font-size: 12px !important;
|
||||
min-width: unset !important;
|
||||
|
||||
td {
|
||||
padding: 1px !important;
|
||||
min-width: 28px !important;
|
||||
|
||||
span {
|
||||
height: 28px !important;
|
||||
width: 28px !important;
|
||||
line-height: 28px !important;
|
||||
font-size: 11px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 头部月份切换按钮 */
|
||||
.el-date-picker__header {
|
||||
padding: 6px 8px !important;
|
||||
|
||||
button {
|
||||
font-size: 12px !important;
|
||||
padding: 0 4px !important;
|
||||
}
|
||||
|
||||
.el-date-picker__header-label {
|
||||
font-size: 13px !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* 快捷选项调整 */
|
||||
.el-picker-panel__sidebar {
|
||||
width: 100% !important;
|
||||
padding: 8px !important;
|
||||
border-right: none !important;
|
||||
border-bottom: 1px solid #e4e7ed !important;
|
||||
|
||||
.el-picker-panel__shortcut {
|
||||
display: inline-block !important;
|
||||
width: calc(50% - 4px) !important;
|
||||
margin: 2px !important;
|
||||
padding: 6px 8px !important;
|
||||
font-size: 12px !important;
|
||||
text-align: center !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* 底部确认按钮 */
|
||||
.el-picker-panel__footer {
|
||||
padding: 8px !important;
|
||||
text-align: center !important;
|
||||
|
||||
.el-button {
|
||||
font-size: 12px !important;
|
||||
height: 28px !important;
|
||||
padding: 0 12px !important;
|
||||
margin: 0 4px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 移动端日期选择器输入框优化 */
|
||||
@media (max-width: 768px) {
|
||||
.el-date-editor.el-input {
|
||||
/* 确保输入框在小屏幕上合适显示 */
|
||||
font-size: 13px !important;
|
||||
|
||||
.el-input__wrapper {
|
||||
padding: 0 6px !important;
|
||||
}
|
||||
|
||||
.el-input__inner {
|
||||
font-size: 13px !important;
|
||||
height: 28px !important;
|
||||
}
|
||||
|
||||
.el-range-separator {
|
||||
font-size: 12px !important;
|
||||
padding: 0 2px !important;
|
||||
}
|
||||
|
||||
.el-input__suffix {
|
||||
.el-input__suffix-inner {
|
||||
.el-icon {
|
||||
font-size: 14px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 防止移动端弹出日期选择器时页面被撑开 */
|
||||
@media (max-width: 768px) {
|
||||
body:has(.mobile-date-picker) {
|
||||
overflow-x: hidden !important;
|
||||
}
|
||||
|
||||
/* 确保弹出层不会导致水平滚动 */
|
||||
.mobile-date-picker * {
|
||||
max-width: 100% !important;
|
||||
box-sizing: border-box !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Element Plus 日期选择器初始化修复 */
|
||||
.el-picker__popper {
|
||||
/* 防止初始化时意外显示 */
|
||||
&[aria-hidden="true"] {
|
||||
display: none !important;
|
||||
visibility: hidden !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* 防止初始化时弹出层闪现 */
|
||||
.el-picker__popper[data-popper-placement] {
|
||||
transition: opacity 0.2s ease, transform 0.2s ease;
|
||||
}
|
||||
|
||||
/* 初始化时隐藏所有遮罩层 */
|
||||
.el-overlay {
|
||||
&:empty {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* 确保日期选择器组件正确初始化 */
|
||||
.el-date-editor {
|
||||
.el-input__inner {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* 防止初始化时的样式闪动 */
|
||||
&.el-input--small {
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
// 主色调
|
||||
$primary-color: #019C7C; // 主色
|
||||
$success-color: #67C23A; // 成功色
|
||||
$warning-color: #E6A23C; // 警告色
|
||||
$danger-color: #F56C6C; // 危险色
|
||||
$info-color: #909399; // 信息色
|
||||
|
||||
// 中性色
|
||||
$text-color-primary: #303133; // 主要文字
|
||||
$text-color-regular: #606266; // 常规文字
|
||||
$text-color-secondary: #909399; // 次要文字
|
||||
$text-color-placeholder: #C0C4CC; // 占位符文字
|
||||
|
||||
// 边框色
|
||||
$border-color-base: #DCDFE6;
|
||||
$border-color-light: #E4E7ED;
|
||||
$border-color-lighter: #EBEEF5;
|
||||
$border-color-extra-light: #F2F6FC;
|
||||
|
||||
// 背景色
|
||||
$background-color-base: #F5F7FA;
|
||||
$background-color-white: #FFFFFF;
|
||||
|
||||
// 字体大小
|
||||
$font-size-extra-large: 20px;
|
||||
$font-size-large: 18px;
|
||||
$font-size-medium: 16px;
|
||||
$font-size-base: 14px;
|
||||
$font-size-small: 13px;
|
||||
$font-size-extra-small: 12px;
|
||||
|
||||
// 字体粗细
|
||||
$font-weight-primary: 500;
|
||||
$font-weight-secondary: 400;
|
||||
|
||||
// 行高
|
||||
$line-height-primary: 24px;
|
||||
$line-height-secondary: 16px;
|
||||
|
||||
// 间距变量
|
||||
$spacing-extra-large: 32px;
|
||||
$spacing-large: 24px;
|
||||
$spacing-medium: 16px;
|
||||
$spacing-base: 12px;
|
||||
$spacing-small: 8px;
|
||||
$spacing-extra-small: 4px;
|
||||
|
||||
// 圆角
|
||||
$border-radius-base: 4px;
|
||||
$border-radius-small: 2px;
|
||||
$border-radius-round: 20px;
|
||||
$border-radius-circle: 100%;
|
||||
|
||||
// 阴影
|
||||
$box-shadow-base: 0 2px 4px rgba(0, 0, 0, 0.12), 0 0 6px rgba(0, 0, 0, 0.04);
|
||||
$box-shadow-light: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
|
||||
|
||||
// 响应式断点
|
||||
$breakpoint-mobile: 768px;
|
||||
$breakpoint-tablet: 992px;
|
||||
$breakpoint-desktop: 1200px;
|
||||
$breakpoint-wide: 1920px;
|
||||
|
||||
// Z-index 层级
|
||||
$z-index-popper: 2000;
|
||||
$z-index-dialog: 2001;
|
||||
$z-index-drawer: 2002;
|
||||
$z-index-message: 3000;
|
||||
$z-index-notification: 4000;
|
||||
$z-index-tooltip: 2003;
|
||||
@@ -0,0 +1,584 @@
|
||||
<template>
|
||||
<div class="license-trend-chart">
|
||||
<!-- 卡片头部 -->
|
||||
<div class="chart-header">
|
||||
<h3 class="chart-title">授权趋势</h3>
|
||||
|
||||
<!-- 时间选择器 -->
|
||||
<div class="time-selector">
|
||||
<!-- 桌面端:横向布局 -->
|
||||
<template v-if="!isMobile">
|
||||
<!-- 快捷选择按钮 -->
|
||||
<div class="quick-selector">
|
||||
<el-button
|
||||
v-for="option in quickOptions"
|
||||
:key="option.value"
|
||||
:type="selectedQuick === option.value ? 'primary' : 'default'"
|
||||
size="small"
|
||||
@click="handleQuickSelect(option.value)"
|
||||
>
|
||||
{{ option.label }}
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 日期范围选择器 - 条件渲染模式 -->
|
||||
<!-- 显示模式:当有快捷选择时,显示只读的文本输入框 -->
|
||||
<div v-if="selectedQuick" class="date-display-wrapper">
|
||||
<div class="date-display" @click="switchToEditMode">
|
||||
{{ formatDateRange(dateRange) }}
|
||||
<el-icon class="date-icon"><Calendar /></el-icon>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 编辑模式:实际的日期选择器 -->
|
||||
<el-date-picker
|
||||
v-else
|
||||
ref="datePickerRef"
|
||||
v-model="dateRange"
|
||||
type="daterange"
|
||||
range-separator="至"
|
||||
start-placeholder="开始日期"
|
||||
end-placeholder="结束日期"
|
||||
size="small"
|
||||
format="MM-DD"
|
||||
value-format="YYYY-MM-DD"
|
||||
:clearable="false"
|
||||
:editable="false"
|
||||
@change="handleDateChange"
|
||||
style="width: 200px;"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<!-- 移动端:上下布局 -->
|
||||
<template v-else>
|
||||
<!-- 快捷选择按钮 -->
|
||||
<div class="quick-selector mobile-quick">
|
||||
<el-button
|
||||
v-for="option in quickOptions"
|
||||
:key="option.value"
|
||||
:type="selectedQuick === option.value ? 'primary' : 'default'"
|
||||
size="small"
|
||||
@click="handleQuickSelect(option.value)"
|
||||
>
|
||||
{{ option.label }}
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 日期范围选择器 -->
|
||||
<MobileDateRange
|
||||
v-model="dateRange"
|
||||
:show-quick-buttons="false"
|
||||
@change="handleDateChange"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 图表容器 -->
|
||||
<div class="chart-container">
|
||||
<v-chart
|
||||
class="trend-chart"
|
||||
:option="chartOption"
|
||||
:autoresize="true"
|
||||
ref="chartRef"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { use } from 'echarts/core'
|
||||
import { useDevice } from '@/composables/useDevice'
|
||||
import MobileDateRange from '@/components/common/MobileDateRange.vue'
|
||||
import { Calendar } from '@element-plus/icons-vue'
|
||||
import { CanvasRenderer } from 'echarts/renderers'
|
||||
import { LineChart } from 'echarts/charts'
|
||||
import {
|
||||
GridComponent,
|
||||
TooltipComponent,
|
||||
LegendComponent
|
||||
} from 'echarts/components'
|
||||
import VChart from 'vue-echarts'
|
||||
|
||||
// 注册必要的组件
|
||||
use([
|
||||
CanvasRenderer,
|
||||
LineChart,
|
||||
GridComponent,
|
||||
TooltipComponent,
|
||||
LegendComponent
|
||||
])
|
||||
|
||||
// 快捷选择选项
|
||||
const quickOptions = [
|
||||
{ label: '本日', value: 'today' },
|
||||
{ label: '本周', value: 'week' },
|
||||
{ label: '本月', value: 'month' }
|
||||
]
|
||||
|
||||
// 响应式数据
|
||||
const { isMobile } = useDevice()
|
||||
const selectedQuick = ref('week')
|
||||
const dateRange = ref<[string, string]>(['2024-05-13', '2024-05-17']) // 初始值会被handleQuickSelect('week')覆盖
|
||||
const chartRef = ref()
|
||||
const datePickerRef = ref()
|
||||
|
||||
// 模拟授权趋势数据
|
||||
const trendData = ref([
|
||||
{ date: '2024-05-13', value: 8 },
|
||||
{ date: '2024-05-14', value: 12 },
|
||||
{ date: '2024-05-15', value: 15 },
|
||||
{ date: '2024-05-16', value: 10 },
|
||||
{ date: '2024-05-17', value: 18 }
|
||||
])
|
||||
|
||||
// 图表配置
|
||||
const chartOption = computed(() => {
|
||||
const dates = trendData.value.map(item => item.date.substring(5)) // 只显示月-日
|
||||
const values = trendData.value.map(item => item.value)
|
||||
|
||||
return {
|
||||
grid: {
|
||||
left: 60,
|
||||
right: 30,
|
||||
top: 30,
|
||||
bottom: 50,
|
||||
containLabel: false
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: dates,
|
||||
axisLine: {
|
||||
show: false
|
||||
},
|
||||
axisTick: {
|
||||
show: false
|
||||
},
|
||||
axisLabel: {
|
||||
color: '#1D1D1D',
|
||||
fontSize: 12,
|
||||
fontFamily: 'Inter',
|
||||
margin: 12
|
||||
}
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
min: 0,
|
||||
max: 20,
|
||||
interval: 5,
|
||||
axisLine: {
|
||||
show: false
|
||||
},
|
||||
axisTick: {
|
||||
show: false
|
||||
},
|
||||
axisLabel: {
|
||||
color: '#1D1D1D',
|
||||
fontSize: 12,
|
||||
fontFamily: 'Inter',
|
||||
align: 'right'
|
||||
},
|
||||
splitLine: {
|
||||
lineStyle: {
|
||||
color: '#EBEBEB',
|
||||
width: 1
|
||||
}
|
||||
}
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
backgroundColor: '#FFFFFF',
|
||||
borderColor: '#FFFFFF',
|
||||
borderWidth: 0.5,
|
||||
borderRadius: 4,
|
||||
textStyle: {
|
||||
color: '#1D1D1D',
|
||||
fontSize: 12
|
||||
},
|
||||
extraCssText: 'box-shadow: 0px 4px 12px 0px rgba(59, 210, 180, 0.2); backdrop-filter: blur(4px);',
|
||||
formatter: (params: any) => {
|
||||
const data = params[0]
|
||||
return `授权数量: ${data.value}`
|
||||
}
|
||||
},
|
||||
series: [
|
||||
{
|
||||
type: 'line',
|
||||
data: values,
|
||||
smooth: true,
|
||||
lineStyle: {
|
||||
color: '#00C27C',
|
||||
width: 2
|
||||
},
|
||||
itemStyle: {
|
||||
color: '#00C27C',
|
||||
borderColor: '#00C27C',
|
||||
borderWidth: 3
|
||||
},
|
||||
symbol: 'circle',
|
||||
symbolSize: 8,
|
||||
emphasis: {
|
||||
itemStyle: {
|
||||
shadowBlur: 12,
|
||||
shadowColor: 'rgba(59, 210, 180, 1)'
|
||||
}
|
||||
},
|
||||
areaStyle: {
|
||||
color: {
|
||||
type: 'linear',
|
||||
x: 0,
|
||||
y: 0,
|
||||
x2: 0,
|
||||
y2: 1,
|
||||
colorStops: [
|
||||
{
|
||||
offset: 0,
|
||||
color: 'rgba(90, 216, 166, 0.6)'
|
||||
},
|
||||
{
|
||||
offset: 1,
|
||||
color: 'rgba(90, 216, 166, 0.08)'
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
})
|
||||
|
||||
// 快捷选择处理 - 简化版,不处理弹出层冲突
|
||||
const handleQuickSelect = (value: string) => {
|
||||
selectedQuick.value = value
|
||||
|
||||
const today = new Date()
|
||||
let startDate: Date
|
||||
let endDate: Date
|
||||
|
||||
switch (value) {
|
||||
case 'today':
|
||||
// 本日:今天到今天
|
||||
startDate = new Date(today)
|
||||
endDate = new Date(today)
|
||||
break
|
||||
case 'week':
|
||||
// 本周:本周一到本周日
|
||||
const currentDay = today.getDay() // 0=周日, 1=周一, ..., 6=周六
|
||||
const mondayOffset = currentDay === 0 ? 6 : currentDay - 1 // 计算到周一的偏移
|
||||
startDate = new Date(today)
|
||||
startDate.setDate(today.getDate() - mondayOffset)
|
||||
endDate = new Date(startDate)
|
||||
endDate.setDate(startDate.getDate() + 6) // 周日
|
||||
break
|
||||
case 'month':
|
||||
// 本月:当前月1号到当前月末
|
||||
startDate = new Date(today.getFullYear(), today.getMonth(), 1)
|
||||
endDate = new Date(today.getFullYear(), today.getMonth() + 1, 0) // 下个月0号=本月最后一天
|
||||
break
|
||||
default:
|
||||
return
|
||||
}
|
||||
|
||||
// 设置日期值
|
||||
dateRange.value = [
|
||||
startDate.toISOString().split('T')[0],
|
||||
endDate.toISOString().split('T')[0]
|
||||
]
|
||||
|
||||
// 更新图表数据
|
||||
updateChartData()
|
||||
}
|
||||
|
||||
// 日期范围改变处理 - 只有通过日期选择器手动选择时才触发
|
||||
const handleDateChange = (dates: [string, string] | null) => {
|
||||
if (dates) {
|
||||
// 清空快捷选择状态,表示用户手动选择了日期
|
||||
selectedQuick.value = ''
|
||||
dateRange.value = dates
|
||||
updateChartData()
|
||||
}
|
||||
}
|
||||
|
||||
// 切换到编辑模式(显示真正的日期选择器)
|
||||
const switchToEditMode = () => {
|
||||
selectedQuick.value = ''
|
||||
}
|
||||
|
||||
// 格式化日期范围用于显示
|
||||
const formatDateRange = (range: [string, string]) => {
|
||||
if (!range || !range[0] || !range[1]) {
|
||||
return '请选择日期范围'
|
||||
}
|
||||
|
||||
const startDate = new Date(range[0])
|
||||
const endDate = new Date(range[1])
|
||||
|
||||
const formatDate = (date: Date) => {
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0')
|
||||
const day = String(date.getDate()).padStart(2, '0')
|
||||
return `${month}-${day}`
|
||||
}
|
||||
|
||||
return `${formatDate(startDate)} 至 ${formatDate(endDate)}`
|
||||
}
|
||||
|
||||
// 更新图表数据
|
||||
const updateChartData = () => {
|
||||
// 这里可以根据选择的日期范围调用API获取实际数据
|
||||
// 现在使用模拟数据
|
||||
const mockData = generateMockData(dateRange.value[0], dateRange.value[1])
|
||||
trendData.value = mockData
|
||||
}
|
||||
|
||||
// 生成模拟数据
|
||||
const generateMockData = (startDate: string, endDate: string) => {
|
||||
const start = new Date(startDate)
|
||||
const end = new Date(endDate)
|
||||
const data = []
|
||||
|
||||
const currentDate = new Date(start)
|
||||
while (currentDate <= end) {
|
||||
data.push({
|
||||
date: currentDate.toISOString().split('T')[0],
|
||||
value: Math.floor(Math.random() * 15) + 5 // 5-20之间的随机数
|
||||
})
|
||||
currentDate.setDate(currentDate.getDate() + 1)
|
||||
}
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
// 初始化时直接调用快捷选择,设置为本周
|
||||
handleQuickSelect('week')
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.license-trend-chart {
|
||||
background: #FFFFFF;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0px 0px 4px 0px rgba(0, 0, 0, 0.1);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.chart-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 24px 24px 0;
|
||||
|
||||
.chart-title {
|
||||
font-family: 'OPPOSans', sans-serif;
|
||||
font-size: 20px;
|
||||
font-weight: 400;
|
||||
color: #1D1D1D;
|
||||
margin: 0;
|
||||
line-height: 1.3;
|
||||
}
|
||||
}
|
||||
|
||||
.time-selector {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
|
||||
.quick-selector {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
:deep(.el-button--small) {
|
||||
height: 32px;
|
||||
padding: 0 12px;
|
||||
font-size: 14px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
:deep(.el-button--default) {
|
||||
background: #F7F8FA;
|
||||
border-color: #F7F8FA;
|
||||
color: #666;
|
||||
|
||||
&:hover {
|
||||
background: #E9ECEF;
|
||||
border-color: #E9ECEF;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-button--primary) {
|
||||
background: #00C27C;
|
||||
border-color: #00C27C;
|
||||
|
||||
&:hover {
|
||||
background: #019C7C;
|
||||
border-color: #019C7C;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-date-editor) {
|
||||
height: 32px;
|
||||
|
||||
.el-input__wrapper {
|
||||
border-radius: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
// 日期显示组件样式
|
||||
.date-display-wrapper {
|
||||
display: inline-block;
|
||||
|
||||
.date-display {
|
||||
height: 32px;
|
||||
width: 200px;
|
||||
padding: 0 12px;
|
||||
border: 1px solid #dcdfe6;
|
||||
border-radius: 4px;
|
||||
background: #fff;
|
||||
font-size: 14px;
|
||||
color: #606266;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
border-color: #c0c4cc;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
border-color: #019C7C;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.date-icon {
|
||||
color: #c0c4cc;
|
||||
font-size: 14px;
|
||||
transition: color 0.2s ease;
|
||||
}
|
||||
|
||||
&:hover .date-icon {
|
||||
color: #909399;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.chart-container {
|
||||
padding: 26px 24px 24px;
|
||||
|
||||
.trend-chart {
|
||||
width: 100%;
|
||||
height: 246px;
|
||||
}
|
||||
}
|
||||
|
||||
// 响应式设计
|
||||
@media (max-width: 768px) {
|
||||
.chart-header {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 0;
|
||||
padding: 16px 16px 0;
|
||||
}
|
||||
|
||||
.chart-title {
|
||||
margin-bottom: 16px;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.time-selector {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
|
||||
// 移动端快捷按钮样式
|
||||
.mobile-quick {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 6px;
|
||||
|
||||
:deep(.el-button--small) {
|
||||
flex: 1;
|
||||
font-size: 12px;
|
||||
height: 32px;
|
||||
padding: 0 8px;
|
||||
border-radius: 4px;
|
||||
min-width: 0;
|
||||
|
||||
// 确保文字不被截断
|
||||
span {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
|
||||
// 主要按钮样式
|
||||
:deep(.el-button--primary) {
|
||||
background: #019C7C;
|
||||
border-color: #019C7C;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
// 默认按钮样式
|
||||
:deep(.el-button--default) {
|
||||
background: #f7f8fa;
|
||||
border-color: #f7f8fa;
|
||||
color: #666;
|
||||
|
||||
&:hover {
|
||||
background: #e9ecef;
|
||||
border-color: #e9ecef;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.chart-container {
|
||||
padding: 16px;
|
||||
|
||||
.trend-chart {
|
||||
height: 200px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 超小屏幕优化 (手机竖屏)
|
||||
@media (max-width: 480px) {
|
||||
.chart-header {
|
||||
padding: 12px 12px 0;
|
||||
}
|
||||
|
||||
.chart-title {
|
||||
font-size: 16px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.time-selector {
|
||||
gap: 12px;
|
||||
|
||||
.mobile-quick {
|
||||
gap: 4px;
|
||||
|
||||
:deep(.el-button--small) {
|
||||
font-size: 11px;
|
||||
height: 28px;
|
||||
padding: 0 6px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.chart-container {
|
||||
padding: 12px;
|
||||
|
||||
.trend-chart {
|
||||
height: 180px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1 @@
|
||||
export { default as LicenseTrendChart } from './LicenseTrendChart.vue'
|
||||
@@ -0,0 +1,276 @@
|
||||
<template>
|
||||
<div class="mobile-date-range">
|
||||
<div class="date-range-inputs">
|
||||
<div class="date-input-group">
|
||||
<label class="date-label">开始日期</label>
|
||||
<input
|
||||
type="date"
|
||||
v-model="startDateValue"
|
||||
:max="endDateValue || maxDate"
|
||||
:min="minDate"
|
||||
class="native-date-input"
|
||||
@change="handleStartChange"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="date-separator">至</div>
|
||||
|
||||
<div class="date-input-group">
|
||||
<label class="date-label">结束日期</label>
|
||||
<input
|
||||
type="date"
|
||||
v-model="endDateValue"
|
||||
:min="startDateValue || minDate"
|
||||
:max="maxDate"
|
||||
class="native-date-input"
|
||||
@change="handleEndChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 快捷选择按钮 -->
|
||||
<div class="quick-buttons" v-if="showQuickButtons">
|
||||
<button
|
||||
v-for="option in quickOptions"
|
||||
:key="option.value"
|
||||
class="quick-btn"
|
||||
:class="{ active: selectedQuick === option.value }"
|
||||
@click="handleQuickSelect(option.value)"
|
||||
>
|
||||
{{ option.label }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue'
|
||||
|
||||
interface Props {
|
||||
modelValue: [string, string] | null
|
||||
minDate?: string
|
||||
maxDate?: string
|
||||
showQuickButtons?: boolean
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'update:modelValue', value: [string, string] | null): void
|
||||
(e: 'change', value: [string, string] | null): void
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
minDate: '2020-01-01',
|
||||
maxDate: '2030-12-31',
|
||||
showQuickButtons: true
|
||||
})
|
||||
|
||||
const emit = defineEmits<Emits>()
|
||||
|
||||
const startDateValue = ref('')
|
||||
const endDateValue = ref('')
|
||||
const selectedQuick = ref('')
|
||||
|
||||
// 快捷选择选项
|
||||
const quickOptions = [
|
||||
{ label: '本日', value: 'today' },
|
||||
{ label: '本周', value: 'week' },
|
||||
{ label: '本月', value: 'month' }
|
||||
]
|
||||
|
||||
// 格式化日期为 YYYY-MM-DD
|
||||
const formatDate = (date: Date): string => {
|
||||
return date.toISOString().split('T')[0]
|
||||
}
|
||||
|
||||
// 处理开始日期变化 - 手动输入时清空快捷选择
|
||||
const handleStartChange = () => {
|
||||
// 清空快捷选择状态,表示用户手动选择了日期
|
||||
selectedQuick.value = ''
|
||||
emitChange()
|
||||
}
|
||||
|
||||
// 处理结束日期变化 - 手动输入时清空快捷选择
|
||||
const handleEndChange = () => {
|
||||
// 清空快捷选择状态,表示用户手动选择了日期
|
||||
selectedQuick.value = ''
|
||||
emitChange()
|
||||
}
|
||||
|
||||
// 发射变化事件
|
||||
const emitChange = () => {
|
||||
if (startDateValue.value && endDateValue.value) {
|
||||
const result: [string, string] = [startDateValue.value, endDateValue.value]
|
||||
emit('update:modelValue', result)
|
||||
emit('change', result)
|
||||
} else {
|
||||
emit('update:modelValue', null)
|
||||
emit('change', null)
|
||||
}
|
||||
}
|
||||
|
||||
// 快捷选择处理
|
||||
const handleQuickSelect = (value: string) => {
|
||||
selectedQuick.value = value
|
||||
|
||||
const today = new Date()
|
||||
let startDate: Date
|
||||
let endDate: Date
|
||||
|
||||
switch (value) {
|
||||
case 'today':
|
||||
// 本日:今天到今天
|
||||
startDate = new Date(today)
|
||||
endDate = new Date(today)
|
||||
break
|
||||
case 'week':
|
||||
// 本周:本周一到本周日
|
||||
const currentDay = today.getDay() // 0=周日, 1=周一, ..., 6=周六
|
||||
const mondayOffset = currentDay === 0 ? 6 : currentDay - 1 // 计算到周一的偏移
|
||||
startDate = new Date(today)
|
||||
startDate.setDate(today.getDate() - mondayOffset)
|
||||
endDate = new Date(startDate)
|
||||
endDate.setDate(startDate.getDate() + 6) // 周日
|
||||
break
|
||||
case 'month':
|
||||
// 本月:当前月1号到当前月末
|
||||
startDate = new Date(today.getFullYear(), today.getMonth(), 1)
|
||||
endDate = new Date(today.getFullYear(), today.getMonth() + 1, 0) // 下个月0号=本月最后一天
|
||||
break
|
||||
default:
|
||||
return
|
||||
}
|
||||
|
||||
startDateValue.value = formatDate(startDate)
|
||||
endDateValue.value = formatDate(endDate)
|
||||
|
||||
emitChange()
|
||||
}
|
||||
|
||||
// 监听外部值变化
|
||||
watch(() => props.modelValue, (newVal) => {
|
||||
if (newVal && newVal[0] && newVal[1]) {
|
||||
startDateValue.value = newVal[0]
|
||||
endDateValue.value = newVal[1]
|
||||
} else {
|
||||
startDateValue.value = ''
|
||||
endDateValue.value = ''
|
||||
}
|
||||
}, { immediate: true })
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.mobile-date-range {
|
||||
width: 100%;
|
||||
padding: 16px;
|
||||
background: #f7f8fa;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #e4e7ed;
|
||||
}
|
||||
|
||||
.date-range-inputs {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.date-input-group {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.date-label {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.native-date-input {
|
||||
width: 100%;
|
||||
height: 40px;
|
||||
padding: 8px 12px;
|
||||
border: 1px solid #dcdfe6;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
background: #fff;
|
||||
color: #606266;
|
||||
transition: border-color 0.2s ease;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
border-color: #019C7C;
|
||||
box-shadow: 0 0 0 2px rgba(1, 156, 124, 0.1);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
border-color: #c0c4cc;
|
||||
}
|
||||
}
|
||||
|
||||
.date-separator {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
margin-top: 20px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.quick-buttons {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.quick-btn {
|
||||
padding: 6px 12px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 20px;
|
||||
background: #fff;
|
||||
color: #666;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
border-color: #019C7C;
|
||||
color: #019C7C;
|
||||
}
|
||||
|
||||
&.active {
|
||||
background: #019C7C;
|
||||
border-color: #019C7C;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
}
|
||||
|
||||
/* 移动端优化 */
|
||||
@media (max-width: 480px) {
|
||||
.mobile-date-range {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.date-range-inputs {
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.date-separator {
|
||||
align-self: center;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.quick-buttons {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.quick-btn {
|
||||
font-size: 11px;
|
||||
padding: 5px 10px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,83 @@
|
||||
<!--
|
||||
* @Author: 13895237362 2205451508@qq.com
|
||||
* @Date: 2025-08-12 00:00:00
|
||||
* @LastEditors: 13895237362 2205451508@qq.com
|
||||
* @LastEditTime: 2025-08-12 00:00:00
|
||||
* @FilePath: /frontend/src/components/common/icons/NavIcon.vue
|
||||
* @Description: 导航栏图标组件
|
||||
-->
|
||||
<template>
|
||||
<div class="nav-icon" :class="iconClass">
|
||||
<component :is="iconComponent" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import SidebarToggleIcon from './svg/SidebarToggleIcon.vue'
|
||||
import SearchIcon from './svg/SearchIcon.vue'
|
||||
import NotificationIcon from './svg/NotificationIcon.vue'
|
||||
import LanguageIcon from './svg/LanguageIcon.vue'
|
||||
import DarkModeIcon from './svg/DarkModeIcon.vue'
|
||||
import UserIcon from './svg/UserIcon.vue'
|
||||
|
||||
interface Props {
|
||||
name: string
|
||||
size?: 'small' | 'medium' | 'large'
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
size: 'medium'
|
||||
})
|
||||
|
||||
const iconMap = {
|
||||
'sidebar-toggle': SidebarToggleIcon,
|
||||
'search': SearchIcon,
|
||||
'notification': NotificationIcon,
|
||||
'language': LanguageIcon,
|
||||
'dark-mode': DarkModeIcon,
|
||||
'user': UserIcon
|
||||
}
|
||||
|
||||
const iconComponent = computed(() => {
|
||||
return iconMap[props.name as keyof typeof iconMap] || SearchIcon
|
||||
})
|
||||
|
||||
const iconClass = computed(() => ({
|
||||
[`nav-icon--${props.size}`]: true
|
||||
}))
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.nav-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.nav-icon--small {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.nav-icon--medium {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.nav-icon--large {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
.nav-icon :deep(svg) {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.nav-icon :deep(path) {
|
||||
fill: #1D1D1D;
|
||||
transition: fill 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,72 @@
|
||||
<!--
|
||||
* @Author: 13895237362 2205451508@qq.com
|
||||
* @Date: 2025-08-12 00:00:00
|
||||
* @LastEditors: 13895237362 2205451508@qq.com
|
||||
* @LastEditTime: 2025-08-12 00:00:00
|
||||
* @FilePath: /frontend/src/components/common/icons/SidebarIcon.vue
|
||||
* @Description: 侧边栏图标组件
|
||||
-->
|
||||
<template>
|
||||
<div class="sidebar-icon" :class="iconClass">
|
||||
<component :is="iconComponent" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import DashboardIcon from './svg/DashboardIcon.vue'
|
||||
import CustomersIcon from './svg/CustomersIcon.vue'
|
||||
import LicensesIcon from './svg/LicensesIcon.vue'
|
||||
import RolesIcon from './svg/RolesIcon.vue'
|
||||
import UsersIcon from './svg/UsersIcon.vue'
|
||||
|
||||
interface Props {
|
||||
name: string
|
||||
active?: boolean
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
active: false
|
||||
})
|
||||
|
||||
const iconMap = {
|
||||
'dashboard': DashboardIcon,
|
||||
'customers': CustomersIcon,
|
||||
'licenses': LicensesIcon,
|
||||
'roles': RolesIcon,
|
||||
'users': UsersIcon
|
||||
}
|
||||
|
||||
const iconComponent = computed(() => {
|
||||
return iconMap[props.name as keyof typeof iconMap] || DashboardIcon
|
||||
})
|
||||
|
||||
const iconClass = computed(() => ({
|
||||
'sidebar-icon--active': props.active
|
||||
}))
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.sidebar-icon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.sidebar-icon :deep(svg) {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.sidebar-icon :deep(path) {
|
||||
fill: #B2B8C2;
|
||||
transition: fill 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.sidebar-icon--active :deep(path) {
|
||||
fill: #019C7C;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,5 @@
|
||||
<template>
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M10.0518 11.6455C11.567 11.6455 13.0337 11.9493 14.4092 12.5527C15.7381 13.134 16.9208 13.9635 17.9258 15.0195L18.3809 15.499L17.9834 16.0244C17.0716 17.2313 15.8779 18.2315 14.5312 18.917C13.1203 19.6358 11.5961 20 10.001 20C8.41942 20 6.90875 19.6408 5.50684 18.9355C4.17128 18.2633 2.98142 17.278 2.06738 16.0889L1.66602 15.5654L2.11621 15.084C3.12342 14.0102 4.31528 13.1627 5.65527 12.5703C7.04169 11.9559 8.52117 11.6456 10.0518 11.6455ZM10.0518 13.1982C7.67586 13.1983 5.4571 14.07 3.7334 15.665C5.33514 17.442 7.58283 18.4472 10.001 18.4473C12.4458 18.4473 14.7107 17.4216 16.3125 15.6113C14.5976 14.0497 12.3988 13.1982 10.0518 13.1982ZM10.001 0C11.3498 0 12.6591 0.263803 13.8926 0.785156C15.0839 1.28876 16.1528 2.01026 17.0713 2.92871C17.9897 3.84716 18.7112 4.91611 19.2148 6.10742C19.7362 7.34092 20 8.65016 20 9.99902C20 11.0905 19.8246 12.1665 19.4785 13.1914L18.0078 12.6963C18.2984 11.8312 18.4473 10.9238 18.4473 10.001C18.4472 5.3444 14.6556 1.55574 9.99902 1.55566C5.34238 1.55566 1.55281 5.34435 1.55273 10.001C1.55273 10.9482 1.70857 11.8776 2.01465 12.7627L0.547852 13.2715C0.184014 12.2221 0 11.1213 0 10.001C1.71756e-05 8.64994 0.26382 7.34088 0.785156 6.10742C1.28876 4.91611 2.01026 3.84716 2.92871 2.92871C3.84716 2.01026 4.91611 1.28876 6.10742 0.785156C7.34088 0.26382 8.65216 1.73133e-05 10.001 0ZM10.0518 4.89648C11.7578 4.89648 13.1475 6.28517 13.1475 7.99121C13.1474 9.69723 11.7578 11.0859 10.0518 11.0859C8.34591 11.0857 6.95707 9.6971 6.95703 7.99121C6.95703 6.28529 8.34589 4.89669 10.0518 4.89648ZM10.0518 6.44922C9.20224 6.44942 8.51074 7.14164 8.51074 7.99121C8.51078 8.84075 9.20226 9.533 10.0518 9.5332C10.9014 9.5332 11.5937 8.84087 11.5938 7.99121C11.5938 7.14152 10.9015 6.44922 10.0518 6.44922Z"/>
|
||||
</svg>
|
||||
</template>
|
||||
@@ -0,0 +1,5 @@
|
||||
<template>
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M6.52954 1.85626C3.75062 2.51889 1.68419 5.01808 1.68419 7.99992C1.68419 11.488 4.51184 14.3156 7.99992 14.3156C10.9818 14.3156 13.4809 12.2492 14.1436 9.4703C13.1961 10.2948 11.958 10.7941 10.6028 10.7941C7.62202 10.7941 5.2057 8.37781 5.2057 5.39708C5.2057 4.04179 5.70503 2.80374 6.52954 1.85626ZM0 7.99992C0 3.58168 3.58168 0 7.99992 0C8.5542 0 9.09619 0.0564954 9.62011 0.164329C10.0035 0.24324 10.282 0.575925 10.2922 0.967222C10.3023 1.35852 10.0416 1.70524 9.66279 1.80399C8.06687 2.22008 6.8899 3.67218 6.8899 5.39708C6.8899 7.44766 8.55218 9.10994 10.6028 9.10994C12.3277 9.10994 13.7798 7.93297 14.1958 6.33706C14.2946 5.95829 14.6413 5.6975 15.0326 5.70768C15.4239 5.71786 15.7566 5.99632 15.8355 6.3797C15.9433 6.90363 15.9998 7.44564 15.9998 7.99992C15.9998 12.4182 12.4182 15.9998 7.99992 15.9998C3.58168 15.9998 0 12.4182 0 7.99992Z"/>
|
||||
</svg>
|
||||
</template>
|
||||
@@ -0,0 +1,5 @@
|
||||
<template>
|
||||
<svg width="19" height="20" viewBox="0 0 19 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M10.7397 0.30622L17.8809 4.06812C18.2187 4.24611 18.5012 4.51164 18.6984 4.83633C18.8955 5.16101 18.9997 5.53264 19 5.91145V14.103C19 14.8779 18.5678 15.5889 17.8771 15.9501L10.735 19.696C9.96265 20.1013 9.03735 20.1013 8.265 19.696L1.1229 15.952C0.783836 15.7742 0.50013 15.5083 0.302282 15.1828C0.104435 14.8574 -7.08312e-05 14.4847 3.60183e-08 14.1049V5.91333C3.60183e-08 5.14026 0.43035 4.4302 1.1191 4.06718L8.26025 0.30622C8.64192 0.105155 9.06765 0 9.5 0C9.93235 0 10.3581 0.105155 10.7397 0.30622ZM8.6507 2.00002L8.4911 2.07807L2.7816 5.15343C2.53785 5.2849 2.33059 5.47382 2.17814 5.70347C2.0257 5.93312 1.93278 6.19643 1.9076 6.47009L1.9 6.62245V13.392C1.9 13.9582 2.1888 14.4829 2.66285 14.7895L2.78445 14.862L8.49395 17.9241C8.77644 18.0757 9.09015 18.1615 9.41119 18.1748C9.73222 18.1882 10.0521 18.1288 10.3464 18.0013L10.5051 17.9241L16.2155 14.862C16.719 14.592 17.0468 14.0908 17.0943 13.5321L17.1 13.391V6.62245C17.1 6.06099 16.815 5.5409 16.3495 5.23243L16.2184 5.15343L10.5089 2.07807C10.2258 1.92545 9.91106 1.83906 9.58895 1.82553C9.26684 1.812 8.94589 1.87168 8.6507 2.00002ZM13.4862 6.78985C13.5943 6.72713 13.7138 6.68618 13.838 6.66938C13.9621 6.65258 14.0884 6.66025 14.2095 6.69195C14.3306 6.72365 14.4442 6.77876 14.5437 6.8541C14.6432 6.92944 14.7267 7.02353 14.7894 7.13094C14.852 7.23835 14.8926 7.35697 14.9087 7.47996C14.9249 7.60296 14.9163 7.7279 14.8835 7.8476C14.8506 7.9673 14.7942 8.07938 14.7175 8.17741C14.6407 8.27543 14.5451 8.35746 14.4362 8.41876L10.45 10.6966V14.7011C10.45 14.9506 10.3499 15.1898 10.1718 15.3661C9.99359 15.5425 9.75196 15.6416 9.5 15.6416C9.24804 15.6416 9.00641 15.5425 8.82825 15.3661C8.65009 15.1898 8.55 14.9506 8.55 14.7011V10.6975L4.5638 8.41876C4.34738 8.29316 4.18994 8.08791 4.12581 7.84781C4.06169 7.60771 4.09611 7.35225 4.22154 7.13718C4.34697 6.9221 4.55325 6.76487 4.79535 6.69981C5.03745 6.63474 5.29572 6.66711 5.5138 6.78985L9.4848 9.05828H9.5133L13.4862 6.78985Z"/>
|
||||
</svg>
|
||||
</template>
|
||||
@@ -0,0 +1,5 @@
|
||||
<template>
|
||||
<svg width="19" height="19" viewBox="0 0 19 19" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M9.49992 18.9998C4.25311 18.9998 0 14.7467 0 9.49992C0 4.25311 4.25311 0 9.49992 0C14.7467 0 18.9998 4.25311 18.9998 9.49992C18.9998 14.7467 14.7467 18.9998 9.49992 18.9998ZM7.37975 16.9504C6.4425 14.9623 5.99966 12.5698 5.88104 10.3751H1.72726C1.91211 11.837 2.62962 13.4055 3.58213 14.5298C4.53464 15.6542 5.96807 16.5277 7.37975 16.9504ZM7.62843 10.3751C7.77188 12.6921 8.43402 14.9434 9.49992 16.8642C10.5946 14.8926 11.2341 12.6261 11.3714 10.3751H7.62843ZM17.2489 10.3751H13.0961C12.9775 12.5698 12.5607 14.9623 11.6235 16.9504C13.0351 16.5277 14.4709 15.6542 15.4234 14.5298C16.3759 13.4055 17.064 11.837 17.2489 10.3751ZM1.72631 8.61799H5.88104C5.99966 6.4233 6.44345 4.04156 7.3807 2.05352C5.96902 2.47615 4.53464 3.34291 3.58213 4.46726C2.62962 5.59162 1.91116 7.15605 1.72631 8.61799ZM7.62843 8.61799H11.3695C11.2325 6.36708 10.5943 4.10732 9.49992 2.13558C8.40522 4.10724 7.76481 6.367 7.62748 8.61799H7.62843ZM11.6235 2.05352C12.5607 4.04156 12.9765 6.4233 13.0952 8.61799H17.2479C17.0631 7.15605 16.3759 5.58593 15.4234 4.46157C14.4709 3.33722 13.0351 2.47615 11.6235 2.05352Z"/>
|
||||
</svg>
|
||||
</template>
|
||||
@@ -0,0 +1,5 @@
|
||||
<template>
|
||||
<svg width="20" height="17" viewBox="0 0 20 17" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M9.37266 9.52207H3.75014C3.40733 9.52207 3.12473 9.83257 3.12473 10.2102V11.567H3.75014C4.4381 11.567 5.00097 12.1735 5.00097 12.9247V15.6407C5.00097 16.3944 4.4381 16.9976 3.75014 16.9976H1.25082C0.562097 17 0 16.3944 0 15.6407V12.9263C0 12.1727 0.562097 11.5694 1.25082 11.5694H1.87623V10.2126C1.87623 9.083 2.7186 8.16686 3.75014 8.16686H9.37498V6.81003H7.50029C6.46952 6.81003 5.62638 5.89469 5.62638 4.76508V2.04495C5.62638 0.916144 6.46952 0 7.50029 0H12.4997C13.5305 0 14.3736 0.916144 14.3736 2.04576V4.76104C14.3736 5.89065 13.5305 6.80679 12.4997 6.80679H10.6258V8.16362H16.2499C17.2814 8.16362 18.1238 9.07896 18.1238 10.2086V11.5654H18.7492C19.4379 11.5654 20 12.171 20 12.9222V15.6391C20 16.3919 19.4379 16.996 18.7492 16.996H16.2475C15.5604 16.996 14.9967 16.3895 14.9967 15.6383V12.9263C14.9967 12.1727 15.5604 11.5694 16.2475 11.5694H16.8729V10.2126C16.8729 9.83662 16.5927 9.5245 16.2475 9.5245H10.6235V11.5694H11.2489C11.9368 11.5694 12.4997 12.1751 12.4997 12.9263V15.6432C12.4997 16.396 11.9368 17 11.2489 17H8.74957C8.06084 17 7.49874 16.3936 7.49874 15.6424V12.9263C7.49874 12.1727 8.06084 11.5694 8.74957 11.5694H9.37498V9.52288H9.37266V9.52207ZM7.49874 1.36006C7.15516 1.36006 6.87334 1.67057 6.87334 2.04737V4.76427C6.87334 5.14027 7.15361 5.45158 7.49874 5.45158H12.4974C12.841 5.45158 13.1228 5.14189 13.1228 4.76427V2.04495C13.1228 1.66976 12.8425 1.35764 12.4974 1.35764H7.49874V1.35926V1.36006ZM1.25005 12.9263V15.6432H3.74937V12.9263H1.25082H1.25005ZM8.74879 12.9263V15.6432H11.2481V12.9263H8.74879ZM16.2475 12.9263V15.6432H18.7469V12.9263H16.2468H16.2475Z"/>
|
||||
</svg>
|
||||
</template>
|
||||
@@ -0,0 +1,5 @@
|
||||
<template>
|
||||
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M15.919 6.92282V13.6726H17.127C17.605 13.6726 17.9924 14.06 17.9924 14.5379C17.9924 15.0158 17.605 15.4033 17.127 15.4033H11.9944C11.7847 16.8714 10.5222 18 8.9962 18C7.47015 18 6.2077 16.8714 5.99804 15.4033H0.865352C0.387431 15.4033 0 15.0158 0 14.5379C0 14.06 0.387431 13.6726 0.865352 13.6726H2.07339V6.92282C2.07339 3.09945 5.17283 0 8.99621 0C12.8196 0 15.919 3.09945 15.919 6.92282ZM3.80409 6.92282C3.80409 4.05529 6.12868 1.7307 8.99621 1.7307C11.8637 1.7307 14.1883 4.05529 14.1883 6.92282V13.6726H3.80409V6.92282ZM8.9962 16.2693C8.43104 16.2693 7.95023 15.9081 7.77203 15.4039H10.2204C10.0422 15.9081 9.56136 16.2693 8.9962 16.2693Z"/>
|
||||
</svg>
|
||||
</template>
|
||||
@@ -0,0 +1,5 @@
|
||||
<template>
|
||||
<svg width="19" height="20" viewBox="0 0 19 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12.2364 9.00488C13.0863 8.8125 13.9705 8.81285 14.8203 9.00586C14.9005 9.02409 14.9536 9.09659 14.9561 9.17676C14.9641 9.43807 15.0352 9.694 15.1631 9.92383C15.3138 10.1945 15.5388 10.4191 15.8125 10.5732C16.0654 10.7155 16.3516 10.7927 16.6436 10.7969C16.9354 10.801 17.2236 10.7325 17.4805 10.5977C17.5532 10.5595 17.6449 10.5681 17.7012 10.627C17.9977 10.9369 18.2568 11.2793 18.4737 11.6465C18.6915 12.0133 18.8654 12.4032 18.9922 12.8086C19.0162 12.8853 18.9783 12.9666 18.9082 13.0088C18.6802 13.1462 18.4887 13.334 18.3487 13.5566C18.1116 13.9335 18.0378 14.3865 18.1426 14.8164C18.2474 15.2463 18.5226 15.6186 18.9082 15.8516C18.9778 15.8938 19.0149 15.9743 18.9912 16.0508C18.738 16.8649 18.2971 17.6123 17.7012 18.2344C17.6449 18.2932 17.5531 18.3018 17.4805 18.2637C17.2235 18.1289 16.9355 18.0602 16.6436 18.0645C16.3516 18.0687 16.0653 18.1457 15.8125 18.2881C15.5388 18.4423 15.3137 18.6677 15.1631 18.9385C15.0354 19.1683 14.965 19.4244 14.9571 19.6855C14.9545 19.7656 14.9013 19.8383 14.8213 19.8564C13.9713 20.0488 13.0871 20.0475 12.2373 19.8545C12.1573 19.8363 12.1042 19.7646 12.1016 19.6846C12.0936 19.4232 12.0226 19.1664 11.8946 18.9365C11.7438 18.6659 11.5189 18.4412 11.2451 18.2871C10.9923 18.1449 10.706 18.0677 10.4141 18.0635C10.1221 18.0593 9.83417 18.1288 9.57717 18.2637C9.50437 18.3019 9.4128 18.2923 9.35647 18.2334C9.06018 17.9235 8.80091 17.5819 8.58401 17.2148C8.36633 16.8477 8.19245 16.4573 8.06545 16.0518C8.0414 15.975 8.07934 15.8938 8.14944 15.8516C8.37736 15.7142 8.56897 15.5263 8.70901 15.3037C8.94583 14.927 9.0197 14.4747 8.91506 14.0449C8.81027 13.615 8.53507 13.2427 8.14944 13.0098C8.07948 12.9676 8.04252 12.8862 8.06643 12.8096C8.31962 11.9956 8.76071 11.2489 9.35647 10.627C9.4128 10.5681 9.50446 10.5595 9.57717 10.5977C9.83414 10.7324 10.1223 10.8011 10.4141 10.7969C10.7061 10.7926 10.9923 10.7156 11.2451 10.5732C11.519 10.419 11.7439 10.1937 11.8946 9.92285C12.0224 9.69303 12.0927 9.43703 12.1006 9.17578C12.103 9.09561 12.1562 9.02312 12.2364 9.00488ZM13.9268 10.2295C13.6625 10.2046 13.3962 10.2049 13.1319 10.2295C12.9285 10.846 12.5028 11.3664 11.9375 11.6904C11.3752 12.0148 10.7075 12.1213 10.0684 11.9883C9.91487 12.2037 9.78194 12.4325 9.6719 12.6719C10.107 13.1552 10.3481 13.782 10.3477 14.4307C10.3476 15.095 10.0976 15.7155 9.6719 16.1885C9.78279 16.4279 9.91664 16.6572 10.0703 16.873C10.709 16.7403 11.3756 16.846 11.9375 17.1699C12.5025 17.4939 12.9277 18.0145 13.1309 18.6309C13.3954 18.6556 13.6613 18.6566 13.9258 18.6318C14.1291 18.0153 14.5548 17.4949 15.1201 17.1709C15.6824 16.8465 16.3492 16.7402 16.9883 16.873C17.1422 16.6574 17.2758 16.4282 17.3858 16.1885C16.9507 15.7053 16.7096 15.0793 16.71 14.4307C16.71 13.7663 16.96 13.1449 17.3858 12.6719C17.2749 12.4325 17.1409 12.2041 16.9873 11.9883C16.3487 12.1211 15.6821 12.0144 15.1201 11.6904C14.555 11.3664 14.1299 10.8459 13.9268 10.2295ZM7.43069 10.7314C7.45168 10.7396 7.4712 10.7512 7.48733 10.7666C7.50351 10.7821 7.51667 10.8009 7.52541 10.8213C7.53411 10.8416 7.53815 10.8637 7.53811 10.8857V12.0674C7.53811 12.1606 7.46005 12.2365 7.36428 12.2393C4.21497 12.3297 1.64857 14.8691 1.56057 18.0586C1.55797 18.1518 1.48055 18.2275 1.38479 18.2275H0.171898C0.149216 18.2276 0.126437 18.2233 0.105491 18.2148C0.0845105 18.2063 0.0648109 18.1945 0.0488507 18.1787C0.0328861 18.1629 0.0211636 18.1436 0.0127179 18.123C0.0042783 18.1025 -0.000365797 18.0807 2.25475e-05 18.0586C0.0889423 14.0632 3.31982 10.8106 7.36428 10.7197C7.38699 10.7193 7.40959 10.7233 7.43069 10.7314ZM13.5293 12.7422C14.5104 12.7423 15.3057 13.5173 15.3057 14.4727C15.3056 15.428 14.5104 16.202 13.5293 16.2021C12.5481 16.2021 11.752 15.4281 11.752 14.4727C11.752 13.5172 12.5481 12.7422 13.5293 12.7422ZM13.5293 13.7549C13.334 13.7549 13.146 13.8304 13.0078 13.9648C12.8697 14.0994 12.792 14.2824 12.792 14.4727C12.792 14.5668 12.8117 14.6601 12.8487 14.7471C12.8856 14.8339 12.9396 14.913 13.0078 14.9795C13.0762 15.0461 13.1577 15.0997 13.2471 15.1357C13.3364 15.1717 13.4327 15.1894 13.5293 15.1895C13.6258 15.1894 13.7214 15.1716 13.8106 15.1357C13.9 15.0997 13.9814 15.0461 14.0498 14.9795C14.1182 14.9129 14.173 14.834 14.21 14.7471C14.247 14.6601 14.2656 14.5668 14.2656 14.4727C14.2657 14.2824 14.188 14.0994 14.0498 13.9648C13.9118 13.8306 13.7245 13.755 13.5293 13.7549ZM8.751 0C11.3836 0 13.5186 2.07823 13.5186 4.6416C13.5184 7.20486 11.3835 9.28223 8.751 9.28223C6.11871 9.28199 3.98454 7.20472 3.9844 4.6416C3.9844 2.07837 6.11863 0.000236777 8.751 0ZM8.751 1.51855C6.98018 1.51879 5.54494 2.91736 5.54494 4.6416C5.54508 6.36573 6.98026 7.76343 8.751 7.76367C10.5219 7.76367 11.9579 6.36587 11.958 4.6416C11.958 2.91721 10.522 1.51855 8.751 1.51855Z"/>
|
||||
</svg>
|
||||
</template>
|
||||
@@ -0,0 +1,5 @@
|
||||
<template>
|
||||
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.20006 0C3.67131 0 0 3.67131 0 8.20006C0 12.7288 3.67131 16.4001 8.20006 16.4001C10.1534 16.4001 11.9472 15.7171 13.3558 14.5769L16.5265 17.7476C16.8636 18.0847 17.4101 18.0847 17.7472 17.7476C18.0843 17.4105 18.0843 16.864 17.7472 16.5269L14.5765 13.3562C15.717 11.9476 16.4001 10.1536 16.4001 8.20006C16.4001 3.67131 12.7288 0 8.20006 0ZM1.72633 8.20006C1.72633 4.62473 4.62473 1.72633 8.20006 1.72633C11.7754 1.72633 14.6738 4.62473 14.6738 8.20006C14.6738 11.7754 11.7754 14.6738 8.20006 14.6738C4.62473 14.6738 1.72633 11.7754 1.72633 8.20006Z"/>
|
||||
</svg>
|
||||
</template>
|
||||