chore: 重组项目文件结构,文档和部署文件归档
文件结构优化: - docs/ 归档所有项目文档(PROJECT.md, PROJECT_ISSUES.md 等) - deploy/ 归档所有部署相关文件(docker-compose.prod.yml, nginx 配置等) - scripts/ 归档脚本文件(deploy.sh, rollback.sh) - tests/ 归档测试文件(test_data.php) 更新引用路径: - CLAUDE.md 更新文档路径引用 - 部署文档更新配置文件路径引用 清理文件: - 移除非必要的测试文件(TestController, 测试脚本等) - 移除重复/错误的目录 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -68,8 +68,9 @@ Lang::setLocale('en');
|
||||
├── docker-compose.yml # Docker 编排
|
||||
├── nginx/ # OpenResty 配置
|
||||
├── frpc/ # frpc 配置
|
||||
├── PROJECT.md # 项目详细文档
|
||||
└── IMPLEMENTATION_PLAN.md # 实现计划
|
||||
├── docs/ # 项目文档
|
||||
│ ├── PROJECT.md # 项目详细文档
|
||||
│ └── IMPLEMENTATION_PLAN.md # 实现计划
|
||||
```
|
||||
|
||||
## 环境配置
|
||||
@@ -269,11 +270,11 @@ composer require symfony/var-dumper:^7.0
|
||||
## 快速参考
|
||||
|
||||
### 查看问题汇总
|
||||
所有历史问题已整理在项目根目录的 `PROJECT_ISSUES.md` 文件中。
|
||||
所有历史问题已整理在 `docs/PROJECT_ISSUES.md` 文件中。
|
||||
|
||||
### 让 CLAUDE 加载问题文档
|
||||
```
|
||||
请参考 /www/x-server-mobile/PROJECT_ISSUES.md 中的问题汇总
|
||||
请参考 docs/PROJECT_ISSUES.md 中的问题汇总
|
||||
```
|
||||
|
||||
### 调试后端
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
# X-Server Mobile 生产环境变量模板
|
||||
# 使用方法: cp .env.production.template .env.production
|
||||
# 然后填写实际值,不要将 .env.production 提交到代码仓库
|
||||
|
||||
# ============================================
|
||||
# 数据库配置
|
||||
# ============================================
|
||||
DB_HOST=1Panel-mysql-xxxx
|
||||
DB_PORT=3306
|
||||
DB_DATABASE=xserver
|
||||
DB_USERNAME=xserver
|
||||
DB_PASSWORD=your_strong_password_here
|
||||
|
||||
# ============================================
|
||||
# Redis 配置
|
||||
# ============================================
|
||||
REDIS_HOST=1Panel-redis-xxxx
|
||||
REDIS_PORT=6379
|
||||
REDIS_PASSWORD=your_redis_password_here
|
||||
REDIS_DATABASE=0
|
||||
|
||||
# ============================================
|
||||
# 飞书配置
|
||||
# ============================================
|
||||
FEISHU_APP_ID=cli_xxxxxxxxxxxxx
|
||||
FEISHU_APP_SECRET=xxxxxxxxxxxxxxxxxxxx
|
||||
FEISHU_ENCRYPT_KEY=xxxxxxxxxxxxxxxxxxxx
|
||||
FEISHU_VERIFICATION_TOKEN=xxxxxxxxxxxxxxxxxxxx
|
||||
FEISHU_TENANT_KEY=14e7bc0fe5985740
|
||||
|
||||
# 飞书应用 URL(获取方式:飞书开放平台 -> 应用详情)
|
||||
FEISHU_APP_URL=https://open.feishu.cn
|
||||
|
||||
# ============================================
|
||||
# 应用配置
|
||||
# ============================================
|
||||
APP_ENV=production
|
||||
APP_DEBUG=false
|
||||
APP_URL=https://xsm.hnxinyouhe.com
|
||||
|
||||
# ============================================
|
||||
# 日志配置
|
||||
# ============================================
|
||||
LOG_LEVEL=info
|
||||
|
||||
# ============================================
|
||||
# 部署配置
|
||||
# ============================================
|
||||
DEPLOY_USER=root
|
||||
DEPLOY_SERVER=your-server-ip
|
||||
DEPLOY_PATH=/opt/x-server-mobile
|
||||
@@ -0,0 +1,61 @@
|
||||
# X-Server Mobile 生产环境 Dockerfile
|
||||
# 基于官方 PHP 8.5 CLI 镜像
|
||||
|
||||
FROM php:8.5-cli
|
||||
|
||||
# 安装系统依赖
|
||||
RUN apt-get update && apt-get install -y \
|
||||
unzip \
|
||||
git \
|
||||
libzip-dev \
|
||||
libpng-dev \
|
||||
libonig-dev \
|
||||
curl \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# 安装 PHP 扩展
|
||||
RUN docker-php-ext-install \
|
||||
zip \
|
||||
pdo \
|
||||
pdo_mysql \
|
||||
bcmath \
|
||||
pcntl \
|
||||
opcache \
|
||||
mbstring
|
||||
|
||||
# 安装 Composer
|
||||
COPY --from=composer:latest /usr/bin/composer /usr/local/bin/composer
|
||||
|
||||
# 设置工作目录
|
||||
WORKDIR /app
|
||||
|
||||
# 复制 composer 文件
|
||||
COPY composer.json composer.lock* ./
|
||||
|
||||
# 安装依赖(生产环境)
|
||||
RUN composer install \
|
||||
--no-dev \
|
||||
--optimize-autoloader \
|
||||
--no-interaction \
|
||||
--no-progress
|
||||
|
||||
# 复制应用文件
|
||||
COPY . .
|
||||
|
||||
# 复制 PHP 生产环境配置
|
||||
COPY php-prod.ini /usr/local/etc/php/conf.d/99-production.ini
|
||||
|
||||
# 创建运行时目录
|
||||
RUN mkdir -p /app/runtime/logs \
|
||||
&& mkdir -p /app/runtime/cache \
|
||||
&& chmod -R 755 /app/runtime
|
||||
|
||||
# 暴露端口
|
||||
EXPOSE 8787
|
||||
|
||||
# 健康检查
|
||||
HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \
|
||||
CMD php -r "exit(0);" || exit 1
|
||||
|
||||
# 启动 Webman(前台运行)
|
||||
CMD ["php", "start.php", "start"]
|
||||
Executable
+169
@@ -0,0 +1,169 @@
|
||||
#!/bin/bash
|
||||
# X-Server Mobile 生产部署脚本
|
||||
# 使用方法: ./deploy.sh [server]
|
||||
|
||||
set -e
|
||||
|
||||
# 配置
|
||||
SERVER=${1:-"your-server.com"} # 默认服务器,可以通过参数指定
|
||||
DEPLOY_USER="root"
|
||||
DEPLOY_PATH="/opt/x-server-mobile"
|
||||
FRONTEND_BUILD_DIR="./frontend/dist"
|
||||
BACKEND_SOURCE_DIR="./backend"
|
||||
|
||||
# 颜色输出
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
log_info() {
|
||||
echo -e "${GREEN}[INFO]${NC} $1"
|
||||
}
|
||||
|
||||
log_warn() {
|
||||
echo -e "${YELLOW}[WARN]${NC} $1"
|
||||
}
|
||||
|
||||
log_error() {
|
||||
echo -e "${RED}[ERROR]${NC} $1"
|
||||
}
|
||||
|
||||
# 检查环境
|
||||
check_env() {
|
||||
log_info "检查部署环境..."
|
||||
|
||||
if ! command -v rsync &> /dev/null; then
|
||||
log_error "rsync 未安装,请先安装: apt-get install rsync"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -d "$FRONTEND_BUILD_DIR" ]; then
|
||||
log_error "前端构建目录不存在: $FRONTEND_BUILD_DIR"
|
||||
log_info "请先运行: cd frontend && npm install && npm run build"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
log_info "环境检查通过"
|
||||
}
|
||||
|
||||
# 备份远程服务器
|
||||
backup_remote() {
|
||||
log_info "备份远程服务器..."
|
||||
|
||||
BACKUP_NAME="$(date +%Y%m%d_%H%M%S)"
|
||||
|
||||
ssh ${DEPLOY_USER}@${SERVER} << EOF
|
||||
mkdir -p ${DEPLOY_PATH}/backup/${BACKUP_NAME}
|
||||
cp -r ${DEPLOY_PATH}/frontend ${DEPLOY_PATH}/backup/${BACKUP_NAME}/ 2>/dev/null || true
|
||||
cp -r ${DEPLOY_PATH}/backend ${DEPLOY_PATH}/backup/${BACKUP_NAME}/ 2>/dev/null || true
|
||||
echo "备份完成: ${BACKUP_NAME}"
|
||||
EOF
|
||||
|
||||
log_info "备份完成: ${BACKUP_NAME}"
|
||||
}
|
||||
|
||||
# 部署前端
|
||||
deploy_frontend() {
|
||||
log_info "部署前端..."
|
||||
|
||||
rsync -avz --delete \
|
||||
--exclude='*.map' \
|
||||
${FRONTEND_BUILD_DIR}/ \
|
||||
${DEPLOY_USER}@${SERVER}:${DEPLOY_PATH}/frontend/
|
||||
|
||||
log_info "前端部署完成"
|
||||
}
|
||||
|
||||
# 部署后端
|
||||
deploy_backend() {
|
||||
log_info "部署后端..."
|
||||
|
||||
rsync -avz \
|
||||
--exclude='vendor/' \
|
||||
--exclude='runtime/' \
|
||||
--exclude='.git/' \
|
||||
--exclude='*.log' \
|
||||
${BACKEND_SOURCE_DIR}/ \
|
||||
${DEPLOY_USER}@${SERVER}:${DEPLOY_PATH}/backend/
|
||||
|
||||
log_info "后端部署完成"
|
||||
}
|
||||
|
||||
# 重启服务
|
||||
restart_services() {
|
||||
log_info "重启服务..."
|
||||
|
||||
ssh ${DEPLOY_USER}@${SERVER} << 'EOF'
|
||||
cd /opt/x-server-mobile
|
||||
|
||||
# 重启后端容器
|
||||
docker-compose -f docker-compose.prod.yml restart backend
|
||||
|
||||
# 等待后端启动
|
||||
sleep 5
|
||||
|
||||
# 检查容器状态
|
||||
docker ps | grep x-server-backend-prod
|
||||
|
||||
# 重载 OpenResty
|
||||
openresty -s reload || nginx -s reload
|
||||
|
||||
echo "服务重启完成"
|
||||
EOF
|
||||
|
||||
log_info "服务重启完成"
|
||||
}
|
||||
|
||||
# 健康检查
|
||||
health_check() {
|
||||
log_info "健康检查..."
|
||||
|
||||
sleep 3
|
||||
|
||||
# 检查前端
|
||||
if curl -s -o /dev/null -w "%{http_code}" http://${SERVER}/ | grep -q "200\|301\|302"; then
|
||||
log_info "前端检查通过"
|
||||
else
|
||||
log_warn "前端检查失败"
|
||||
fi
|
||||
|
||||
# 检查 API
|
||||
if curl -s http://${SERVER}/api/v1/health 2>/dev/null | grep -q "ok\|success"; then
|
||||
log_info "API 检查通过"
|
||||
else
|
||||
log_warn "API 检查失败(可能需要登录)"
|
||||
fi
|
||||
}
|
||||
|
||||
# 主流程
|
||||
main() {
|
||||
echo "========================================="
|
||||
echo " X-Server Mobile 生产部署脚本"
|
||||
echo "========================================="
|
||||
echo "服务器: ${SERVER}"
|
||||
echo "部署路径: ${DEPLOY_PATH}"
|
||||
echo ""
|
||||
|
||||
# 确认部署
|
||||
read -p "确认部署到 ${SERVER}? (y/N) " -n 1 -r
|
||||
echo
|
||||
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
|
||||
log_info "部署已取消"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
check_env
|
||||
backup_remote
|
||||
deploy_frontend
|
||||
deploy_backend
|
||||
restart_services
|
||||
health_check
|
||||
|
||||
echo ""
|
||||
log_info "部署完成!"
|
||||
echo "查看日志: ssh ${DEPLOY_USER}@${SERVER} 'docker logs -f x-server-backend-prod'"
|
||||
}
|
||||
|
||||
# 执行主流程
|
||||
main
|
||||
@@ -0,0 +1,58 @@
|
||||
# X-Server Mobile 生产环境 Docker Compose
|
||||
# 使用方法: docker-compose -f docker-compose.prod.yml up -d
|
||||
|
||||
services:
|
||||
# PHP 后端服务
|
||||
backend:
|
||||
build:
|
||||
context: ./backend
|
||||
dockerfile: Dockerfile.prod
|
||||
container_name: x-server-backend-prod
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "8787:8787"
|
||||
environment:
|
||||
# 应用环境
|
||||
- APP_ENV=production
|
||||
- APP_DEBUG=false
|
||||
|
||||
# 数据库配置(使用 1Panel 的 MySQL)
|
||||
# 注意:请替换为实际的容器名和密码
|
||||
- DB_HOST=1Panel-mysql-409s
|
||||
- DB_PORT=3306
|
||||
- DB_DATABASE=xserver
|
||||
- DB_USERNAME=xserver
|
||||
- DB_PASSWORD=${DB_PASSWORD:-change_me_in_production}
|
||||
|
||||
# Redis 配置(使用 1Panel 的 Redis)
|
||||
# 注意:请替换为实际的容器名和密码
|
||||
- REDIS_HOST=1Panel-redis-xxxx
|
||||
- REDIS_PORT=6379
|
||||
- REDIS_PASSWORD=${REDIS_PASSWORD:-change_me_in_production}
|
||||
|
||||
# 飞书配置(请替换为实际值)
|
||||
- FEISHU_APP_ID=${FEISHU_APP_ID}
|
||||
- FEISHU_APP_SECRET=${FEISHU_APP_SECRET}
|
||||
- FEISHU_ENCRYPT_KEY=${FEISHU_ENCRYPT_KEY}
|
||||
- FEISHU_VERIFICATION_TOKEN=${FEISHU_VERIFICATION_TOKEN}
|
||||
- FEISHU_TENANT_KEY=${FEISHU_TENANT_KEY:-14e7bc0fe5985740}
|
||||
volumes:
|
||||
- ./backend:/app
|
||||
- ./logs/backend:/app/runtime/logs
|
||||
networks:
|
||||
- 1panel-network
|
||||
healthcheck:
|
||||
test: ["CMD", "php", "-r", "exit(0);"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 40s
|
||||
logging:
|
||||
driver: "json-file"
|
||||
options:
|
||||
max-size: "10m"
|
||||
max-file: "3"
|
||||
|
||||
networks:
|
||||
1panel-network:
|
||||
external: true
|
||||
@@ -0,0 +1,58 @@
|
||||
; X-Server Mobile 生产环境 PHP 配置
|
||||
; 文件位置: backend/php-prod.ini
|
||||
; 用途: Docker 容器内的 PHP 生产环境配置
|
||||
|
||||
; ============================================
|
||||
; OPcache 配置
|
||||
; ============================================
|
||||
opcache.enable=1
|
||||
opcache.enable_cli=1
|
||||
opcache.memory_consumption=128
|
||||
opcache.interned_strings_buffer=8
|
||||
opcache.max_accelerated_files=10000
|
||||
opcache.revalidate_freq=60
|
||||
opcache.fast_shutdown=1
|
||||
opcache.save_comments=1
|
||||
|
||||
; ============================================
|
||||
; 错误处理
|
||||
; ============================================
|
||||
display_errors=0
|
||||
display_startup_errors=0
|
||||
log_errors=1
|
||||
error_log=/app/runtime/logs/php_errors.log
|
||||
error_reporting=E_ALL & ~E_DEPRECATED & ~E_STRICT
|
||||
|
||||
; ============================================
|
||||
; 安全配置
|
||||
; ============================================
|
||||
expose_php=Off
|
||||
allow_url_fopen=On
|
||||
allow_url_include=Off
|
||||
|
||||
; 禁用危险函数(根据实际需求调整)
|
||||
; disable_functions=exec,passthru,shell_exec,system,proc_open,popen,curl_exec,curl_multi_exec,parse_ini_file,show_source
|
||||
|
||||
; ============================================
|
||||
; 性能配置
|
||||
; ============================================
|
||||
max_execution_time=60
|
||||
max_input_time=60
|
||||
memory_limit=256M
|
||||
post_max_size=50M
|
||||
upload_max_filesize=50M
|
||||
max_file_uploads=20
|
||||
|
||||
; ============================================
|
||||
; 会话配置
|
||||
; ============================================
|
||||
session.cookie_httponly=1
|
||||
session.cookie_secure=1
|
||||
session.use_strict_mode=1
|
||||
session.cookie_samesite="Lax"
|
||||
|
||||
; ============================================
|
||||
; 其他配置
|
||||
; ============================================
|
||||
date.timezone=Asia/Shanghai
|
||||
default_charset="UTF-8"
|
||||
Executable
+159
@@ -0,0 +1,159 @@
|
||||
#!/bin/bash
|
||||
# X-Server Mobile 回滚脚本
|
||||
# 使用方法: ./rollback.sh [backup_name]
|
||||
|
||||
set -e
|
||||
|
||||
# 配置
|
||||
SERVER="your-server.com"
|
||||
DEPLOY_USER="root"
|
||||
DEPLOY_PATH="/opt/x-server-mobile"
|
||||
BACKUP_NAME=$1
|
||||
|
||||
# 颜色输出
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m'
|
||||
|
||||
log_info() {
|
||||
echo -e "${GREEN}[INFO]${NC} $1"
|
||||
}
|
||||
|
||||
log_warn() {
|
||||
echo -e "${YELLOW}[WARN]${NC} $1"
|
||||
}
|
||||
|
||||
log_error() {
|
||||
echo -e "${RED}[ERROR]${NC} $1"
|
||||
}
|
||||
|
||||
# 列出可用备份
|
||||
list_backups() {
|
||||
log_info "可用备份列表:"
|
||||
|
||||
ssh ${DEPLOY_USER}@${SERVER} << 'EOF'
|
||||
cd /opt/x-server-mobile/backup
|
||||
ls -1t | head -10
|
||||
EOF
|
||||
}
|
||||
|
||||
# 确认备份存在
|
||||
check_backup() {
|
||||
log_info "检查备份 ${BACKUP_NAME}..."
|
||||
|
||||
EXISTS=$(ssh ${DEPLOY_USER}@${SERVER} "
|
||||
if [ -d ${DEPLOY_PATH}/backup/${BACKUP_NAME} ]; then
|
||||
echo 'exists'
|
||||
else
|
||||
echo 'not_found'
|
||||
fi
|
||||
")
|
||||
|
||||
if [ "$EXISTS" != "exists" ]; then
|
||||
log_error "备份不存在: ${BACKUP_NAME}"
|
||||
list_backups
|
||||
exit 1
|
||||
fi
|
||||
|
||||
log_info "备份检查通过"
|
||||
}
|
||||
|
||||
# 回滚前端
|
||||
rollback_frontend() {
|
||||
log_info "回滚前端..."
|
||||
|
||||
ssh ${DEPLOY_USER}@${SERVER} << EOF
|
||||
rm -rf ${DEPLOY_PATH}/frontend/*
|
||||
cp -r ${DEPLOY_PATH}/backup/${BACKUP_NAME}/frontend/* ${DEPLOY_PATH}/frontend/
|
||||
EOF
|
||||
|
||||
log_info "前端回滚完成"
|
||||
}
|
||||
|
||||
# 回滚后端(可选)
|
||||
rollback_backend() {
|
||||
read -p "是否回滚后端代码? (y/N) " -n 1 -r
|
||||
echo
|
||||
|
||||
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
|
||||
log_info "跳过后端回滚"
|
||||
return
|
||||
fi
|
||||
|
||||
log_info "回滚后端..."
|
||||
|
||||
ssh ${DEPLOY_USER}@${SERVER} << EOF
|
||||
# 保留 vendor 目录
|
||||
mv ${DEPLOY_PATH}/backend/vendor /tmp/backend_vendor_backup 2>/dev/null || true
|
||||
|
||||
rm -rf ${DEPLOY_PATH}/backend/*
|
||||
cp -r ${DEPLOY_PATH}/backup/${BACKUP_NAME}/backend/* ${DEPLOY_PATH}/backend/
|
||||
|
||||
# 恢复 vendor 目录
|
||||
mv /tmp/backend_vendor_backup ${DEPLOY_PATH}/backend/vendor 2>/dev/null || true
|
||||
EOF
|
||||
|
||||
log_info "后端回滚完成"
|
||||
}
|
||||
|
||||
# 重启服务
|
||||
restart_services() {
|
||||
log_info "重启服务..."
|
||||
|
||||
ssh ${DEPLOY_USER}@${SERVER} << 'EOF'
|
||||
cd /opt/x-server-mobile
|
||||
|
||||
# 重启后端容器
|
||||
docker-compose -f docker-compose.prod.yml restart backend
|
||||
|
||||
# 等待后端启动
|
||||
sleep 5
|
||||
|
||||
# 重载 OpenResty
|
||||
openresty -s reload || nginx -s reload
|
||||
|
||||
echo "服务重启完成"
|
||||
EOF
|
||||
|
||||
log_info "服务重启完成"
|
||||
}
|
||||
|
||||
# 主流程
|
||||
main() {
|
||||
echo "========================================="
|
||||
echo " X-Server Mobile 回滚脚本"
|
||||
echo "========================================="
|
||||
echo "服务器: ${SERVER}"
|
||||
|
||||
if [ -z "$BACKUP_NAME" ]; then
|
||||
log_error "请指定备份名称"
|
||||
echo ""
|
||||
list_backups
|
||||
echo ""
|
||||
echo "使用方法: ./rollback.sh <backup_name>"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "备份名称: ${BACKUP_NAME}"
|
||||
echo ""
|
||||
|
||||
# 确认回滚
|
||||
read -p "确认回滚到 ${BACKUP_NAME}? (y/N) " -n 1 -r
|
||||
echo
|
||||
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
|
||||
log_info "回滚已取消"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
check_backup
|
||||
rollback_frontend
|
||||
rollback_backend
|
||||
restart_services
|
||||
|
||||
echo ""
|
||||
log_info "回滚完成!"
|
||||
}
|
||||
|
||||
# 执行主流程
|
||||
main
|
||||
@@ -0,0 +1,183 @@
|
||||
# X-Server Mobile 线上环境 OpenResty 配置
|
||||
# 文件位置: /usr/local/openresty/nginx/conf/conf.d/x-server-mobile.conf
|
||||
|
||||
# HTTP 重定向到 HTTPS
|
||||
server {
|
||||
listen 80;
|
||||
server_name xsm.hnxinyouhe.com;
|
||||
|
||||
# Let's Encrypt ACME 挑战
|
||||
location /.well-known/acme-challenge/ {
|
||||
root /var/www/certbot;
|
||||
}
|
||||
|
||||
# 强制 HTTPS
|
||||
location / {
|
||||
return 301 https://$server_name$request_uri;
|
||||
}
|
||||
}
|
||||
|
||||
# HTTPS 主服务
|
||||
server {
|
||||
listen 443 ssl http2;
|
||||
server_name xsm.hnxinyouhe.com;
|
||||
|
||||
# ============================================
|
||||
# SSL 证书配置
|
||||
# ============================================
|
||||
|
||||
# Let's Encrypt 证书路径
|
||||
ssl_certificate /etc/letsencrypt/live/xsm.hnxinyouhe.com/fullchain.pem;
|
||||
ssl_certificate_key /etc/letsencrypt/live/xsm.hnxinyouhe.com/privkey.pem;
|
||||
|
||||
# 如果使用自签名证书,取消下面的注释并注释上面的 Let's Encrypt 配置
|
||||
# ssl_certificate /etc/nginx/ssl/xsm.hnxinyouhe.com.crt;
|
||||
# ssl_certificate_key /etc/nginx/ssl/xsm.hnxinyouhe.com.key;
|
||||
|
||||
# SSL 协议和加密套件
|
||||
ssl_protocols TLSv1.2 TLSv1.3;
|
||||
ssl_ciphers 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384';
|
||||
ssl_prefer_server_ciphers off;
|
||||
|
||||
# SSL 会话缓存
|
||||
ssl_session_cache shared:SSL:10m;
|
||||
ssl_session_timeout 10m;
|
||||
ssl_session_tickets off;
|
||||
|
||||
# OCSP Stapling
|
||||
ssl_stapling on;
|
||||
ssl_stapling_verify on;
|
||||
ssl_trusted_certificate /etc/letsencrypt/live/xsm.hnxinyouhe.com/chain.pem;
|
||||
resolver 8.8.8.8 8.8.4.4 valid=300s;
|
||||
resolver_timeout 5s;
|
||||
|
||||
# ============================================
|
||||
# 日志配置
|
||||
# ============================================
|
||||
|
||||
access_log /opt/x-server-mobile/logs/nginx_access.log combined;
|
||||
error_log /opt/x-server-mobile/logs/nginx_error.log warn;
|
||||
|
||||
# ============================================
|
||||
# 安全头
|
||||
# ============================================
|
||||
|
||||
add_header X-Frame-Options "SAMEORIGIN" always;
|
||||
add_header X-Content-Type-Options "nosniff" always;
|
||||
add_header X-XSS-Protection "1; mode=block" always;
|
||||
add_header Referrer-Policy "no-referrer-when-downgrade" always;
|
||||
|
||||
# ============================================
|
||||
# Gzip 压缩
|
||||
# ============================================
|
||||
|
||||
gzip on;
|
||||
gzip_vary on;
|
||||
gzip_min_length 1024;
|
||||
gzip_comp_level 6;
|
||||
gzip_types
|
||||
text/plain
|
||||
text/css
|
||||
text/xml
|
||||
text/javascript
|
||||
application/json
|
||||
application/javascript
|
||||
application/xml+rss
|
||||
application/rss+xml
|
||||
font/truetype
|
||||
font/opentype
|
||||
application/vnd.ms-fontobject
|
||||
image/svg+xml;
|
||||
|
||||
# ============================================
|
||||
# 静态资源(长期缓存)
|
||||
# ============================================
|
||||
|
||||
location ~* \.(js|css|png|jpg|jpeg|gif|webp|svg|ico|woff|woff2|ttf|eot)$ {
|
||||
root /opt/x-server-mobile/frontend;
|
||||
expires 7d;
|
||||
add_header Cache-Control "public, immutable";
|
||||
access_log off;
|
||||
}
|
||||
|
||||
# ============================================
|
||||
# 飞书回调路由(必须在 API 之前)
|
||||
# ============================================
|
||||
|
||||
location /login {
|
||||
proxy_pass http://127.0.0.1:8787;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header Host $host;
|
||||
}
|
||||
|
||||
location /login/callback {
|
||||
proxy_pass http://127.0.0.1:8787;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header Host $host;
|
||||
}
|
||||
|
||||
location /login/qrcode {
|
||||
proxy_pass http://127.0.0.1:8787;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header Host $host;
|
||||
}
|
||||
|
||||
# 飞书卡片回调
|
||||
location /feishu/card/callback {
|
||||
proxy_pass http://127.0.0.1:8787;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header Host $host;
|
||||
}
|
||||
|
||||
# ============================================
|
||||
# API 代理
|
||||
# ============================================
|
||||
|
||||
location /api/ {
|
||||
proxy_pass http://127.0.0.1:8787;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header Host $host;
|
||||
|
||||
# 超时配置
|
||||
proxy_connect_timeout 60s;
|
||||
proxy_send_timeout 60s;
|
||||
proxy_read_timeout 60s;
|
||||
|
||||
# 缓冲配置
|
||||
proxy_buffering on;
|
||||
proxy_buffer_size 4k;
|
||||
proxy_buffers 8 4k;
|
||||
proxy_busy_buffers_size 8k;
|
||||
}
|
||||
|
||||
# ============================================
|
||||
# 前端 SPA(所有其他请求返回 index.html)
|
||||
# ============================================
|
||||
|
||||
location / {
|
||||
root /opt/x-server-mobile/frontend;
|
||||
|
||||
# SPA 路由支持
|
||||
try_files $uri $uri/ /index.html;
|
||||
|
||||
# 禁用 HTML 缓存
|
||||
add_header Cache-Control "no-cache, no-store, must-revalidate";
|
||||
add_header Pragma "no-cache";
|
||||
add_header Expires "0";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,598 @@
|
||||
# X-Server Mobile 线上部署方案
|
||||
|
||||
## 线上环境
|
||||
|
||||
- **面板**: 1Panel (Docker 管理)
|
||||
- **Web 服务器**: OpenResty
|
||||
- **数据库**: MySQL (容器)
|
||||
- **缓存**: Redis (容器)
|
||||
- **域名**: `xsm.hnxinyouhe.com` (待配置)
|
||||
|
||||
---
|
||||
|
||||
## 架构设计
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────┐
|
||||
│ OpenResty (80/443) │
|
||||
│ ┌─────────────────────────────┐ │
|
||||
│ │ 静态资源托管 │ │
|
||||
│ │ /frontend/dist/* │ │
|
||||
│ │ SPA Router fallback │ │
|
||||
│ └─────────────────────────────┘ │
|
||||
│ ┌─────────────────────────────┐ │
|
||||
│ │ API 反向代理 │ │
|
||||
│ │ /api/* → localhost:8787 │ │
|
||||
│ └─────────────────────────────┘ │
|
||||
│ ┌─────────────────────────────┐ │
|
||||
│ │ 飞书回调代理 │ │
|
||||
│ │ /login/* → localhost:8787 │ │
|
||||
│ └─────────────────────────────┘ │
|
||||
└─────────────────────────────────────┘
|
||||
│
|
||||
┌───────────────┼───────────────┐
|
||||
▼ ▼ ▼
|
||||
┌───────────┐ ┌───────────┐ ┌───────────┐
|
||||
│ 后端 │ │ MySQL │ │ Redis │
|
||||
│ :8787 │ │ :3306 │ │ :6379 │
|
||||
│ Docker │ │ 1Panel │ │ 1Panel │
|
||||
└───────────┘ └───────────┘ └───────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 部署步骤
|
||||
|
||||
### 第一步:准备线上目录结构
|
||||
|
||||
```bash
|
||||
# 在服务器上创建部署目录
|
||||
mkdir -p /opt/x-server-mobile
|
||||
mkdir -p /opt/x-server-mobile/frontend
|
||||
mkdir -p /opt/x-server-mobile/backend
|
||||
mkdir -p /opt/x-server-mobile/logs
|
||||
mkdir -p /opt/x-server-mobile/backup
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 第二步:前端部署
|
||||
|
||||
#### 2.1 本地构建
|
||||
|
||||
```bash
|
||||
# 在开发环境
|
||||
cd /www/x-server-mobile/frontend
|
||||
|
||||
# 安装依赖(如果需要)
|
||||
npm install
|
||||
|
||||
# 构建生产版本
|
||||
npm run build
|
||||
|
||||
# 生成的文件在 frontend/dist/ 目录
|
||||
```
|
||||
|
||||
#### 2.2 上传静态文件
|
||||
|
||||
```bash
|
||||
# 方式1:使用 scp 上传
|
||||
scp -r frontend/dist/* root@your-server:/opt/x-server-mobile/frontend/
|
||||
|
||||
# 方式2:使用 rsync 同步
|
||||
rsync -avz --delete frontend/dist/ root@your-server:/opt/x-server-mobile/frontend/
|
||||
```
|
||||
|
||||
#### 2.3 构建环境配置
|
||||
|
||||
创建 `frontend/.env.production`:
|
||||
|
||||
```env
|
||||
# API 地址(使用相对路径,由 OpenResty 代理)
|
||||
VITE_API_BASE=/api/v1
|
||||
|
||||
# 或使用绝对地址(如果前后端分离部署)
|
||||
# VITE_API_BASE=https://api.xsm.hnxinyouhe.com
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 第三步:后端部署
|
||||
|
||||
#### 3.1 创建生产环境 Docker Compose
|
||||
|
||||
**文件**: `/opt/x-server-mobile/deploy/docker-compose.prod.yml`
|
||||
|
||||
```yaml
|
||||
services:
|
||||
backend:
|
||||
build:
|
||||
context: ./backend
|
||||
dockerfile: Dockerfile
|
||||
container_name: x-server-backend-prod
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "8787:8787"
|
||||
environment:
|
||||
# 生产环境配置
|
||||
- APP_ENV=production
|
||||
- APP_DEBUG=false
|
||||
|
||||
# 数据库配置(使用 1Panel 的 MySQL)
|
||||
- DB_HOST=1Panel-mysql-xxxx # 替换为实际容器名
|
||||
- DB_PORT=3306
|
||||
- DB_DATABASE=xserver
|
||||
- DB_USERNAME=xserver # 建议创建专用数据库用户
|
||||
- DB_PASSWORD=your_password # 强密码
|
||||
|
||||
# Redis 配置(使用 1Panel 的 Redis)
|
||||
- REDIS_HOST=1Panel-redis-xxxx # 替换为实际容器名
|
||||
- REDIS_PORT=6379
|
||||
- REDIS_PASSWORD=your_redis_password
|
||||
|
||||
# 飞书配置
|
||||
- FEISHU_APP_ID=cli_xxxxxxxxxxxxx
|
||||
- FEISHU_APP_SECRET=xxxxxxxxxxxxxxxxxxxx
|
||||
- FEISHU_ENCRYPT_KEY=xxxxxxxxxxxxxxxxxxxx
|
||||
- FEISHU_VERIFICATION_TOKEN=xxxxxxxxxxxxxxxxxxxx
|
||||
- FEISHU_TENANT_KEY=xxxxxxxxxxxxxxxxxxxx
|
||||
volumes:
|
||||
- ./backend:/app
|
||||
- ./logs/app:/app/runtime/logs
|
||||
networks:
|
||||
- 1panel-network
|
||||
healthcheck:
|
||||
test: ["CMD", "php", "-r", "exit(0);"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 40s
|
||||
|
||||
networks:
|
||||
1panel-network:
|
||||
external: true
|
||||
```
|
||||
|
||||
#### 3.2 创建生产环境后端配置
|
||||
|
||||
**后端 OPcache 配置** (`deploy/php-prod.ini`):
|
||||
|
||||
```ini
|
||||
# 生产环境 OPcache 配置
|
||||
opcache.enable=1
|
||||
opcache.memory_consumption=128
|
||||
opcache.interned_strings_buffer=8
|
||||
opcache.max_accelerated_files=10000
|
||||
opcache.revalidate_freq=60
|
||||
opcache.fast_shutdown=1
|
||||
opcache.enable_cli=1
|
||||
|
||||
# 禁用错误显示
|
||||
display_errors=0
|
||||
display_startup_errors=0
|
||||
log_errors=1
|
||||
error_log=/app/runtime/logs/php_errors.log
|
||||
```
|
||||
|
||||
#### 3.3 更新 Dockerfile
|
||||
|
||||
```dockerfile
|
||||
FROM php:8.5-cli
|
||||
|
||||
# Install system dependencies
|
||||
RUN apt-get update && apt-get install -y \
|
||||
unzip \
|
||||
git \
|
||||
libzip-dev \
|
||||
libpng-dev \
|
||||
curl \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Install PHP extensions
|
||||
RUN docker-php-ext-install zip pdo pdo_mysql bcmath pcntl opcache
|
||||
|
||||
# Install Composer
|
||||
COPY --from=composer:latest /usr/bin/composer /usr/local/bin/composer
|
||||
|
||||
# Set working directory
|
||||
WORKDIR /app
|
||||
|
||||
# Copy composer files
|
||||
COPY composer.json composer.lock* ./
|
||||
|
||||
# Install dependencies
|
||||
RUN composer install --no-dev --optimize-autoloader --no-interaction
|
||||
|
||||
# Copy application files
|
||||
COPY . .
|
||||
|
||||
# Copy PHP configuration
|
||||
COPY php-prod.ini /usr/local/etc/php/conf.d/99-production.ini
|
||||
|
||||
# Create runtime directories
|
||||
RUN mkdir -p /app/runtime/logs
|
||||
|
||||
# Expose port
|
||||
EXPOSE 8787
|
||||
|
||||
# Start webman in foreground mode
|
||||
CMD ["php", "start.php", "start"]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 第四步:OpenResty 配置
|
||||
|
||||
#### 4.1 站点配置
|
||||
|
||||
**文件**: `/usr/local/openresty/nginx/conf/conf.d/x-server-mobile.conf`
|
||||
|
||||
```nginx
|
||||
# X-Server Mobile 线上环境配置
|
||||
server {
|
||||
listen 80;
|
||||
server_name xsm.hnxinyouhe.com;
|
||||
|
||||
# 强制 HTTPS
|
||||
return 301 https://$server_name$request_uri;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 443 ssl http2;
|
||||
server_name xsm.hnxinyouhe.com;
|
||||
|
||||
# SSL 证书配置(使用 Let's Encrypt 或自签名)
|
||||
ssl_certificate /etc/nginx/ssl/xsm.hnxinyouhe.com.crt;
|
||||
ssl_certificate_key /etc/nginx/ssl/xsm.hnxinyouhe.com.key;
|
||||
|
||||
# SSL 安全配置
|
||||
ssl_protocols TLSv1.2 TLSv1.3;
|
||||
ssl_ciphers HIGH:!aNULL:!MD5;
|
||||
ssl_prefer_server_ciphers on;
|
||||
ssl_session_cache shared:SSL:10m;
|
||||
ssl_session_timeout 10m;
|
||||
|
||||
# 日志
|
||||
access_log /opt/x-server-mobile/logs/nginx_access.log;
|
||||
error_log /opt/x-server-mobile/logs/nginx_error.log;
|
||||
|
||||
# Gzip 压缩
|
||||
gzip on;
|
||||
gzip_vary on;
|
||||
gzip_min_length 1024;
|
||||
gzip_types text/plain text/css text/xml text/javascript
|
||||
application/json application/javascript application/xml+rss
|
||||
application/rss+xml font/truetype font/opentype
|
||||
application/vnd.ms-fontobject image/svg+xml;
|
||||
|
||||
# 静态资源缓存
|
||||
location ~* \.(js|css|png|jpg|jpeg|gif|webp|svg|ico|woff|woff2|ttf|eot)$ {
|
||||
root /opt/x-server-mobile/frontend;
|
||||
expires 7d;
|
||||
add_header Cache-Control "public, immutable";
|
||||
}
|
||||
|
||||
# 飞书回调(必须在 API 之前配置)
|
||||
location /login {
|
||||
proxy_pass http://127.0.0.1:8787;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
location /login/callback {
|
||||
proxy_pass http://127.0.0.1:8787;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
location /login/qrcode {
|
||||
proxy_pass http://127.0.0.1:8787;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
# 飞书卡片回调
|
||||
location /feishu/card/callback {
|
||||
proxy_pass http://127.0.0.1:8787;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
# API 代理
|
||||
location /api/ {
|
||||
proxy_pass http://127.0.0.1:8787;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header Host $host;
|
||||
|
||||
# 超时配置
|
||||
proxy_connect_timeout 60s;
|
||||
proxy_send_timeout 60s;
|
||||
proxy_read_timeout 60s;
|
||||
}
|
||||
|
||||
# 前端 SPA(所有其他请求返回 index.html)
|
||||
location / {
|
||||
root /opt/x-server-mobile/frontend;
|
||||
try_files $uri $uri/ /index.html;
|
||||
add_header Cache-Control "no-cache, no-store, must-revalidate";
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 4.2 配置说明
|
||||
|
||||
1. **静态资源**: 直接由 OpenResty 托管,7天缓存
|
||||
2. **API 请求**: 代理到后端 8787 端口
|
||||
3. **飞书回调**: 特殊路径直接代理到后端
|
||||
4. **SPA 路由**: 所有其他请求返回 index.html
|
||||
|
||||
---
|
||||
|
||||
### 第五步:数据库准备
|
||||
|
||||
#### 5.1 创建数据库和用户
|
||||
|
||||
```sql
|
||||
-- 创建专用数据库用户
|
||||
CREATE USER 'xserver'@'%' IDENTIFIED BY 'your_strong_password';
|
||||
|
||||
-- 授予权限
|
||||
GRANT ALL PRIVILEGES ON xserver.* TO 'xserver'@'%';
|
||||
FLUSH PRIVILEGES;
|
||||
|
||||
-- 如果数据库不存在,创建并导入结构
|
||||
CREATE DATABASE IF NOT EXISTS xserver CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
```
|
||||
|
||||
#### 5.2 数据库迁移
|
||||
|
||||
```bash
|
||||
# 从开发环境导出数据
|
||||
docker exec 1Panel-mysql-409s mysqldump -uroot -pmysql_xEsdPa xserver > xserver_backup.sql
|
||||
|
||||
# 导入到线上数据库
|
||||
docker exec -i 1Panel-mysql-xxxx mysql -uxserver -pyour_password xserver < xserver_backup.sql
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 第六步:SSL 证书配置
|
||||
|
||||
#### 6.1 使用 Let's Encrypt(推荐)
|
||||
|
||||
```bash
|
||||
# 安装 certbot
|
||||
apt-get install certbot
|
||||
|
||||
# 获取证书
|
||||
certbot certonly --standalone -d xsm.hnxinyouhe.com
|
||||
|
||||
# 证书路径
|
||||
# /etc/letsencrypt/live/xsm.hnxinyouhe.com/fullchain.pem
|
||||
# /etc/letsencrypt/live/xsm.hnxinyouhe.com/privkey.pem
|
||||
```
|
||||
|
||||
#### 6.2 自动续期
|
||||
|
||||
```bash
|
||||
# 添加 crontab
|
||||
0 3 * * * certbot renew --quiet && nginx -s reload
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 第七步:部署流程
|
||||
|
||||
#### 开发到生产部署
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# deploy.sh - 部署脚本
|
||||
|
||||
set -e
|
||||
|
||||
echo "========================================="
|
||||
echo " X-Server Mobile 生产部署脚本"
|
||||
echo "========================================="
|
||||
|
||||
# 1. 备份当前版本
|
||||
echo "📦 备份当前版本..."
|
||||
ssh root@your-server "mkdir -p /opt/x-server-mobile/backup/\$(date +%Y%m%d_%H%M%S)"
|
||||
ssh root@your-server "cp -r /opt/x-server-mobile/frontend /opt/x-server-mobile/backup/\$(date +%Y%m%d_%H%M%S)/"
|
||||
|
||||
# 2. 构建前端
|
||||
echo "🔨 构建前端..."
|
||||
cd frontend
|
||||
npm install
|
||||
npm run build
|
||||
|
||||
# 3. 上传前端
|
||||
echo "📤 上传前端..."
|
||||
rsync -avz --delete dist/ root@your-server:/opt/x-server-mobile/frontend/
|
||||
|
||||
# 4. 上传后端
|
||||
echo "📤 上传后端..."
|
||||
rsync -avz --exclude='vendor' --exclude='runtime' \
|
||||
../backend/ root@your-server:/opt/x-server-mobile/backend/
|
||||
|
||||
# 5. 重启后端容器
|
||||
echo "🔄 重启后端..."
|
||||
ssh root@your-server "cd /opt/x-server-mobile && docker-compose -f docker-compose.prod.yml restart backend"
|
||||
|
||||
# 6. 重载 OpenResty
|
||||
echo "🔄 重载 OpenResty..."
|
||||
ssh root@your-server "openresty -s reload"
|
||||
|
||||
echo "✅ 部署完成!"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 第八步:监控和日志
|
||||
|
||||
#### 8.1 日志位置
|
||||
|
||||
```bash
|
||||
# Nginx 访问日志
|
||||
/opt/x-server-mobile/logs/nginx_access.log
|
||||
|
||||
# Nginx 错误日志
|
||||
/opt/x-server-mobile/logs/nginx_error.log
|
||||
|
||||
# 后端应用日志
|
||||
docker logs x-server-backend-prod -f
|
||||
|
||||
# 后端运行时日志
|
||||
/opt/x-server-mobile/logs/app/
|
||||
```
|
||||
|
||||
#### 8.2 监控命令
|
||||
|
||||
```bash
|
||||
# 检查后端容器状态
|
||||
docker ps | grep x-server-backend-prod
|
||||
|
||||
# 检查后端健康
|
||||
curl http://127.0.0.1:8787/
|
||||
|
||||
# 检查数据库连接
|
||||
docker exec x-server-backend-prod php -r "var_dump(getenv('DB_HOST'));"
|
||||
|
||||
# 查看 PHP OPcache 状态
|
||||
# 需要在后端添加 OPcache 状态页面
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 第九步:安全加固
|
||||
|
||||
#### 9.1 防火墙配置
|
||||
|
||||
```bash
|
||||
# 只开放必要端口
|
||||
ufw allow 22 # SSH
|
||||
ufw allow 80 # HTTP
|
||||
ufw allow 443 # HTTPS
|
||||
ufw enable
|
||||
```
|
||||
|
||||
#### 9.2 PHP 安全配置
|
||||
|
||||
```ini
|
||||
# php-prod.ini
|
||||
expose_php = Off
|
||||
allow_url_fopen = Off
|
||||
allow_url_include = Off
|
||||
disable_functions = exec,passthru,shell_exec,system,proc_open,popen
|
||||
```
|
||||
|
||||
#### 9.3 环境变量保护
|
||||
|
||||
- 不要将敏感信息提交到代码仓库
|
||||
- 使用 `.env` 文件或 Docker secrets 管理敏感信息
|
||||
- 定期轮换密码和密钥
|
||||
|
||||
---
|
||||
|
||||
### 第十步:回滚方案
|
||||
|
||||
#### 10.1 快速回滚
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# rollback.sh - 回滚脚本
|
||||
|
||||
BACKUP_DATE=$1 # 格式: 20240227_150000
|
||||
|
||||
if [ -z "$BACKUP_DATE" ]; then
|
||||
echo "请指定备份日期,格式: 20240227_150000"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "🔄 回滚到 $BACKUP_DATE ..."
|
||||
|
||||
# 恢复前端
|
||||
cp -r /opt/x-server-mobile/backup/$BACKUP_DATE/frontend/* /opt/x-server-mobile/frontend/
|
||||
|
||||
# 恢复后端(可选)
|
||||
# cd /opt/x-server-mobile/backend
|
||||
# git checkout <previous-commit>
|
||||
# docker-compose -f docker-compose.prod.yml restart backend
|
||||
|
||||
# 重载 OpenResty
|
||||
openresty -s reload
|
||||
|
||||
echo "✅ 回滚完成"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 检查清单
|
||||
|
||||
部署前检查:
|
||||
|
||||
- [ ] 域名 DNS 已配置指向服务器 IP
|
||||
- [ ] SSL 证书已配置
|
||||
- [ ] MySQL 数据库已创建
|
||||
- [ ] Redis 可正常连接
|
||||
- [ ] 飞书应用回调地址已更新
|
||||
- [ ] 环境变量已正确配置
|
||||
- [ ] 防火墙规则已设置
|
||||
- [ ] 日志目录已创建
|
||||
|
||||
部署后验证:
|
||||
|
||||
- [ ] 前端页面可正常访问
|
||||
- [ ] API 接口返回正常
|
||||
- [ ] 飞书登录功能正常
|
||||
- [ ] 审批流程功能正常
|
||||
- [ ] 日志输出正常
|
||||
- [ ] 容器健康检查通过
|
||||
|
||||
---
|
||||
|
||||
## 常见问题
|
||||
|
||||
### Q1: 前端刷新 404
|
||||
|
||||
**原因**: OpenResty 配置没有正确处理 SPA 路由
|
||||
|
||||
**解决**: 确保 `try_files $uri $uri/ /index.html;` 配置正确
|
||||
|
||||
### Q2: API 请求跨域
|
||||
|
||||
**原因**: 前端和后端域名不同
|
||||
|
||||
**解决**: 在后端添加 CORS 头,或使用 OpenResty 代理统一域名
|
||||
|
||||
### Q3: 飞书回调失败
|
||||
|
||||
**原因**: 回调地址配置错误或 SSL 证书问题
|
||||
|
||||
**解决**:
|
||||
- 确认飞书后台配置的回调地址为 `https://xsm.hnxinyouhe.com/login/callback`
|
||||
- 确保服务器可公网访问
|
||||
- 检查 SSL 证书是否有效
|
||||
|
||||
### Q4: 容器无法连接数据库
|
||||
|
||||
**原因**: 容器网络不在同一个网络
|
||||
|
||||
**解决**: 确保后端容器和 MySQL/Redis 容器在同一个 Docker 网络(如 1panel-network)
|
||||
|
||||
---
|
||||
|
||||
## 联系方式
|
||||
|
||||
如有问题,请联系开发团队。
|
||||
@@ -0,0 +1,287 @@
|
||||
# X-Server Mobile 部署检查清单
|
||||
|
||||
## 部署前检查
|
||||
|
||||
### 服务器环境
|
||||
- [ ] 服务器已安装 1Panel 面板
|
||||
- [ ] 已安装 OpenResty 并正常运行
|
||||
- [ ] 已安装 MySQL 容器(1Panel 管理)
|
||||
- [ ] 已安装 Redis 容器(1Panel 管理)
|
||||
- [ ] Docker 网络配置正确(1panel-network 存在)
|
||||
|
||||
### 网络配置
|
||||
- [ ] 域名 DNS 已解析到服务器 IP
|
||||
- [ ] 服务器防火墙已开放 80、443 端口
|
||||
- [ ] 服务器可从外网访问
|
||||
|
||||
### 数据库准备
|
||||
- [ ] 创建生产数据库 `xserver`
|
||||
- [ ] 创建专用数据库用户 `xserver`
|
||||
- [ ] 授予用户权限
|
||||
- [ ] 导入数据库结构和初始数据
|
||||
|
||||
### SSL 证书
|
||||
- [ ] 安装 certbot: `apt-get install certbot`
|
||||
- [ ] 获取证书: `certbot certonly --standalone -d xsm.hnxinyouhe.com`
|
||||
- [ ] 验证证书路径正确
|
||||
|
||||
### 环境变量
|
||||
- [ ] 复制 `.env.production.template` 为 `.env.production`
|
||||
- [ ] 填写数据库配置
|
||||
- [ ] 填写 Redis 配置
|
||||
- [ ] 填写飞书配置
|
||||
- [ ] 更新飞书回调地址为 `https://xsm.hnxinyouhe.com/login/callback`
|
||||
|
||||
### 目录结构
|
||||
- [ ] 创建部署目录: `/opt/x-server-mobile`
|
||||
- [ ] 创建子目录: frontend, backend, logs, backup
|
||||
- [ ] 设置正确的目录权限
|
||||
|
||||
---
|
||||
|
||||
## 部署步骤
|
||||
|
||||
### 1. 准备服务器环境
|
||||
|
||||
```bash
|
||||
# SSH 登录服务器
|
||||
ssh root@your-server
|
||||
|
||||
# 创建目录结构
|
||||
mkdir -p /opt/x-server-mobile/{frontend,backend,logs,backup}
|
||||
mkdir -p /opt/x-server-mobile/logs/backend
|
||||
mkdir -p /opt/x-server-mobile/logs/nginx
|
||||
|
||||
# 创建日志文件
|
||||
touch /opt/x-server-mobile/logs/nginx_access.log
|
||||
touch /opt/x-server-mobile/logs/nginx_error.log
|
||||
```
|
||||
|
||||
### 2. 配置 OpenResty
|
||||
|
||||
```bash
|
||||
# 复制配置文件到 OpenResty 配置目录
|
||||
cp deploy/nginx/x-server-mobile-prod.conf /usr/local/openresty/nginx/conf/conf.d/x-server-mobile.conf
|
||||
|
||||
# 测试配置
|
||||
openresty -t
|
||||
|
||||
# 如果测试通过,重载配置
|
||||
openresty -s reload
|
||||
```
|
||||
|
||||
### 3. 构建前端
|
||||
|
||||
```bash
|
||||
# 在开发环境
|
||||
cd /www/x-server-mobile/frontend
|
||||
npm install
|
||||
npm run build
|
||||
|
||||
# 验证构建产物
|
||||
ls -la dist/
|
||||
```
|
||||
|
||||
### 4. 部署
|
||||
|
||||
```bash
|
||||
# 方式1: 使用部署脚本
|
||||
./deploy.sh your-server-ip
|
||||
|
||||
# 方式2: 手动部署
|
||||
# 上传前端
|
||||
scp -r frontend/dist/* root@server:/opt/x-server-mobile/frontend/
|
||||
|
||||
# 上传后端
|
||||
scp -r backend/ root@server:/opt/x-server-mobile/backend/
|
||||
|
||||
# 上传 docker-compose
|
||||
scp docker-compose.prod.yml root@server:/opt/x-server-mobile/
|
||||
```
|
||||
|
||||
### 5. 启动后端
|
||||
|
||||
```bash
|
||||
# SSH 登录服务器
|
||||
ssh root@server
|
||||
|
||||
# 进入部署目录
|
||||
cd /opt/x-server-mobile
|
||||
|
||||
# 加载环境变量
|
||||
source .env.production
|
||||
|
||||
# 启动容器
|
||||
docker-compose -f docker-compose.prod.yml up -d
|
||||
|
||||
# 查看容器状态
|
||||
docker ps | grep x-server
|
||||
|
||||
# 查看日志
|
||||
docker logs -f x-server-backend-prod
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 部署后验证
|
||||
|
||||
### 前端验证
|
||||
- [ ] 访问 `https://xsm.hnxinyouhe.com` 显示登录页面
|
||||
- [ ] 页面样式加载正常
|
||||
- [ ] 浏览器控制台无错误
|
||||
|
||||
### 后端验证
|
||||
- [ ] 容器运行正常: `docker ps | grep x-server`
|
||||
- [ ] 健康检查通过
|
||||
- [ ] 日志无错误信息
|
||||
|
||||
### 功能验证
|
||||
- [ ] 邮箱密码登录功能正常
|
||||
- [ ] 飞书客户端内免登正常
|
||||
- [ ] 飞书扫码登录正常
|
||||
- [ ] 审批列表正常显示
|
||||
- [ ] 出差申请功能正常
|
||||
- [ ] 飞书卡片通知正常
|
||||
|
||||
### 飞书回调验证
|
||||
- [ ] 飞书后台配置回调地址: `https://xsm.hnxinyouhe.com/login/callback`
|
||||
- [ ] 点击飞书卡片"查看详情"跳转正确
|
||||
- [ ] 快速审批按钮功能正常
|
||||
|
||||
---
|
||||
|
||||
## 监控配置
|
||||
|
||||
### 日志监控
|
||||
|
||||
```bash
|
||||
# Nginx 访问日志
|
||||
tail -f /opt/x-server-mobile/logs/nginx_access.log
|
||||
|
||||
# Nginx 错误日志
|
||||
tail -f /opt/x-server-mobile/logs/nginx_error.log
|
||||
|
||||
# 后端容器日志
|
||||
docker logs -f x-server-backend-prod
|
||||
|
||||
# 后端应用日志
|
||||
tail -f /opt/x-server-mobile/logs/backend/*.log
|
||||
```
|
||||
|
||||
### 性能监控
|
||||
|
||||
```bash
|
||||
# 容器资源使用
|
||||
docker stats x-server-backend-prod
|
||||
|
||||
# 磁盘使用
|
||||
du -sh /opt/x-server-mobile/*
|
||||
|
||||
# 内存使用
|
||||
free -h
|
||||
```
|
||||
|
||||
### 数据库监控
|
||||
|
||||
```bash
|
||||
# 检查 MySQL 连接
|
||||
docker exec x-server-backend-prod php -r "
|
||||
try {
|
||||
new PDO('mysql:host=DB_HOST;dbname=xserver', 'xserver', 'password');
|
||||
echo 'Database connection OK\n';
|
||||
} catch (Exception \$e) {
|
||||
echo 'Database connection FAILED: ' . \$e->getMessage() . '\n';
|
||||
}
|
||||
"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 回滚准备
|
||||
|
||||
### 备份检查
|
||||
|
||||
```bash
|
||||
# 检查备份目录
|
||||
ls -la /opt/x-server-mobile/backup/
|
||||
|
||||
# 查看最新备份
|
||||
ls -lt /opt/x-server-mobile/backup/ | head -5
|
||||
```
|
||||
|
||||
### 回滚测试
|
||||
|
||||
```bash
|
||||
# 测试回滚脚本
|
||||
./rollback.sh <backup_name>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 定期维护
|
||||
|
||||
### 每周
|
||||
- [ ] 检查容器运行状态
|
||||
- [ ] 检查日志错误
|
||||
- [ ] 检查磁盘空间
|
||||
|
||||
### 每月
|
||||
- [ ] 更新 SSL 证书(自动续期)
|
||||
- [ ] 数据库备份
|
||||
- [ ] 日志归档和清理
|
||||
|
||||
### 每季度
|
||||
- [ ] 安全更新检查
|
||||
- [ ] 性能优化评估
|
||||
- [ ] 灾难恢复演练
|
||||
|
||||
---
|
||||
|
||||
## 应急处理
|
||||
|
||||
### 服务异常
|
||||
|
||||
```bash
|
||||
# 查看容器状态
|
||||
docker ps -a | grep x-server
|
||||
|
||||
# 重启后端容器
|
||||
docker restart x-server-backend-prod
|
||||
|
||||
# 查看详细日志
|
||||
docker logs --tail 100 x-server-backend-prod
|
||||
```
|
||||
|
||||
### 数据库异常
|
||||
|
||||
```bash
|
||||
# 检查 MySQL 容器
|
||||
docker ps | grep mysql
|
||||
|
||||
# 进入 MySQL
|
||||
docker exec -it <mysql-container> mysql -uxserver -p
|
||||
|
||||
# 检查连接数
|
||||
SHOW PROCESSLIST;
|
||||
```
|
||||
|
||||
### 网络异常
|
||||
|
||||
```bash
|
||||
# 检查端口监听
|
||||
netstat -tlnp | grep -E '80|443|8787'
|
||||
|
||||
# 检查防火墙
|
||||
ufw status
|
||||
|
||||
# 测试外网访问
|
||||
curl -I https://xsm.hnxinyouhe.com
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 联系信息
|
||||
|
||||
- 开发负责人: ___________
|
||||
- 运维负责人: ___________
|
||||
- 紧急联系电话: ___________
|
||||
@@ -0,0 +1,219 @@
|
||||
# 开发环境反向代理配置指南
|
||||
|
||||
本指南用于配置开发环境的 OpenResty 和 frpc,以便将本地 API 暴露到公网调试飞书登录。
|
||||
|
||||
## 前提条件
|
||||
|
||||
- 已安装并运行 OpenResty
|
||||
- 已安装并运行 frpc
|
||||
- Docker 中运行的后端服务 (localhost:8787)
|
||||
|
||||
## 配置步骤
|
||||
|
||||
### 1. OpenResty 配置
|
||||
|
||||
在你的 OpenResty 配置目录中创建以下配置文件:
|
||||
|
||||
```nginx
|
||||
# /usr/local/openresty/nginx/conf/conf.d/x-server-api.conf
|
||||
|
||||
server {
|
||||
listen 8080; # 本地开发端口,避免与 80 端口冲突
|
||||
server_name api.x-server.local;
|
||||
|
||||
# API 反向代理
|
||||
location /api/ {
|
||||
proxy_pass http://127.0.0.1:8787/api/;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
|
||||
# CORS 配置(飞书请求需要)
|
||||
add_header Access-Control-Allow-Origin *;
|
||||
add_header Access-Control-Allow-Methods 'GET, POST, PUT, DELETE, OPTIONS';
|
||||
add_header Access-Control-Allow-Headers 'DNT,X-Mx-ReqToken,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization,X-Tenant-Key';
|
||||
|
||||
if ($request_method = 'OPTIONS') {
|
||||
return 204;
|
||||
}
|
||||
|
||||
proxy_connect_timeout 60s;
|
||||
proxy_send_timeout 60s;
|
||||
proxy_read_timeout 60s;
|
||||
}
|
||||
|
||||
# 健康检查
|
||||
location /health {
|
||||
return 200 "OK\n";
|
||||
add_header Content-Type text/plain;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
重载配置:
|
||||
```bash
|
||||
sudo openresty -s reload
|
||||
# 或
|
||||
sudo systemctl reload openresty
|
||||
```
|
||||
|
||||
### 2. frpc 配置
|
||||
|
||||
配置 frpc 将本地 OpenResty 服务暴露到公网:
|
||||
|
||||
```ini
|
||||
# ~/.frpc.ini 或 /etc/frp/frpc.ini
|
||||
|
||||
[common]
|
||||
server_addr = your-frp-server.com # 你的 frp 服务器地址
|
||||
server_port = 7000
|
||||
token = your-token
|
||||
|
||||
[x-server-api]
|
||||
type = http
|
||||
local_ip = 127.0.0.1
|
||||
local_port = 8080 # OpenResty 监听端口
|
||||
custom_domains = api-dev.x-server.your-domain.com # 开发环境域名
|
||||
|
||||
# 可选:启用加密和压缩
|
||||
use_encryption = true
|
||||
use_compression = true
|
||||
```
|
||||
|
||||
启动 frpc:
|
||||
```bash
|
||||
frpc -c ~/.frpc.ini
|
||||
```
|
||||
|
||||
### 3. 本地 hosts 配置
|
||||
|
||||
在本地开发时配置 hosts:
|
||||
|
||||
```bash
|
||||
# /etc/hosts
|
||||
127.0.0.1 api.x-server.local
|
||||
```
|
||||
|
||||
### 4. 飞书应用配置
|
||||
|
||||
在飞书开放平台配置开发环境回调 URL:
|
||||
|
||||
```
|
||||
https://api-dev.x-server.your-domain.com/api/v1/auth/feishu-login
|
||||
```
|
||||
|
||||
## 验证配置
|
||||
|
||||
### 1. 本地测试
|
||||
```bash
|
||||
# 直接访问后端
|
||||
curl http://localhost:8787/api/v1/user/info
|
||||
|
||||
# 通过 OpenResty 访问
|
||||
curl http://localhost:8080/api/v1/user/info
|
||||
|
||||
# 通过本地域名访问
|
||||
curl http://api.x-server.local/api/v1/user/info
|
||||
```
|
||||
|
||||
### 2. 公网测试
|
||||
```bash
|
||||
# 通过 frps 域名访问
|
||||
curl https://api-dev.x-server.your-domain.com/api/v1/user/info
|
||||
```
|
||||
|
||||
### 3. 飞书调试
|
||||
1. 在飞书开放平台创建测试应用
|
||||
2. 配置回调 URL 为开发环境域名
|
||||
3. 使用飞书 APP ID 和 APP SECRET 进行测试
|
||||
4. 检查后端日志查看请求详情
|
||||
|
||||
## 开发与生产环境区分
|
||||
|
||||
### 开发环境(使用 frpc)
|
||||
- 域名:`api-dev.x-server.your-domain.com`
|
||||
- 使用 frpc 暴露本地服务
|
||||
- CORS 允许所有来源
|
||||
- 详细日志输出
|
||||
|
||||
### 生产环境(不使用 frpc)
|
||||
- 域名:`api.x-server.com`
|
||||
- 直接部署到服务器
|
||||
- CORS 严格限制
|
||||
- 日志级别调整
|
||||
|
||||
## 故障排查
|
||||
|
||||
### OpenResty 无法访问后端
|
||||
1. 检查 Docker 容器是否运行:`docker ps`
|
||||
2. 检查端口是否正确:`docker ps | grep 8787`
|
||||
3. 检查网络连通性:`curl http://127.0.0.1:8787`
|
||||
|
||||
### frpc 连接失败
|
||||
1. 检查 frp 服务器是否可访问
|
||||
2. 检查 token 是否正确
|
||||
3. 查看 frpc 日志:`frpc -c ~/.frpc.ini`
|
||||
|
||||
### 飞书回调失败
|
||||
1. 检查域名 DNS 解析是否指向 frp 服务器
|
||||
2. 检查 frp 服务器是否配置了域名白名单
|
||||
3. 检查后端日志是否收到请求
|
||||
4. 验证 CORS 配置是否正确
|
||||
|
||||
## 安全建议
|
||||
|
||||
1. **开发环境使用独立的域名和 token**
|
||||
2. **定期更新 frp token**
|
||||
3. **不要在代码中硬编码生产环境配置**
|
||||
4. **使用环境变量管理敏感信息**
|
||||
5. **生产环境部署前移除 frpc 配置**
|
||||
|
||||
## 常用命令
|
||||
|
||||
```bash
|
||||
# 查看 Docker 容器日志
|
||||
docker logs x-server-backend -f
|
||||
|
||||
# 重启后端服务
|
||||
docker compose restart backend
|
||||
|
||||
# 重载 OpenResty 配置
|
||||
sudo openresty -s reload
|
||||
|
||||
# 查看 frpc 状态
|
||||
frpc status -c ~/.frpc.ini
|
||||
|
||||
# 测试 API 连通性
|
||||
curl -v http://localhost:8787/api/v1/user/info
|
||||
```
|
||||
|
||||
## 环境变量配置
|
||||
|
||||
创建 `.env.development` 文件用于开发环境:
|
||||
|
||||
```env
|
||||
# 后端配置
|
||||
DB_HOST=mysql
|
||||
DB_PORT=3306
|
||||
DB_DATABASE=x_server_mobile
|
||||
DB_USERNAME=root
|
||||
DB_PASSWORD=mysql_xEsdPa
|
||||
|
||||
# Redis 配置
|
||||
REDIS_HOST=redis
|
||||
REDIS_PORT=6379
|
||||
|
||||
# 飞书配置(开发环境)
|
||||
FEISHU_APP_ID=cli_xxxxxxxxxxxxx
|
||||
FEISHU_APP_SECRET=xxxxxxxxxxxxxxxxxxxx
|
||||
FEISHU_ENCRYPT_KEY=xxxxxxxxxxxxxxxxxxxx
|
||||
FEISHU_VERIFICATION_TOKEN=xxxxxxxxxxxxxxxxxxxx
|
||||
|
||||
# API 配置
|
||||
API_BASE_URL=http://localhost:8787
|
||||
PUBLIC_API_URL=https://api-dev.x-server.your-domain.com
|
||||
```
|
||||
|
||||
生产环境使用 `.env.production` 文件,移除 frpc 相关配置。
|
||||
@@ -0,0 +1,171 @@
|
||||
# Git 工作流
|
||||
|
||||
## 分支策略
|
||||
|
||||
采用 Git Flow 简化版工作流:
|
||||
|
||||
### 主要分支
|
||||
|
||||
| 分支 | 用途 | 说明 |
|
||||
|------|------|------|
|
||||
| `master` | 生产环境 | 保持稳定,只接受从 develop 合并的代码 |
|
||||
| `develop` | 开发环境 | 主开发分支,包含最新的开发功能 |
|
||||
| `feature/*` | 功能分支 | 从 develop 分支,开发完成后合并回 develop |
|
||||
| `hotfix/*` | 热修复分支 | 从 master 分支,修复后合并到 master 和 develop |
|
||||
|
||||
## 分支命名规范
|
||||
|
||||
### 功能分支
|
||||
```
|
||||
feature/功能名称
|
||||
例如:feature/i18n-support
|
||||
```
|
||||
|
||||
### 热修复分支
|
||||
```
|
||||
hotfix/问题描述
|
||||
例如:hotfix/login-error
|
||||
```
|
||||
|
||||
## 工作流程
|
||||
|
||||
### 1. 开发新功能
|
||||
|
||||
```bash
|
||||
# 从 develop 创建功能分支
|
||||
git checkout develop
|
||||
git pull origin develop
|
||||
git checkout -b feature/your-feature-name
|
||||
|
||||
# 开发并提交
|
||||
git add .
|
||||
git commit -m "feat: add your feature"
|
||||
|
||||
# 推送到远程
|
||||
git push -u origin feature/your-feature-name
|
||||
|
||||
# 完成后合并到 develop
|
||||
git checkout develop
|
||||
git merge feature/your-feature-name
|
||||
git push origin develop
|
||||
|
||||
# 删除功能分支(可选)
|
||||
git branch -d feature/your-feature-name
|
||||
```
|
||||
|
||||
### 2. 修复紧急问题
|
||||
|
||||
```bash
|
||||
# 从 master 创建热修复分支
|
||||
git checkout master
|
||||
git pull origin master
|
||||
git checkout -b hotfix/your-fix-name
|
||||
|
||||
# 修复并提交
|
||||
git add .
|
||||
git commit -m "fix: describe the fix"
|
||||
|
||||
# 推送到远程
|
||||
git push -u origin hotfix/your-fix-name
|
||||
|
||||
# 合并到 master 和 develop
|
||||
git checkout master
|
||||
git merge hotfix/your-fix-name
|
||||
git push origin master
|
||||
|
||||
git checkout develop
|
||||
git merge hotfix/your-fix-name
|
||||
git push origin develop
|
||||
```
|
||||
|
||||
### 3. 发布到生产
|
||||
|
||||
```bash
|
||||
# 将 develop 合并到 master
|
||||
git checkout master
|
||||
git merge develop
|
||||
git push origin master
|
||||
|
||||
# 打标签
|
||||
git tag -a v1.0.0 -m "Release version 1.0.0"
|
||||
git push origin v1.0.0
|
||||
```
|
||||
|
||||
## 提交信息规范
|
||||
|
||||
使用 Conventional Commits 规范:
|
||||
|
||||
| 类型 | 说明 | 示例 |
|
||||
|------|------|------|
|
||||
| `feat` | 新功能 | `feat: add user login` |
|
||||
| `fix` | 修复bug | `fix: correct i18n loading` |
|
||||
| `docs` | 文档变更 | `docs: update README` |
|
||||
| `style` | 代码格式 | `style: format code` |
|
||||
| `refactor` | 重构 | `refactor: simplify API` |
|
||||
| `test` | 测试 | `test: add unit tests` |
|
||||
| `chore` | 构建/工具 | `chore: update dependencies` |
|
||||
|
||||
格式:
|
||||
```
|
||||
<type>(<scope>): <subject>
|
||||
|
||||
<body>
|
||||
|
||||
<footer>
|
||||
```
|
||||
|
||||
## 当前分支状态
|
||||
|
||||
| 分支 | 状态 | 说明 |
|
||||
|------|------|------|
|
||||
| `master` | ✅ 稳定 | 生产代码(含5语言支持) |
|
||||
| `develop` | ✅ 活跃 | 主开发分支(含5语言支持) |
|
||||
|
||||
## 常用命令
|
||||
|
||||
```bash
|
||||
# 查看所有分支
|
||||
git branch -a
|
||||
|
||||
# 查看当前分支
|
||||
git branch --show-current
|
||||
|
||||
# 切换分支
|
||||
git checkout <branch-name>
|
||||
|
||||
# 创建并切换到新分支
|
||||
git checkout -b <new-branch>
|
||||
|
||||
# 删除本地分支
|
||||
git branch -d <branch-name>
|
||||
|
||||
# 删除远程分支
|
||||
git push origin --delete <branch-name>
|
||||
|
||||
# 查看分支差异
|
||||
git diff develop
|
||||
|
||||
# 查看分支树
|
||||
git log --graph --oneline --all
|
||||
```
|
||||
|
||||
## 发布流程
|
||||
|
||||
1. **开发阶段**:在 `develop` 分支进行开发
|
||||
2. **测试阶段**:创建 `release` 分支进行测试
|
||||
3. **发布阶段**:合并到 `master` 并打标签
|
||||
4. **维护阶段**:使用 `hotfix` 分支修复紧急问题
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **永远不要直接在 master 分支开发**
|
||||
2. **功能分支必须从 develop 分支创建**
|
||||
3. **提交前先拉取最新代码**
|
||||
4. **保持提交信息清晰规范**
|
||||
5. **及时删除已合并的功能分支**
|
||||
|
||||
## 远程仓库
|
||||
|
||||
- 仓库地址:http://219.157.255.213:25311/xinyouhe/x-server-mobile.git
|
||||
- 默认分支:develop(建议设置)
|
||||
- Pull Request:http://219.157.255.213:25311/xinyouhe/x-server-mobile/compare/master...develop
|
||||
@@ -0,0 +1,601 @@
|
||||
# 企业内部工具系统 - 实现计划
|
||||
|
||||
## 项目概述
|
||||
|
||||
开发一个企业内部工具系统平台,以出差审批为首个功能模块,后续可持续扩展新功能。系统主要支持移动 H5,嵌入飞书应用中使用。
|
||||
|
||||
### 核心目标
|
||||
- **H5 移动端优先**:专注于移动端体验,PC 端后期扩展
|
||||
- 从工具台层面设计可扩展的页面结构
|
||||
- **审批模块作为独立通用工具**:支持多样化审批场景,业务可复用
|
||||
- 良好的飞书集成能力(多公司、多应用架构)
|
||||
|
||||
---
|
||||
|
||||
## 技术方案
|
||||
|
||||
### 技术架构
|
||||
|
||||
#### 前端技术栈(H5 移动端)
|
||||
| 技术 | 用途 | 说明 |
|
||||
|------|------|------|
|
||||
| React 18 | 前端框架 | 配合 TypeScript |
|
||||
| Ant Design Mobile | UI 组件库 | 移动端组件库 |
|
||||
| Vant | 补充组件 | 必要时补充使用 |
|
||||
| Vite | 构建工具 | 快速开发体验 |
|
||||
| React Router | 路由管理 | 支持多页面应用 |
|
||||
| Zustand / Jotai | 状态管理 | 轻量级方案 |
|
||||
| React Query | 数据请求 | 服务端状态管理 |
|
||||
| PostCSS + pxtorem | 移动端适配 | rem 转换 |
|
||||
| VConsole | 调试工具 | 移动端调试 |
|
||||
|
||||
#### 后端技术栈
|
||||
| 技术 | 用途 | 说明 |
|
||||
|------|------|------|
|
||||
| Webman | PHP 框架 | 高性能常驻内存框架 |
|
||||
| MySQL | 数据库 | 关系型数据库 |
|
||||
| Redis | 缓存 | 会话、缓存存储 |
|
||||
| JWT | 认证 | Token 认证机制 |
|
||||
|
||||
### 多租户架构设计
|
||||
|
||||
**核心概念**:
|
||||
- 系统**不按公司隔离用户**,用户全局唯一
|
||||
- 用户关联所属公司,用于选择对应飞书应用配置
|
||||
- 审批流程跨公司,只识别人员
|
||||
- 飞书仅提供:系统入口 + 人员识别鉴权
|
||||
|
||||
**数据模型**:
|
||||
|
||||
```sql
|
||||
-- 公司表(飞书应用配置)
|
||||
CREATE TABLE company (
|
||||
id BIGINT PRIMARY KEY,
|
||||
name VARCHAR(100),
|
||||
feishu_app_id VARCHAR(100),
|
||||
feishu_app_secret VARCHAR(255),
|
||||
feishu_tenant_key VARCHAR(100),
|
||||
status TINYINT DEFAULT 1
|
||||
);
|
||||
|
||||
-- 用户表(全局用户)
|
||||
CREATE TABLE user (
|
||||
id BIGINT PRIMARY KEY,
|
||||
company_id BIGINT, -- 所属公司
|
||||
feishu_user_id VARCHAR(100), -- 飞书用户ID
|
||||
feishu_union_id VARCHAR(100),-- 飞书UnionID(跨应用唯一)
|
||||
name VARCHAR(50),
|
||||
email VARCHAR(100),
|
||||
phone VARCHAR(20),
|
||||
avatar VARCHAR(255),
|
||||
status TINYINT DEFAULT 1
|
||||
);
|
||||
|
||||
-- 用户与飞书应用关联(支持用户切换不同公司应用)
|
||||
CREATE TABLE user_feishu_binding (
|
||||
id BIGINT PRIMARY KEY,
|
||||
user_id BIGINT,
|
||||
company_id BIGINT,
|
||||
feishu_open_id VARCHAR(100),
|
||||
created_at TIMESTAMP
|
||||
);
|
||||
```
|
||||
|
||||
### 项目目录结构(前后端分离)
|
||||
|
||||
```
|
||||
x-server-mobile/ # 项目根目录
|
||||
├── frontend/ # 前端项目(React + Vite)
|
||||
│ ├── src/
|
||||
│ │ ├── components/ # 通用组件
|
||||
│ │ │ ├── approval/ # 审批组件(可复用)
|
||||
│ │ │ ├── layout/ # 布局组件
|
||||
│ │ │ └── common/ # 通用组件
|
||||
│ │ ├── pages/ # 页面
|
||||
│ │ │ ├── dashboard/ # 工具台首页
|
||||
│ │ │ ├── business-trip/ # 出差审批
|
||||
│ │ │ └── profile/ # 个人中心
|
||||
│ │ ├── hooks/ # 自定义 Hooks
|
||||
│ │ ├── services/ # API 服务
|
||||
│ │ ├── stores/ # 状态管理
|
||||
│ │ ├── utils/ # 工具函数
|
||||
│ │ ├── types/ # TypeScript 类型
|
||||
│ │ └── config/ # 配置文件
|
||||
│ ├── public/
|
||||
│ ├── package.json
|
||||
│ └── vite.config.ts
|
||||
│
|
||||
└── backend/ # 后端项目(Webman)
|
||||
├── app/
|
||||
│ ├── controller/
|
||||
│ ├── model/
|
||||
│ ├── service/
|
||||
│ ├── middleware/
|
||||
│ └── util/
|
||||
├── config/
|
||||
├── routes/
|
||||
├── support/
|
||||
├── composer.json
|
||||
└── start.php
|
||||
```
|
||||
|
||||
### 后端项目结构(Webman)
|
||||
```
|
||||
backend/
|
||||
├── app/
|
||||
│ ├── controller/
|
||||
│ │ ├── ApprovalController.php # 审批控制器
|
||||
│ │ ├── BusinessTripController.php # 出差审批控制器
|
||||
│ │ ├── UserController.php # 用户控制器
|
||||
│ │ ├── CompanyController.php # 公司管理控制器
|
||||
│ │ └── BaseController.php # 基础控制器
|
||||
│ ├── model/
|
||||
│ │ ├── Approval.php # 审批模型
|
||||
│ │ ├── ApprovalNode.php # 审批节点模型
|
||||
│ │ ├── BusinessTrip.php # 出差模型
|
||||
│ │ ├── User.php # 用户模型
|
||||
│ │ ├── Company.php # 公司模型
|
||||
│ │ └── UserFeishuBinding.php # 用户飞书绑定模型
|
||||
│ ├── service/
|
||||
│ │ ├── ApprovalService.php # 审批服务
|
||||
│ │ ├── FeishuService.php # 飞书服务(多应用)
|
||||
│ │ ├── FeishuAppManager.php # 飞书应用配置管理
|
||||
│ │ ├── UserService.php # 用户服务
|
||||
│ │ └── CompanyService.php # 公司服务
|
||||
│ ├── middleware/
|
||||
│ │ ├── AuthMiddleware.php # 认证中间件
|
||||
│ │ ├── CompanyResolveMiddleware.php # 公司识别中间件
|
||||
│ │ └── CorsMiddleware.php # 跨域中间件
|
||||
│ └── util/
|
||||
│ ├── JwtUtil.php # JWT工具
|
||||
│ └── Response.php # 响应封装
|
||||
├── config/
|
||||
│ ├── route.php # 路由配置
|
||||
│ ├── database.php # 数据库配置
|
||||
│ ├── redis.php # Redis配置
|
||||
│ └── feishu.php # 飞书应用配置模板
|
||||
├── routes/
|
||||
│ └── api.php # API路由定义
|
||||
└── support/
|
||||
└── Bootstrap.php # 启动文件
|
||||
```
|
||||
|
||||
### API 接口设计
|
||||
|
||||
#### 通用 API 规范
|
||||
```
|
||||
基础URL: /api/v1
|
||||
响应格式:
|
||||
{
|
||||
"code": 0, // 0表示成功
|
||||
"message": "success",
|
||||
"data": {}
|
||||
}
|
||||
```
|
||||
|
||||
#### 审批相关接口
|
||||
| 方法 | 路径 | 说明 |
|
||||
|------|------|------|
|
||||
| GET | /approval/list | 获取审批列表 |
|
||||
| GET | /approval/detail/:id | 获取审批详情 |
|
||||
| POST | /approval/submit | 提交审批申请 |
|
||||
| POST | /approval/approve/:id | 审批通过 |
|
||||
| POST | /approval/reject/:id | 审批拒绝 |
|
||||
| POST | /approval/cancel/:id | 撤销申请 |
|
||||
| GET | /approval/flow/:type | 获取审批流程配置 |
|
||||
|
||||
#### 出差审批接口
|
||||
| 方法 | 路径 | 说明 |
|
||||
|------|------|------|
|
||||
| GET | /business-trip/list | 获取出差列表 |
|
||||
| GET | /business-trip/detail/:id | 获取出差详情 |
|
||||
| POST | /business-trip/apply | 提交出差申请 |
|
||||
| PUT | /business-trip/:id | 更新出差申请(草稿状态) |
|
||||
|
||||
#### 用户认证接口(多飞书应用)
|
||||
| 方法 | 路径 | 说明 |
|
||||
|------|------|------|
|
||||
| POST | /auth/login | 用户登录 |
|
||||
| POST | /auth/feishu-login | 飞书免登(自动识别公司应用) |
|
||||
| GET | /user/info | 获取用户信息 |
|
||||
| GET | /user/menu | 获取用户菜单权限 |
|
||||
| GET | /user/companies | 获取用户可切换的公司列表 |
|
||||
| POST | /user/switch-company | 切换当前公司 |
|
||||
|
||||
#### 飞书应用管理接口(管理端)
|
||||
| 方法 | 路径 | 说明 |
|
||||
|------|------|------|
|
||||
| GET | /admin/company/list | 获取公司列表 |
|
||||
| POST | /admin/company/add | 添加公司飞书应用配置 |
|
||||
| PUT | /admin/company/:id | 更新公司飞书配置 |
|
||||
| DELETE | /admin/company/:id | 删除公司配置 |
|
||||
|
||||
---
|
||||
|
||||
## 功能模块设计
|
||||
|
||||
### 1. 工具台架构(Dashboard - H5 移动端)
|
||||
|
||||
**设计理念**:工具台作为统一入口,支持模块化扩展,专注于移动端体验
|
||||
|
||||
```
|
||||
工具台首页(H5)
|
||||
├── 顶部导航栏
|
||||
│ ├── Logo + 系统名称
|
||||
│ ├── 搜索框(全局搜索)
|
||||
│ ├── 通知图标
|
||||
│ └── 用户头像
|
||||
├── 主内容区
|
||||
│ ├── 待办事项卡片(横向滚动)
|
||||
│ ├── 快捷入口(九宫格/宫格)
|
||||
│ ├── 数据统计卡片
|
||||
│ └── 最新动态列表
|
||||
└── 底部导航栏(TabBar)
|
||||
├── 首页
|
||||
├── 应用(宫格展开所有工具)
|
||||
├── 消息
|
||||
└── 我的
|
||||
```
|
||||
|
||||
**移动端适配要点**:
|
||||
- 使用 TabBar 替代侧边栏(符合移动端习惯)
|
||||
- 750px 设计稿 → rem 自适应转换
|
||||
- 触摸手势支持(下拉刷新、上拉加载)
|
||||
- 安全区域适配(刘海屏、底部横条)
|
||||
|
||||
**关键文件**:
|
||||
- `src/pages/dashboard/index.tsx` - 工具台主页
|
||||
- `src/components/layout/MobileLayout.tsx` - 移动端主布局
|
||||
- `src/components/layout/TabBar.tsx` - 底部导航栏
|
||||
- `src/components/layout/Header.tsx` - 顶部栏
|
||||
|
||||
### 2. 通用审批模块设计(独立工具)
|
||||
|
||||
**设计理念**:审批模块作为独立的通用工具,与具体业务解耦,支持多样化的审批场景
|
||||
|
||||
**核心特性**:
|
||||
- ✅ 业务无关:不绑定特定业务表单和流程
|
||||
- ✅ 流程灵活:支持单级、多级、会签、或签、抄送等任意组合
|
||||
- ✅ 动态配置:审批流程可动态配置和调整
|
||||
- ✅ 易于集成:任何业务都可快速接入审批模块
|
||||
|
||||
**核心数据模型**:
|
||||
|
||||
```typescript
|
||||
// 审批流程节点类型
|
||||
enum ApprovalNodeType {
|
||||
APPROVER = 'approver', // 审批人(单人)
|
||||
AND_APPROVE = 'and_approve', // 会签(需所有人通过)
|
||||
OR_APPROVE = 'or_approve', // 或签(任一人通过)
|
||||
NOTIFY = 'notify' // 抄送(通知用)
|
||||
}
|
||||
|
||||
// 审批单状态
|
||||
enum ApprovalStatus {
|
||||
DRAFT = 'draft', // 草稿
|
||||
PENDING = 'pending', // 审批中
|
||||
APPROVED = 'approved', // 已通过
|
||||
REJECTED = 'rejected', // 已拒绝
|
||||
CANCELLED = 'cancelled' // 已撤销
|
||||
}
|
||||
|
||||
// 审批流程配置(业务无关)
|
||||
interface ApprovalFlow {
|
||||
id: string;
|
||||
businessType: string; // 业务类型标识(如:business-trip, purchase)
|
||||
name: string;
|
||||
nodes: ApprovalNode[]; // 流程节点
|
||||
}
|
||||
|
||||
// 审批节点
|
||||
interface ApprovalNode {
|
||||
id: string;
|
||||
type: ApprovalNodeType;
|
||||
approvers: User[]; // 审批人列表
|
||||
order: number; // 节点顺序
|
||||
conditions?: any; // 条件判断(可选,用于条件分支)
|
||||
}
|
||||
|
||||
// 审批单(业务通用)
|
||||
interface ApprovalOrder {
|
||||
id: string;
|
||||
businessType: string; // 业务类型
|
||||
businessId: string; // 业务单据ID
|
||||
title: string; // 审批标题
|
||||
applicant: User; // 申请人
|
||||
status: ApprovalStatus;
|
||||
currentNodes: string[]; // 当前待审批节点ID
|
||||
flowId: string; // 使用的流程ID
|
||||
formData: any; // 业务表单数据(JSON存储)
|
||||
attachments: Attachment[]; // 附件
|
||||
timeline: ApprovalTimeline[]; // 审批轨迹
|
||||
}
|
||||
```
|
||||
|
||||
**审批模块架构**:
|
||||
|
||||
```
|
||||
src/modules/approval/ # 审批模块(独立)
|
||||
├── components/ # UI组件
|
||||
│ ├── ApprovalFlow.tsx # 流程可视化
|
||||
│ ├── ApprovalDetail.tsx # 审批详情
|
||||
│ ├── ApprovalList.tsx # 审批列表
|
||||
│ └── ApprovalAction.tsx # 审批操作
|
||||
├── hooks/ # 自定义Hooks
|
||||
│ ├── useApprovalSubmit.ts # 提交审批
|
||||
│ ├── useApprovalAction.ts # 审批操作
|
||||
│ └── useApprovalList.ts # 获取列表
|
||||
├── services/ # API服务
|
||||
│ └── approval.ts
|
||||
├── types/ # 类型定义
|
||||
│ └── index.ts
|
||||
└── utils/ # 工具函数
|
||||
├── flowParser.ts # 流程解析
|
||||
└── statusCalculator.ts # 状态计算
|
||||
```
|
||||
|
||||
**业务接入示例(出差审批)**:
|
||||
|
||||
```typescript
|
||||
// 业务模块只需要调用审批模块的Hook
|
||||
import { useApprovalSubmit } from '@/modules/approval/hooks'
|
||||
|
||||
function BusinessTripApply() {
|
||||
const { submitApproval } = useApprovalSubmit('business-trip')
|
||||
|
||||
const handleSubmit = async (formData: BusinessTripFormData) => {
|
||||
// 1. 先保存业务数据
|
||||
const businessTripId = await saveBusinessTrip(formData)
|
||||
|
||||
// 2. 提交审批(传入业务类型和业务ID)
|
||||
await submitApproval({
|
||||
businessId: businessTripId,
|
||||
title: `出差申请 - ${formData.destination}`,
|
||||
formData: formData,
|
||||
flowType: 'default' // 使用的流程模板
|
||||
})
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 出差审批模块
|
||||
|
||||
**页面结构**:
|
||||
```
|
||||
出差审批
|
||||
├── 申请列表页
|
||||
│ ├── 我发起的
|
||||
│ ├── 待我审批
|
||||
│ ├── 我已审批
|
||||
│ └── 抄送我的
|
||||
├── 申请表单页
|
||||
│ ├── 基本信息(出差事由、时间、地点)
|
||||
│ ├── 行程明细
|
||||
│ ├── 费用预算
|
||||
│ └── 附件上传
|
||||
└── 审批详情页
|
||||
├── 表单内容展示
|
||||
├── 流程进度可视化
|
||||
├── 审批历史
|
||||
└── 操作按钮
|
||||
```
|
||||
|
||||
**关键文件**:
|
||||
- `src/pages/business-trip/list.tsx` - 列表页
|
||||
- `src/pages/business-trip/apply.tsx` - 申请表单
|
||||
- `src/pages/business-trip/detail.tsx` - 详情页
|
||||
- `src/pages/business-trip/types.ts` - 出差相关类型
|
||||
|
||||
### 4. 飞书集成(多应用架构)
|
||||
|
||||
**集成要点**:
|
||||
|
||||
#### 后端 - 多飞书应用管理
|
||||
```php
|
||||
// 飞书应用管理器
|
||||
class FeishuAppManager
|
||||
{
|
||||
// 根据飞书租户Key获取公司配置
|
||||
public function getCompanyByTenantKey(string $tenantKey): Company
|
||||
{
|
||||
// 从数据库查询对应的飞书应用配置
|
||||
}
|
||||
|
||||
// 获取指定公司的飞书 access_token
|
||||
public function getAccessToken(int $companyId): string
|
||||
{
|
||||
// 缓存 access_token,过期自动刷新
|
||||
}
|
||||
|
||||
// 根据用户ID获取其所属公司的飞书配置
|
||||
public function getUserFeishuConfig(int $userId): array
|
||||
{
|
||||
// 返回用户所属公司的飞书 AppID、AppSecret 等
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 前端 - 飞书 SDK 封装
|
||||
```typescript
|
||||
// src/utils/feishu.ts
|
||||
export const FeishuSDK = {
|
||||
// 获取免登授权码
|
||||
getAuthCode: () => { /* ... */ },
|
||||
|
||||
// 判断是否在飞书环境
|
||||
isFeishuEnv: () => { /* ... */ },
|
||||
|
||||
// 获取当前租户信息(用于后端识别公司)
|
||||
getTenantContext: () => {
|
||||
// 从飞书容器获取 tenant_key 或其他标识
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 飞书免登流程(多应用)
|
||||
```
|
||||
1. 用户在飞书中打开应用
|
||||
2. 前端获取 auth_code 和 tenant_key
|
||||
3. 调用后端 /auth/feishu-login
|
||||
4. 后端根据 tenant_key 查询对应公司配置
|
||||
5. 使用该公司的 AppSecret 换取用户信息
|
||||
6. 生成 JWT Token(包含 user_id 和 company_id)
|
||||
7. 返回登录信息
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 实施步骤
|
||||
|
||||
### 阶段一:项目初始化
|
||||
**前端:**
|
||||
1. 初始化 Vite + React + TypeScript 项目
|
||||
2. 安装 Ant Design 及相关依赖
|
||||
3. 配置路由、状态管理、请求库(axios + React Query)
|
||||
4. 配置 Tailwind CSS
|
||||
|
||||
**后端:**
|
||||
1. 安装 Webman 框架
|
||||
2. 配置 MySQL、Redis 连接
|
||||
3. 配置 JWT 认证
|
||||
4. 设置 CORS 中间件
|
||||
5. **创建多租户数据表(company, user, user_feishu_binding)**
|
||||
|
||||
### 阶段二:工具台框架(H5)
|
||||
**前端:**
|
||||
1. 实现 MobileLayout 移动端布局组件
|
||||
2. 实现 TabBar 底部导航栏
|
||||
3. 实现 Header 组件
|
||||
4. 创建工具台首页 Dashboard(H5 适配)
|
||||
5. 配置 rem 移动端适配
|
||||
|
||||
**后端:**
|
||||
1. 实现 /auth/login 登录接口
|
||||
2. **实现 /auth/feishu-login 飞书免登(多应用识别)**
|
||||
3. 实现 /user/info 用户信息接口
|
||||
4. 实现 /user/menu 菜单权限接口
|
||||
5. **实现 FeishuAppManager 服务(多应用配置管理)**
|
||||
6. **实现 CompanyResolveMiddleware 中间件**
|
||||
|
||||
### 阶段三:独立审批模块(通用工具)
|
||||
**前端:**
|
||||
1. 创建 `src/modules/approval/` 独立模块目录
|
||||
2. 定义审批相关 TypeScript 类型(业务无关)
|
||||
3. 实现 ApprovalFlow 流程可视化组件
|
||||
4. 实现 ApprovalDetail 详情页(通用)
|
||||
5. 实现 ApprovalList 列表页(通用)
|
||||
6. 实现 ApprovalAction 操作组件
|
||||
7. **封装审批 Hooks(useApprovalSubmit, useApprovalAction)**
|
||||
8. **提供业务接入示例**
|
||||
|
||||
**后端:**
|
||||
1. 设计审批流程数据表(approval_flow, approval_node)
|
||||
2. 设计审批单数据表(approval_order, approval_record)
|
||||
3. 实现审批服务层逻辑(单级、多级、会签、或签)
|
||||
4. **确保业务无关性,通过 businessType + businessId 关联业务**
|
||||
5. 实现审批相关 API 接口
|
||||
|
||||
### 阶段四:出差审批业务(作为审批模块的使用示例)
|
||||
**前端:**
|
||||
1. 创建出差申请表单页面
|
||||
2. **调用审批模块的 useApprovalSubmit Hook**
|
||||
3. 创建出差审批列表(复用审批模块组件)
|
||||
4. 创建出差审批详情(复用审批模块组件 + 业务表单展示)
|
||||
|
||||
**后端:**
|
||||
1. 设计出差数据表(business_trip, business_trip_itinerary)
|
||||
2. 实现出差业务逻辑(与审批解耦)
|
||||
3. 实现出差相关 API 接口
|
||||
4. **配置出差审批的默认流程模板**
|
||||
|
||||
### 阶段五:飞书集成与 H5 适配(多应用架构)
|
||||
**前端:**
|
||||
1. 集成飞书 JSAPI SDK
|
||||
2. 实现飞书免登逻辑(获取 tenant_key)
|
||||
3. **H5 移动端样式优化**
|
||||
4. **配置 pxtorem(750px 设计稿适配)**
|
||||
5. **安全区域适配(刘海屏、底部横条)**
|
||||
6. 集成 VConsole 调试工具
|
||||
|
||||
**后端:**
|
||||
1. **实现 FeishuService(支持多应用配置)**
|
||||
2. **实现飞书应用配置管理接口(增删改查)**
|
||||
3. **实现用户切换公司功能**
|
||||
4. **实现飞书消息推送(根据用户所属公司推送)**
|
||||
|
||||
---
|
||||
|
||||
## 待确认问题
|
||||
|
||||
1. **飞书应用配置**:是否有飞书应用的 App ID、App Secret?
|
||||
2. **权限设计**:是否需要 RBAC 权限系统?
|
||||
3. **数据 Mock**:开发阶段是否需要 Mock 数据?
|
||||
4. **部署环境**:开发/测试/生产环境配置?
|
||||
|
||||
---
|
||||
|
||||
## 关键文件清单
|
||||
|
||||
### 前端核心文件(H5)
|
||||
| 文件路径 | 说明 |
|
||||
|---------|------|
|
||||
| `src/App.tsx` | 应用入口 |
|
||||
| `src/main.tsx` | React 挂载点 |
|
||||
| `src/router/index.tsx` | 路由配置 |
|
||||
| `src/components/layout/MobileLayout.tsx` | 移动端主布局 |
|
||||
| `src/components/layout/TabBar.tsx` | 底部导航栏 |
|
||||
| `src/components/layout/Header.tsx` | 顶部栏 |
|
||||
| `src/pages/dashboard/index.tsx` | 工具台首页 |
|
||||
| `src/modules/approval/` | **审批模块(独立)** |
|
||||
| `src/pages/business-trip/` | 出差审批业务 |
|
||||
| `src/services/api.ts` | API 基础配置 |
|
||||
| `src/utils/feishu.ts` | 飞书 SDK 封装 |
|
||||
| `src/config/rem.ts` | rem 转换配置 |
|
||||
| `package.json` | 项目依赖 |
|
||||
| `vite.config.ts` | Vite 配置 |
|
||||
| `tsconfig.json` | TypeScript 配置 |
|
||||
| `postcss.config.js` | PostCSS 配置(pxtorem) |
|
||||
|
||||
### 后端核心文件
|
||||
| 文件路径 | 说明 |
|
||||
|---------|------|
|
||||
| `app/controller/BaseController.php` | 基础控制器 |
|
||||
| `app/controller/ApprovalController.php` | 审批控制器 |
|
||||
| `app/controller/BusinessTripController.php` | 出差审批控制器 |
|
||||
| `app/controller/UserController.php` | 用户控制器 |
|
||||
| `app/model/Approval.php` | 审批模型 |
|
||||
| `app/model/ApprovalNode.php` | 审批节点模型 |
|
||||
| `app/service/ApprovalService.php` | 审批服务 |
|
||||
| `app/service/FeishuService.php` | 飞书服务 |
|
||||
| `app/middleware/AuthMiddleware.php` | 认证中间件 |
|
||||
| `config/route.php` | 路由配置 |
|
||||
| `config/database.php` | 数据库配置 |
|
||||
| `routes/api.php` | API 路由定义 |
|
||||
|
||||
---
|
||||
|
||||
## 验证计划
|
||||
|
||||
### 前端验证(H5)
|
||||
1. **运行项目**:`npm run dev` 确保项目启动
|
||||
2. **路由测试**:访问各个页面确保路由正常
|
||||
3. **审批模块测试**:验证审批组件的通用性和可复用性
|
||||
4. **H5 适配测试**:
|
||||
- 测试不同屏幕尺寸(375px - 768px)
|
||||
- 测试刘海屏和底部横条适配
|
||||
- 测试触摸手势交互
|
||||
5. **飞书环境测试**:
|
||||
- 在飞书中测试免登
|
||||
- 在飞书中测试 H5 适配
|
||||
- 测试多公司应用切换
|
||||
6. **真机测试**:在真实移动设备上测试
|
||||
|
||||
### 后端验证
|
||||
1. **启动 Webman**:`php start.php start` 确保服务启动
|
||||
2. **API 测试**:使用 Postman/Apifox 测试各接口
|
||||
3. **数据库测试**:验证数据表创建和 CRUD 操作
|
||||
4. **认证测试**:验证 JWT 认证流程
|
||||
5. **多租户测试**:
|
||||
- 测试多公司飞书应用配置
|
||||
- 测试跨公司审批流程
|
||||
6. **飞书集成测试**:验证飞书免登和消息推送
|
||||
+356
@@ -0,0 +1,356 @@
|
||||
# X-Server Mobile 企业工具系统
|
||||
|
||||
## 项目概述
|
||||
|
||||
一个基于飞书的企业内部H5移动工具平台,采用多租户架构,支持审批流程引擎和多种业务应用。
|
||||
|
||||
**技术栈:**
|
||||
- 前端:React 18 + TypeScript + Vite + Ant Design Mobile
|
||||
- 后端:PHP 8.5 + Webman 2.x + MySQL + Redis
|
||||
- 部署:Docker + OpenResty + frpc (开发环境)
|
||||
|
||||
**支持语言:**
|
||||
- 🇨🇳 简体中文 (zh)
|
||||
- 🇬🇧 English (en)
|
||||
- 🇷🇺 Русский (ru)
|
||||
- 🇪🇸 Español (es)
|
||||
- 🇫🇷 Français (fr)
|
||||
|
||||
## 国际化 (i18n)
|
||||
|
||||
### 前端国际化
|
||||
|
||||
**位置:** `frontend/src/i18n/`
|
||||
|
||||
**使用方式:**
|
||||
```tsx
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
function MyComponent() {
|
||||
const { t } = useTranslation()
|
||||
return <div>{t('common.confirm')}</div>
|
||||
}
|
||||
```
|
||||
|
||||
**切换语言:**
|
||||
```tsx
|
||||
import { showLanguageSwitcher } from '@/components/LanguageSwitcher'
|
||||
|
||||
// 显示语言选择弹窗
|
||||
showLanguageSwitcher()
|
||||
```
|
||||
|
||||
**语言存储:** 浏览器 localStorage,键名为 `i18nextLng`
|
||||
|
||||
### 后端国际化
|
||||
|
||||
**位置:** `backend/lang/`
|
||||
|
||||
**使用方式:**
|
||||
```php
|
||||
use App\Util\Lang;
|
||||
|
||||
// 获取翻译
|
||||
$message = Lang::get('common.confirm');
|
||||
|
||||
// 带参数的翻译
|
||||
$message = Lang::get('approval.status', ['status' => 'pending']);
|
||||
|
||||
// 检查翻译是否存在
|
||||
if (Lang::has('common.submit')) {
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
**设置语言:**
|
||||
```php
|
||||
// 从请求检测语言
|
||||
$locale = Lang::detectFromRequest();
|
||||
Lang::setLocale($locale);
|
||||
```
|
||||
|
||||
## 项目关键特性
|
||||
|
||||
### 1. 多租户架构
|
||||
|
||||
- 用户全局唯一,关联多个公司
|
||||
- 租户切换功能
|
||||
- 公司级数据隔离
|
||||
|
||||
### 2. 审批流程引擎
|
||||
|
||||
**支持节点类型:**
|
||||
- 单人审批 (single_approve)
|
||||
- 会签 (and_sign) - 所有人通过
|
||||
- 或签 (or_sign) - 任一人通过
|
||||
- 通知节点 (notify)
|
||||
|
||||
**审批状态:**
|
||||
- pending: 待审批
|
||||
- approved: 已通过
|
||||
- rejected: 已拒绝
|
||||
- cancelled: 已撤销
|
||||
|
||||
**关键文件:**
|
||||
- `backend/app/Service/ApprovalService.php` - 审批核心逻辑
|
||||
- `backend/app/Model/ApprovalOrder.php` - 审批单模型
|
||||
- `frontend/src/modules/approval/` - 前端审批模块
|
||||
|
||||
### 3. 飞书集成
|
||||
|
||||
**飞书免登:**
|
||||
- 回调地址:`/api/v1/auth/feishu-login`
|
||||
- 配置项:
|
||||
- `FEISHU_APP_ID` - 飞书应用 ID
|
||||
- `FEISHU_APP_SECRET` - 飞书应用密钥
|
||||
- `FEISHU_ENCRYPT_KEY` - 加密密钥
|
||||
- `FEISHU_VERIFICATION_TOKEN` - 验证令牌
|
||||
|
||||
### 4. 数据库设计
|
||||
|
||||
**核心表:**
|
||||
- `users` - 用户表(全局唯一)
|
||||
- `companies` - 公司表
|
||||
- `user_company` - 用户公司关联表
|
||||
- `approval_flows` - 审批流程定义
|
||||
- `approval_nodes` - 审批节点
|
||||
- `approval_orders` - 审批单
|
||||
- `approval_records` - 审批记录
|
||||
- `business_trips` - 出差申请
|
||||
- `business_trip_items` - 出差明细
|
||||
|
||||
## 目录结构
|
||||
|
||||
```
|
||||
x-server-mobile/
|
||||
├── frontend/ # React 前端
|
||||
│ ├── src/
|
||||
│ │ ├── i18n/ # 国际化配置
|
||||
│ │ │ ├── locales/ # 翻译文件
|
||||
│ │ │ └── index.ts
|
||||
│ │ ├── components/ # 通用组件
|
||||
│ │ │ ├── layout/ # 布局组件
|
||||
│ │ │ └── LanguageSwitcher/
|
||||
│ │ ├── modules/ # 业务模块
|
||||
│ │ │ └── approval/ # 审批模块
|
||||
│ │ ├── pages/ # 页面组件
|
||||
│ │ ├── stores/ # 状态管理
|
||||
│ │ ├── router/ # 路由配置
|
||||
│ │ └── utils/ # 工具函数
|
||||
│ └── package.json
|
||||
├── backend/ # PHP 后端
|
||||
│ ├── app/
|
||||
│ │ ├── Controller/ # 控制器
|
||||
│ │ ├── Model/ # 数据模型
|
||||
│ │ ├── Service/ # 业务逻辑
|
||||
│ │ ├── Util/ # 工具类
|
||||
│ │ │ ├── Lang.php # 国际化工具
|
||||
│ │ │ └── Db.php # 数据库连接
|
||||
│ │ └── Middleware/ # 中间件
|
||||
│ ├── config/ # 配置文件
|
||||
│ ├── lang/ # 语言文件
|
||||
│ ├── database/migrations/ # 数据库迁移
|
||||
│ ├── composer.json
|
||||
│ └── Dockerfile
|
||||
├── docker-compose.yml # Docker 编排
|
||||
├── nginx/ # OpenResty 配置
|
||||
├── frpc/ # frpc 配置
|
||||
└── IMPLEMENTATION_PLAN.md # 实现计划
|
||||
```
|
||||
|
||||
## 开发环境设置
|
||||
|
||||
### 端口分配
|
||||
|
||||
| 服务 | 端口 |
|
||||
|------|------|
|
||||
| Frontend (Vite) | 5173 |
|
||||
| Backend (Webman) | 8787 |
|
||||
| OpenResty | 8080 |
|
||||
| MySQL | 3306 |
|
||||
| Redis | 6379 |
|
||||
|
||||
### 环境变量
|
||||
|
||||
**前端 (frontend/.env):**
|
||||
```env
|
||||
VITE_API_BASE_URL=http://localhost:8787/api/v1
|
||||
```
|
||||
|
||||
**后端 (backend/.env):**
|
||||
```env
|
||||
DB_HOST=mysql
|
||||
DB_PORT=3306
|
||||
DB_DATABASE=x_server_mobile
|
||||
DB_USERNAME=root
|
||||
DB_PASSWORD=mysql_xEsdPa
|
||||
|
||||
REDIS_HOST=redis
|
||||
REDIS_PORT=6379
|
||||
|
||||
APP_DEBUG=true
|
||||
|
||||
# 飞书配置
|
||||
FEISHU_APP_ID=cli_xxxxxxxxxxxxx
|
||||
FEISHU_APP_SECRET=xxxxxxxxxxxxxxxxxxxx
|
||||
FEISHU_ENCRYPT_KEY=xxxxxxxxxxxxxxxxxxxx
|
||||
FEISHU_VERIFICATION_TOKEN=xxxxxxxxxxxxxxxxxxxx
|
||||
```
|
||||
|
||||
### 反向代理配置(开发环境)
|
||||
|
||||
**frpc 配置:** `frpc/frpc-dev.ini`
|
||||
```ini
|
||||
[common]
|
||||
server_addr = your-frp-server.com
|
||||
server_port = 7000
|
||||
token = your-frp-token
|
||||
|
||||
[x-server-api-dev]
|
||||
type = http
|
||||
local_ip = 127.0.0.1
|
||||
local_port = 8080
|
||||
custom_domains = api-dev.x-server.your-domain.com
|
||||
```
|
||||
|
||||
**OpenResty 配置:** `nginx/x-server-api.conf`
|
||||
```nginx
|
||||
server {
|
||||
listen 8080;
|
||||
server_name api.x-server.local;
|
||||
|
||||
location /api/ {
|
||||
proxy_pass http://127.0.0.1:8787/api/;
|
||||
|
||||
add_header Access-Control-Allow-Origin *;
|
||||
add_header Access-Control-Allow-Methods 'GET, POST, PUT, DELETE, OPTIONS';
|
||||
add_header Access-Control-Allow-Headers 'DNT,X-Mx-ReqToken,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization,X-Tenant-Key';
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## API 接口
|
||||
|
||||
### 认证接口
|
||||
|
||||
- `POST /api/v1/auth/login` - 用户名密码登录
|
||||
- `POST /api/v1/auth/feishu-login` - 飞书免登
|
||||
|
||||
### 用户接口
|
||||
|
||||
- `GET /api/v1/user/info` - 获取用户信息
|
||||
- `GET /api/v1/user/menu` - 获取用户菜单
|
||||
- `GET /api/v1/user/companies` - 获取公司列表
|
||||
- `POST /api/v1/user/switch-company` - 切换公司
|
||||
|
||||
### 审批接口
|
||||
|
||||
- `GET /api/v1/approval/list` - 审批列表
|
||||
- `GET /api/v1/approval/detail/{id}` - 审批详情
|
||||
- `POST /api/v1/approval/submit` - 提交审批
|
||||
- `POST /api/v1/approval/approve/{id}` - 审批通过
|
||||
- `POST /api/v1/approval/reject/{id}` - 审批拒绝
|
||||
- `POST /api/v1/approval/cancel/{id}` - 撤销申请
|
||||
|
||||
### 出差接口
|
||||
|
||||
- `GET /api/v1/business-trip/list` - 出差列表
|
||||
- `GET /api/v1/business-trip/detail/{id}` - 出差详情
|
||||
- `POST /api/v1/business-trip/apply` - 申请出差
|
||||
- `PUT /api/v1/business-trip/{id}` - 更新申请
|
||||
|
||||
## 开发指南
|
||||
|
||||
### 前端开发
|
||||
|
||||
```bash
|
||||
cd frontend
|
||||
npm install # 安装依赖
|
||||
npm run dev # 启动开发服务器
|
||||
npm run build # 构建生产版本
|
||||
```
|
||||
|
||||
### 后端开发
|
||||
|
||||
```bash
|
||||
cd backend
|
||||
composer install # 安装依赖
|
||||
php start.php start # 启动 Webman
|
||||
php start.php stop # 停止 Webman
|
||||
```
|
||||
|
||||
### Docker 开发
|
||||
|
||||
```bash
|
||||
# 启动所有服务
|
||||
docker compose up -d
|
||||
|
||||
# 仅启动后端
|
||||
docker compose up -d backend
|
||||
|
||||
# 查看日志
|
||||
docker logs x-server-backend -f
|
||||
|
||||
# 重启服务
|
||||
docker compose restart backend
|
||||
```
|
||||
|
||||
## 常见问题
|
||||
|
||||
### Q: 如何添加新的翻译文本?
|
||||
|
||||
**前端:** 在 `frontend/src/i18n/locales/` 对应语言的 JSON 文件中添加
|
||||
```json
|
||||
{
|
||||
"myModule": {
|
||||
"myKey": "翻译文本"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**后端:** 在 `backend/lang/` 对应语言的 PHP 文件中添加
|
||||
```php
|
||||
return [
|
||||
'myModule' => [
|
||||
'myKey' => '翻译文本',
|
||||
],
|
||||
];
|
||||
```
|
||||
|
||||
### Q: 如何添加新的应用模块?
|
||||
|
||||
1. 在 `frontend/src/pages/` 创建新页面
|
||||
2. 在 `frontend/src/router/index.tsx` 添加路由
|
||||
3. 在 `backend/app/Controller/` 创建控制器
|
||||
4. 在 `backend/config/route.php` 添加 API 路由
|
||||
|
||||
### Q: 数据库连接失败怎么办?
|
||||
|
||||
1. 检查 MySQL 容器是否运行:`docker ps | grep mysql`
|
||||
2. 检查数据库配置:`backend/.env`
|
||||
3. 检查网络连接:`docker network ls | grep 1panel-network`
|
||||
|
||||
### Q: 前端页面白屏?
|
||||
|
||||
1. 检查浏览器控制台错误
|
||||
2. 检查 React 版本:确保是 18.3.1
|
||||
3. 检查路由配置
|
||||
4. 尝试清除缓存:`rm -rf frontend/node_modules/.vite`
|
||||
|
||||
## 安全建议
|
||||
|
||||
1. **生产环境务必使用 HTTPS**
|
||||
2. **定期更新依赖包**
|
||||
3. **配置防火墙规则**
|
||||
4. **使用强密码**
|
||||
5. **定期备份数据库**
|
||||
6. **不要在代码中硬编码敏感信息**
|
||||
|
||||
## 更新日志
|
||||
|
||||
### 2024-02-24
|
||||
- ✅ 升级 PHP 到 8.5
|
||||
- ✅ 添加 5 语言国际化支持
|
||||
- ✅ 配置 Docker 开发环境
|
||||
- ✅ 实现审批流程引擎基础框架
|
||||
- ✅ 完成前端基础页面和路由
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,287 @@
|
||||
# X-Server Mobile 快速部署指南
|
||||
|
||||
## 一、服务器准备
|
||||
|
||||
### 1.1 安装必要软件
|
||||
|
||||
```bash
|
||||
# 更新系统
|
||||
apt-get update && apt-get upgrade -y
|
||||
|
||||
# 安装 certbot(SSL 证书)
|
||||
apt-get install certbot -y
|
||||
|
||||
# 安装 rsync(文件同步)
|
||||
apt-get install rsync -y
|
||||
```
|
||||
|
||||
### 1.2 创建部署目录
|
||||
|
||||
```bash
|
||||
mkdir -p /opt/x-server-mobile/{frontend,backend,logs,backup}
|
||||
mkdir -p /opt/x-server-mobile/logs/{backend,nginx}
|
||||
```
|
||||
|
||||
### 1.3 获取 SSL 证书
|
||||
|
||||
```bash
|
||||
# 停止 OpenResty(如果占用 80 端口)
|
||||
systemctl stop openresty
|
||||
|
||||
# 获取证书
|
||||
certbot certonly --standalone -d xsm.hnxinyouhe.com
|
||||
|
||||
# 重新启动 OpenResty
|
||||
systemctl start openresty
|
||||
```
|
||||
|
||||
### 1.4 准备数据库
|
||||
|
||||
```bash
|
||||
# 创建数据库用户
|
||||
docker exec -it <mysql-container> mysql -uroot -p
|
||||
|
||||
# 在 MySQL 中执行:
|
||||
CREATE USER 'xserver'@'%' IDENTIFIED BY 'your_strong_password';
|
||||
GRANT ALL PRIVILEGES ON xserver.* TO 'xserver'@'%';
|
||||
FLUSH PRIVILEGES;
|
||||
|
||||
# 导入数据库(如果有备份)
|
||||
docker exec -i <mysql-container> mysql -uxserver -pyour_password xserver < backup.sql
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 二、配置文件准备
|
||||
|
||||
### 2.1 环境变量
|
||||
|
||||
```bash
|
||||
# 在项目根目录
|
||||
cp deploy/.env.production.template .env.production
|
||||
|
||||
# 编辑填写实际配置
|
||||
vi .env.production
|
||||
```
|
||||
|
||||
### 2.2 更新 docker-compose.prod.yml
|
||||
|
||||
```yaml
|
||||
# 修改以下配置为实际值:
|
||||
- DB_HOST=1Panel-mysql-xxxx # 实际 MySQL 容器名
|
||||
- DB_PASSWORD=your_password # 实际密码
|
||||
- REDIS_HOST=1Panel-redis-xxxx # 实际 Redis 容器名
|
||||
- REDIS_PASSWORD=your_redis_password # 实际 Redis 密码
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 三、部署
|
||||
|
||||
### 3.1 方式一:使用部署脚本(推荐)
|
||||
|
||||
```bash
|
||||
# 1. 构建前端
|
||||
cd frontend
|
||||
npm install
|
||||
npm run build
|
||||
cd ..
|
||||
|
||||
# 2. 执行部署脚本
|
||||
./scripts/deploy.sh your-server-ip
|
||||
|
||||
# 脚本会自动完成:
|
||||
# - 备份当前版本
|
||||
# - 上传前端和后端
|
||||
# - 重启容器
|
||||
# - 重载 OpenResty
|
||||
```
|
||||
|
||||
### 3.2 方式二:手动部署
|
||||
|
||||
```bash
|
||||
# 1. 构建前端
|
||||
cd frontend && npm run build && cd ..
|
||||
|
||||
# 2. 上传前端
|
||||
rsync -avz --delete frontend/dist/ root@server:/opt/x-server-mobile/frontend/
|
||||
|
||||
# 3. 上传后端
|
||||
rsync -avz --exclude='vendor' --exclude='runtime' backend/ root@server:/opt/x-server-mobile/backend/
|
||||
|
||||
# 4. 上传配置文件
|
||||
scp deploy/docker-compose.prod.yml root@server:/opt/x-server-mobile/
|
||||
scp deploy/nginx/x-server-mobile-prod.conf root@server:/tmp/
|
||||
|
||||
# 5. 登录服务器配置 OpenResty
|
||||
ssh root@server
|
||||
cp /tmp/x-server-mobile-prod.conf /usr/local/openresty/nginx/conf/conf.d/x-server-mobile.conf
|
||||
openresty -t && openresty -s reload
|
||||
|
||||
# 6. 启动后端容器
|
||||
cd /opt/x-server-mobile
|
||||
docker-compose -f docker-compose.prod.yml up -d
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 四、验证部署
|
||||
|
||||
### 4.1 检查容器状态
|
||||
|
||||
```bash
|
||||
docker ps | grep x-server
|
||||
docker logs -f x-server-backend-prod
|
||||
```
|
||||
|
||||
### 4.2 检查服务
|
||||
|
||||
```bash
|
||||
# 检查前端
|
||||
curl -I https://xsm.hnxinyouhe.com
|
||||
|
||||
# 检查 API
|
||||
curl https://xsm.hnxinyouhe.com/api/v1/user/info \
|
||||
-H "Authorization: Bearer YOUR_TOKEN"
|
||||
```
|
||||
|
||||
### 4.3 功能测试
|
||||
|
||||
- [ ] 登录页面正常访问
|
||||
- [ ] 邮箱密码登录成功
|
||||
- [ ] 飞书免登成功
|
||||
- [ ] 审批列表正常显示
|
||||
- [ ] 飞书卡片通知正常
|
||||
|
||||
---
|
||||
|
||||
## 五、常见问题
|
||||
|
||||
### Q1: 容器无法启动
|
||||
|
||||
```bash
|
||||
# 查看详细日志
|
||||
docker logs x-server-backend-prod
|
||||
|
||||
# 检查网络
|
||||
docker network ls | grep 1panel
|
||||
docker network inspect 1panel-network
|
||||
|
||||
# 检查环境变量
|
||||
docker exec x-server-backend-prod env | grep DB_
|
||||
```
|
||||
|
||||
### Q2: API 返回 500
|
||||
|
||||
```bash
|
||||
# 查看后端日志
|
||||
docker logs x-server-backend-prod --tail 50
|
||||
|
||||
# 检查数据库连接
|
||||
docker exec x-server-backend-prod php -r "
|
||||
var_dump(getenv('DB_HOST'));
|
||||
var_dump(getenv('DB_DATABASE'));
|
||||
"
|
||||
|
||||
# 进入容器调试
|
||||
docker exec -it x-server-backend-prod bash
|
||||
```
|
||||
|
||||
### Q3: SSL 证书问题
|
||||
|
||||
```bash
|
||||
# 检查证书路径
|
||||
ls -la /etc/letsencrypt/live/xsm.hnxinyouhe.com/
|
||||
|
||||
# 测试证书
|
||||
openssl s_client -connect xsm.hnxinyouhe.com:443 -servername xsm.hnxinyouhe.com
|
||||
|
||||
# 手动续期
|
||||
certbot renew --force-renewal
|
||||
```
|
||||
|
||||
### Q4: 前端刷新 404
|
||||
|
||||
检查 OpenResty 配置中的 `try_files` 指令:
|
||||
```nginx
|
||||
location / {
|
||||
root /opt/x-server-mobile/frontend;
|
||||
try_files $uri $uri/ /index.html; # 确保有这行
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 六、回滚
|
||||
|
||||
```bash
|
||||
# 查看可用备份
|
||||
ssh root@server "ls -la /opt/x-server-mobile/backup/"
|
||||
|
||||
# 执行回滚
|
||||
./scripts/rollback.sh 20240227_150000
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 七、监控命令
|
||||
|
||||
```bash
|
||||
# 实时日志
|
||||
docker logs -f x-server-backend-prod
|
||||
|
||||
# 容器资源
|
||||
docker stats x-server-backend-prod
|
||||
|
||||
# Nginx 访问日志
|
||||
tail -f /opt/x-server-mobile/logs/nginx_access.log
|
||||
|
||||
# Nginx 错误日志
|
||||
tail -f /opt/x-server-mobile/logs/nginx_error.log
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 八、更新部署
|
||||
|
||||
当代码有更新时:
|
||||
|
||||
```bash
|
||||
# 1. 拉取最新代码
|
||||
git pull
|
||||
|
||||
# 2. 构建前端
|
||||
cd frontend && npm run build && cd ..
|
||||
|
||||
# 3. 部署
|
||||
./scripts/deploy.sh your-server-ip
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 配置检查清单
|
||||
|
||||
部署前确认:
|
||||
|
||||
```bash
|
||||
# 1. 检查环境变量
|
||||
cat .env.production | grep -v "^#" | grep -v "^$"
|
||||
|
||||
# 2. 检查构建产物
|
||||
ls -la frontend/dist/
|
||||
|
||||
# 3. 检查 OpenResty 配置
|
||||
cat deploy/nginx/x-server-mobile-prod.conf | grep server_name
|
||||
|
||||
# 4. 测试 SSH 连接
|
||||
ssh root@your-server "echo 'SSH OK'"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 联系支持
|
||||
|
||||
如有问题,请参考完整文档:
|
||||
- `DEPLOYMENT.md` - 完整部署文档
|
||||
- `DEPLOYMENT_CHECKLIST.md` - 部署检查清单
|
||||
- `PROJECT_ISSUES.md` - 问题汇总
|
||||
@@ -0,0 +1,142 @@
|
||||
# 反向代理配置指南
|
||||
|
||||
本指南用于配置 OpenResty 和 frpc 以将 API 暴露到公网,便于调试飞书登录。
|
||||
|
||||
## 环境要求
|
||||
|
||||
- 已安装 OpenResty 或 Nginx
|
||||
- 已安装 frpc
|
||||
- 拥有可用的 frp 服务器
|
||||
- 拥有可配置的域名
|
||||
|
||||
## 安装步骤
|
||||
|
||||
### 1. 安装 OpenResty (Ubuntu/Debian)
|
||||
|
||||
```bash
|
||||
# 导入 GPG 密钥
|
||||
wget -qO - https://openresty.org/package/pubkey.gpg | sudo apt-key add -
|
||||
|
||||
# 添加 OpenResty 仓库
|
||||
sudo apt-get -y install -y wget gnupg ca-certificates
|
||||
wget -O - https://openresty.org/package/pubkey.gpg | sudo apt-key add -
|
||||
echo "deb http://openresty.org/package/ubuntu $(lsb_release -sc) main" | sudo tee /etc/apt/sources.list.d/openresty.list
|
||||
|
||||
# 安装 OpenResty
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y openresty
|
||||
|
||||
# 启动服务
|
||||
sudo systemctl start openresty
|
||||
sudo systemctl enable openresty
|
||||
```
|
||||
|
||||
### 2. 配置 OpenResty
|
||||
|
||||
将 `nginx/x-server-api.conf` 复制到 OpenResty 配置目录:
|
||||
|
||||
```bash
|
||||
sudo cp nginx/x-server-api.conf /usr/local/openresty/nginx/conf/conf.d/
|
||||
sudo mkdir -p /usr/local/openresty/nginx/logs
|
||||
sudo openresty -s reload
|
||||
```
|
||||
|
||||
### 3. 安装 frpc
|
||||
|
||||
```bash
|
||||
# 下载 frpc(根据你的系统选择版本)
|
||||
wget https://github.com/fatedier/frp/releases/download/v0.52.3/frp_0.52.3_linux_amd64.tar.gz
|
||||
|
||||
# 解压
|
||||
tar xzf frp_0.52.3_linux_amd64.tar.gz
|
||||
cd frp_0.52.3_linux_amd64
|
||||
|
||||
# 复制到系统路径
|
||||
sudo cp frpc /usr/local/bin/
|
||||
sudo chmod +x /usr/local/bin/frpc
|
||||
```
|
||||
|
||||
### 4. 配置 frpc
|
||||
|
||||
编辑 `frpc/frpc.ini`,填入你的 frp 服务器信息:
|
||||
|
||||
```ini
|
||||
[common]
|
||||
server_addr = your-frp-server.com # 你的 frp 服务器地址
|
||||
server_port = 7000 # frp 服务器端口
|
||||
token = your-frp-token # 认证令牌
|
||||
|
||||
[x-server-api]
|
||||
type = http
|
||||
local_ip = 127.0.0.1
|
||||
local_port = 8787
|
||||
custom_domains = api.x-server.your-domain.com # 你的公网域名
|
||||
```
|
||||
|
||||
### 5. 启动 frpc
|
||||
|
||||
```bash
|
||||
# 前台运行(测试)
|
||||
frpc -c frpc/frpc.ini
|
||||
|
||||
# 后台运行(生产)
|
||||
nohup frpc -c frpc/frpc.ini > /dev/null 2>&1 &
|
||||
```
|
||||
|
||||
### 6. 配置 DNS
|
||||
|
||||
将你的域名 `api.x-server.your-domain.com` 的 A 记录指向你的 frp 服务器 IP。
|
||||
|
||||
### 7. 配置飞书应用
|
||||
|
||||
在飞书开放平台配置回调 URL:
|
||||
|
||||
```
|
||||
https://api.x-server.your-domain.com/api/v1/auth/feishu-login
|
||||
```
|
||||
|
||||
## 验证配置
|
||||
|
||||
### 测试本地 API
|
||||
|
||||
```bash
|
||||
curl http://localhost:8787/api/v1/user/info
|
||||
```
|
||||
|
||||
### 测试反向代理
|
||||
|
||||
```bash
|
||||
curl http://api.x-server.local/api/v1/user/info
|
||||
```
|
||||
|
||||
### 测试公网访问
|
||||
|
||||
```bash
|
||||
curl https://api.x-server.your-domain.com/api/v1/user/info
|
||||
```
|
||||
|
||||
## 常见问题
|
||||
|
||||
### frpc 连接失败
|
||||
|
||||
1. 检查 frp 服务器是否在线
|
||||
2. 检查 token 是否正确
|
||||
3. 检查端口是否被防火墙阻止
|
||||
|
||||
### 域名无法访问
|
||||
|
||||
1. 检查 DNS 解析是否生效
|
||||
2. 检查 frp 服务器是否配置了该域名
|
||||
3. 检查 frpc 日志
|
||||
|
||||
### CORS 错误
|
||||
|
||||
如果前端调用 API 时出现 CORS 错误,检查 nginx 配置中的 CORS 头部设置。
|
||||
|
||||
## 安全建议
|
||||
|
||||
1. **生产环境务必使用 HTTPS**
|
||||
2. 配置 IP 白名单限制访问
|
||||
3. 使用强密码和 token
|
||||
4. 定期更新 frp 和 OpenResty 版本
|
||||
5. 配置防火墙规则
|
||||
@@ -0,0 +1,529 @@
|
||||
<?php
|
||||
|
||||
require_once __DIR__ . '/vendor/autoload.php';
|
||||
require_once __DIR__ . '/support/bootstrap.php';
|
||||
|
||||
use support\Db;
|
||||
|
||||
$userId1 = 1; // 李佳博
|
||||
$userId2 = 2; // 闫继光
|
||||
$userId3 = 3; // 孟启
|
||||
|
||||
// 获取出差审批流程
|
||||
$flow = Db::table('approval_flow')->where('business_type', 'business_trip')->where('status', 1)->first();
|
||||
|
||||
if (!$flow) {
|
||||
echo "Error: No approval flow found\n";
|
||||
exit;
|
||||
}
|
||||
|
||||
echo "Creating test data...\n";
|
||||
|
||||
// 首先删除现有的测试数据(只保留测试数据,id > 10)
|
||||
echo "Cleaning up old test data...\n";
|
||||
Db::table('approval_record')->where('order_id', '>', 10)->delete();
|
||||
Db::table('business_trip_itinerary')->whereIn('business_trip_id', function($q) {
|
||||
$q->select('id')->from('business_trip')->where('id', '>', 10);
|
||||
})->delete();
|
||||
Db::table('business_trip')->where('id', '>', 10)->delete();
|
||||
Db::table('approval_order')->where('id', '>', 10)->delete();
|
||||
echo "Cleanup completed.\n";
|
||||
|
||||
// ========== 用户1(李佳博)发起的订单 ==========
|
||||
|
||||
// 1. 审批中 - 用户1发起的,等待用户2审批
|
||||
$trip1 = Db::table('business_trip')->insertGetId([
|
||||
'user_id' => $userId1,
|
||||
'reason' => '杭州客户洽谈',
|
||||
'start_date' => '2026-03-25',
|
||||
'end_date' => '2026-03-26',
|
||||
'destination' => '杭州',
|
||||
'estimated_cost' => 3000,
|
||||
'status' => 'pending',
|
||||
'created_at' => date('Y-m-d H:i:s', strtotime('-1 hour')),
|
||||
'updated_at' => date('Y-m-d H:i:s', strtotime('-1 hour')),
|
||||
]);
|
||||
|
||||
Db::table('business_trip_itinerary')->insert([
|
||||
'business_trip_id' => $trip1,
|
||||
'start_time' => '2026-03-25 08:00:00',
|
||||
'end_time' => '2026-03-26 18:00:00',
|
||||
'departure' => '郑州',
|
||||
'destination' => '杭州',
|
||||
'transport' => '高铁',
|
||||
'trip_type' => 'roundtrip',
|
||||
'remark' => '洽谈业务合作',
|
||||
'created_at' => date('Y-m-d H:i:s', strtotime('-1 hour')),
|
||||
'updated_at' => date('Y-m-d H:i:s', strtotime('-1 hour')),
|
||||
]);
|
||||
|
||||
$order1 = Db::table('approval_order')->insertGetId([
|
||||
'business_type' => 'business_trip',
|
||||
'business_id' => $trip1,
|
||||
'title' => '出差申请 - 杭州客户洽谈',
|
||||
'applicant_id' => $userId1,
|
||||
'status' => 'pending',
|
||||
'current_nodes' => json_encode([2]), // 节点2是抄送人-孟启(作为审批人)
|
||||
'flow_id' => $flow->id,
|
||||
'form_data' => json_encode(['reason' => '杭州客户洽谈', 'approvers' => [$userId2]]),
|
||||
'created_at' => date('Y-m-d H:i:s', strtotime('-1 hour')),
|
||||
'updated_at' => date('Y-m-d H:i:s', strtotime('-1 hour')),
|
||||
]);
|
||||
|
||||
Db::table('business_trip')->where('id', $trip1)->update(['approval_order_id' => $order1]);
|
||||
|
||||
Db::table('approval_record')->insert([
|
||||
'order_id' => $order1,
|
||||
'node_id' => 1,
|
||||
'node_type' => 'single_approve',
|
||||
'approver_id' => $userId1,
|
||||
'action' => 'submit',
|
||||
'created_at' => date('Y-m-d H:i:s', strtotime('-1 hour')),
|
||||
'updated_at' => date('Y-m-d H:i:s', strtotime('-1 hour')),
|
||||
]);
|
||||
|
||||
echo "Order $order1: 审批中 - 杭州客户洽谈(用户1发起)\n";
|
||||
|
||||
// 2. 已通过 - 用户1发起的,已被批准
|
||||
$trip2 = Db::table('business_trip')->insertGetId([
|
||||
'user_id' => $userId1,
|
||||
'reason' => '南京技术调研',
|
||||
'start_date' => '2026-03-28',
|
||||
'end_date' => '2026-03-29',
|
||||
'destination' => '南京',
|
||||
'estimated_cost' => 2500,
|
||||
'status' => 'approved',
|
||||
'created_at' => date('Y-m-d H:i:s', strtotime('-2 days')),
|
||||
'updated_at' => date('Y-m-d H:i:s', strtotime('-1 day')),
|
||||
]);
|
||||
|
||||
Db::table('business_trip_itinerary')->insert([
|
||||
'business_trip_id' => $trip2,
|
||||
'start_time' => '2026-03-28 08:00:00',
|
||||
'end_time' => '2026-03-29 18:00:00',
|
||||
'departure' => '郑州',
|
||||
'destination' => '南京',
|
||||
'transport' => '高铁',
|
||||
'trip_type' => 'roundtrip',
|
||||
'remark' => '技术调研学习',
|
||||
'created_at' => date('Y-m-d H:i:s', strtotime('-2 days')),
|
||||
'updated_at' => date('Y-m-d H:i:s', strtotime('-2 days')),
|
||||
]);
|
||||
|
||||
$order2 = Db::table('approval_order')->insertGetId([
|
||||
'business_type' => 'business_trip',
|
||||
'business_id' => $trip2,
|
||||
'title' => '出差申请 - 南京技术调研',
|
||||
'applicant_id' => $userId1,
|
||||
'status' => 'approved',
|
||||
'current_nodes' => json_encode([]),
|
||||
'flow_id' => $flow->id,
|
||||
'form_data' => json_encode(['reason' => '南京技术调研', 'approvers' => [$userId2]]),
|
||||
'created_at' => date('Y-m-d H:i:s', strtotime('-2 days')),
|
||||
'updated_at' => date('Y-m-d H:i:s', strtotime('-1 day')),
|
||||
]);
|
||||
|
||||
Db::table('business_trip')->where('id', $trip2)->update(['approval_order_id' => $order2]);
|
||||
|
||||
Db::table('approval_record')->insert([
|
||||
'order_id' => $order2,
|
||||
'node_id' => 1,
|
||||
'node_type' => 'single_approve',
|
||||
'approver_id' => $userId1,
|
||||
'action' => 'submit',
|
||||
'created_at' => date('Y-m-d H:i:s', strtotime('-2 days')),
|
||||
'updated_at' => date('Y-m-d H:i:s', strtotime('-2 days')),
|
||||
]);
|
||||
|
||||
Db::table('approval_record')->insert([
|
||||
'order_id' => $order2,
|
||||
'node_id' => 1,
|
||||
'node_type' => 'single_approve',
|
||||
'approver_id' => $userId2,
|
||||
'action' => 'approve',
|
||||
'comment' => '同意,请及时反馈调研结果',
|
||||
'created_at' => date('Y-m-d H:i:s', strtotime('-1 day')),
|
||||
'updated_at' => date('Y-m-d H:i:s', strtotime('-1 day')),
|
||||
]);
|
||||
|
||||
echo "Order $order2: 已通过 - 南京技术调研(用户1发起)\n";
|
||||
|
||||
// 3. 已拒绝 - 用户1发起的,已被拒绝
|
||||
$trip3 = Db::table('business_trip')->insertGetId([
|
||||
'user_id' => $userId1,
|
||||
'reason' => '成都展会参观',
|
||||
'start_date' => '2026-04-01',
|
||||
'end_date' => '2026-04-03',
|
||||
'destination' => '成都',
|
||||
'estimated_cost' => 6000,
|
||||
'status' => 'rejected',
|
||||
'created_at' => date('Y-m-d H:i:s', strtotime('-3 days')),
|
||||
'updated_at' => date('Y-m-d H:i:s', strtotime('-2 days')),
|
||||
]);
|
||||
|
||||
Db::table('business_trip_itinerary')->insert([
|
||||
'business_trip_id' => $trip3,
|
||||
'start_time' => '2026-04-01 08:00:00',
|
||||
'end_time' => '2026-04-03 18:00:00',
|
||||
'departure' => '郑州',
|
||||
'destination' => '成都',
|
||||
'transport' => '飞机',
|
||||
'trip_type' => 'roundtrip',
|
||||
'remark' => '参观行业展会',
|
||||
'created_at' => date('Y-m-d H:i:s', strtotime('-3 days')),
|
||||
'updated_at' => date('Y-m-d H:i:s', strtotime('-3 days')),
|
||||
]);
|
||||
|
||||
$order3 = Db::table('approval_order')->insertGetId([
|
||||
'business_type' => 'business_trip',
|
||||
'business_id' => $trip3,
|
||||
'title' => '出差申请 - 成都展会参观',
|
||||
'applicant_id' => $userId1,
|
||||
'status' => 'rejected',
|
||||
'current_nodes' => json_encode([]),
|
||||
'flow_id' => $flow->id,
|
||||
'form_data' => json_encode(['reason' => '成都展会参观', 'approvers' => [$userId2]]),
|
||||
'created_at' => date('Y-m-d H:i:s', strtotime('-3 days')),
|
||||
'updated_at' => date('Y-m-d H:i:s', strtotime('-2 days')),
|
||||
]);
|
||||
|
||||
Db::table('business_trip')->where('id', $trip3)->update(['approval_order_id' => $order3]);
|
||||
|
||||
Db::table('approval_record')->insert([
|
||||
'order_id' => $order3,
|
||||
'node_id' => 1,
|
||||
'node_type' => 'single_approve',
|
||||
'approver_id' => $userId1,
|
||||
'action' => 'submit',
|
||||
'created_at' => date('Y-m-d H:i:s', strtotime('-3 days')),
|
||||
'updated_at' => date('Y-m-d H:i:s', strtotime('-3 days')),
|
||||
]);
|
||||
|
||||
Db::table('approval_record')->insert([
|
||||
'order_id' => $order3,
|
||||
'node_id' => 1,
|
||||
'node_type' => 'single_approve',
|
||||
'approver_id' => $userId2,
|
||||
'action' => 'reject',
|
||||
'comment' => '预算紧张,暂不批准参加展会',
|
||||
'created_at' => date('Y-m-d H:i:s', strtotime('-2 days')),
|
||||
'updated_at' => date('Y-m-d H:i:s', strtotime('-2 days')),
|
||||
]);
|
||||
|
||||
echo "Order $order3: 已拒绝 - 成都展会参观(用户1发起)\n";
|
||||
|
||||
// 4. 已撤回 - 用户1发起后自行撤回
|
||||
$trip4 = Db::table('business_trip')->insertGetId([
|
||||
'user_id' => $userId1,
|
||||
'reason' => '西安供应商考察',
|
||||
'start_date' => '2026-04-05',
|
||||
'end_date' => '2026-04-06',
|
||||
'destination' => '西安',
|
||||
'estimated_cost' => 3500,
|
||||
'status' => 'cancelled',
|
||||
'created_at' => date('Y-m-d H:i:s', strtotime('-4 days')),
|
||||
'updated_at' => date('Y-m-d H:i:s', strtotime('-3 days')),
|
||||
]);
|
||||
|
||||
Db::table('business_trip_itinerary')->insert([
|
||||
'business_trip_id' => $trip4,
|
||||
'start_time' => '2026-04-05 08:00:00',
|
||||
'end_time' => '2026-04-06 18:00:00',
|
||||
'departure' => '郑州',
|
||||
'destination' => '西安',
|
||||
'transport' => '高铁',
|
||||
'trip_type' => 'roundtrip',
|
||||
'remark' => '考察供应商',
|
||||
'created_at' => date('Y-m-d H:i:s', strtotime('-4 days')),
|
||||
'updated_at' => date('Y-m-d H:i:s', strtotime('-4 days')),
|
||||
]);
|
||||
|
||||
$order4 = Db::table('approval_order')->insertGetId([
|
||||
'business_type' => 'business_trip',
|
||||
'business_id' => $trip4,
|
||||
'title' => '出差申请 - 西安供应商考察',
|
||||
'applicant_id' => $userId1,
|
||||
'status' => 'cancelled',
|
||||
'current_nodes' => json_encode([]),
|
||||
'flow_id' => $flow->id,
|
||||
'form_data' => json_encode(['reason' => '西安供应商考察', 'approvers' => [$userId2]]),
|
||||
'created_at' => date('Y-m-d H:i:s', strtotime('-4 days')),
|
||||
'updated_at' => date('Y-m-d H:i:s', strtotime('-3 days')),
|
||||
]);
|
||||
|
||||
Db::table('business_trip')->where('id', $trip4)->update(['approval_order_id' => $order4]);
|
||||
|
||||
Db::table('approval_record')->insert([
|
||||
'order_id' => $order4,
|
||||
'node_id' => 1,
|
||||
'node_type' => 'single_approve',
|
||||
'approver_id' => $userId1,
|
||||
'action' => 'submit',
|
||||
'created_at' => date('Y-m-d H:i:s', strtotime('-4 days')),
|
||||
'updated_at' => date('Y-m-d H:i:s', strtotime('-4 days')),
|
||||
]);
|
||||
|
||||
Db::table('approval_record')->insert([
|
||||
'order_id' => $order4,
|
||||
'node_id' => 0,
|
||||
'node_type' => 'cancel',
|
||||
'approver_id' => $userId1,
|
||||
'action' => 'cancel',
|
||||
'comment' => '申请人撤销',
|
||||
'created_at' => date('Y-m-d H:i:s', strtotime('-3 days')),
|
||||
'updated_at' => date('Y-m-d H:i:s', strtotime('-3 days')),
|
||||
]);
|
||||
|
||||
echo "Order $order4: 已撤回 - 西安供应商考察(用户1发起)\n";
|
||||
|
||||
// ========== 其他用户发起,待用户1审批的订单 ==========
|
||||
|
||||
// 5. 待用户1审批 - 用户2发起
|
||||
$trip5 = Db::table('business_trip')->insertGetId([
|
||||
'user_id' => $userId2,
|
||||
'reason' => '北京技术交流',
|
||||
'start_date' => '2026-03-20',
|
||||
'end_date' => '2026-03-22',
|
||||
'destination' => '北京',
|
||||
'estimated_cost' => 5000,
|
||||
'status' => 'pending',
|
||||
'created_at' => date('Y-m-d H:i:s', strtotime('-5 hours')),
|
||||
'updated_at' => date('Y-m-d H:i:s', strtotime('-5 hours')),
|
||||
]);
|
||||
|
||||
Db::table('business_trip_itinerary')->insert([
|
||||
'business_trip_id' => $trip5,
|
||||
'start_time' => '2026-03-20 08:00:00',
|
||||
'end_time' => '2026-03-22 18:00:00',
|
||||
'departure' => '郑州',
|
||||
'destination' => '北京',
|
||||
'transport' => '高铁',
|
||||
'trip_type' => 'roundtrip',
|
||||
'remark' => '参加技术交流会议',
|
||||
'created_at' => date('Y-m-d H:i:s', strtotime('-5 hours')),
|
||||
'updated_at' => date('Y-m-d H:i:s', strtotime('-5 hours')),
|
||||
]);
|
||||
|
||||
$order5 = Db::table('approval_order')->insertGetId([
|
||||
'business_type' => 'business_trip',
|
||||
'business_id' => $trip5,
|
||||
'title' => '出差申请 - 北京技术交流',
|
||||
'applicant_id' => $userId2,
|
||||
'status' => 'pending',
|
||||
'current_nodes' => json_encode([1]), // 节点1是主管审批,审批人是用户1
|
||||
'flow_id' => $flow->id,
|
||||
'form_data' => json_encode(['reason' => '北京技术交流', 'approvers' => [$userId1]]),
|
||||
'created_at' => date('Y-m-d H:i:s', strtotime('-5 hours')),
|
||||
'updated_at' => date('Y-m-d H:i:s', strtotime('-5 hours')),
|
||||
]);
|
||||
|
||||
Db::table('business_trip')->where('id', $trip5)->update(['approval_order_id' => $order5]);
|
||||
|
||||
Db::table('approval_record')->insert([
|
||||
'order_id' => $order5,
|
||||
'node_id' => 1,
|
||||
'node_type' => 'single_approve',
|
||||
'approver_id' => $userId2,
|
||||
'action' => 'submit',
|
||||
'created_at' => date('Y-m-d H:i:s', strtotime('-5 hours')),
|
||||
'updated_at' => date('Y-m-d H:i:s', strtotime('-5 hours')),
|
||||
]);
|
||||
|
||||
echo "Order $order5: 待审批 - 北京技术交流(用户2发起,用户1审批)\n";
|
||||
|
||||
// 6. 待用户1审批 - 用户3发起
|
||||
$trip6 = Db::table('business_trip')->insertGetId([
|
||||
'user_id' => $userId3,
|
||||
'reason' => '上海客户拜访',
|
||||
'start_date' => '2026-03-18',
|
||||
'end_date' => '2026-03-19',
|
||||
'destination' => '上海',
|
||||
'estimated_cost' => 4000,
|
||||
'status' => 'pending',
|
||||
'created_at' => date('Y-m-d H:i:s', strtotime('-3 hours')),
|
||||
'updated_at' => date('Y-m-d H:i:s', strtotime('-3 hours')),
|
||||
]);
|
||||
|
||||
Db::table('business_trip_itinerary')->insert([
|
||||
'business_trip_id' => $trip6,
|
||||
'start_time' => '2026-03-18 08:00:00',
|
||||
'end_time' => '2026-03-19 18:00:00',
|
||||
'departure' => '郑州',
|
||||
'destination' => '上海',
|
||||
'transport' => '高铁',
|
||||
'trip_type' => 'roundtrip',
|
||||
'remark' => '拜访重要客户',
|
||||
'created_at' => date('Y-m-d H:i:s', strtotime('-3 hours')),
|
||||
'updated_at' => date('Y-m-d H:i:s', strtotime('-3 hours')),
|
||||
]);
|
||||
|
||||
$order6 = Db::table('approval_order')->insertGetId([
|
||||
'business_type' => 'business_trip',
|
||||
'business_id' => $trip6,
|
||||
'title' => '出差申请 - 上海客户拜访',
|
||||
'applicant_id' => $userId3,
|
||||
'status' => 'pending',
|
||||
'current_nodes' => json_encode([1]), // 节点1是主管审批,审批人是用户1
|
||||
'flow_id' => $flow->id,
|
||||
'form_data' => json_encode(['reason' => '上海客户拜访', 'approvers' => [$userId1]]),
|
||||
'created_at' => date('Y-m-d H:i:s', strtotime('-3 hours')),
|
||||
'updated_at' => date('Y-m-d H:i:s', strtotime('-3 hours')),
|
||||
]);
|
||||
|
||||
Db::table('business_trip')->where('id', $trip6)->update(['approval_order_id' => $order6]);
|
||||
|
||||
Db::table('approval_record')->insert([
|
||||
'order_id' => $order6,
|
||||
'node_id' => 1,
|
||||
'node_type' => 'single_approve',
|
||||
'approver_id' => $userId3,
|
||||
'action' => 'submit',
|
||||
'created_at' => date('Y-m-d H:i:s', strtotime('-3 hours')),
|
||||
'updated_at' => date('Y-m-d H:i:s', strtotime('-3 hours')),
|
||||
]);
|
||||
|
||||
echo "Order $order6: 待审批 - 上海客户拜访(用户3发起,用户1审批)\n";
|
||||
|
||||
// ========== 用户1已审批的订单(已通过/已拒绝) ==========
|
||||
|
||||
// 7. 用户1已审批(已通过)- 用户2发起
|
||||
$trip7 = Db::table('business_trip')->insertGetId([
|
||||
'user_id' => $userId2,
|
||||
'reason' => '广州展会',
|
||||
'start_date' => '2026-03-10',
|
||||
'end_date' => '2026-03-12',
|
||||
'destination' => '广州',
|
||||
'estimated_cost' => 8000,
|
||||
'status' => 'approved',
|
||||
'created_at' => date('Y-m-d H:i:s', strtotime('-7 days')),
|
||||
'updated_at' => date('Y-m-d H:i:s', strtotime('-6 days')),
|
||||
]);
|
||||
|
||||
Db::table('business_trip_itinerary')->insert([
|
||||
'business_trip_id' => $trip7,
|
||||
'start_time' => '2026-03-10 08:00:00',
|
||||
'end_time' => '2026-03-12 18:00:00',
|
||||
'departure' => '郑州',
|
||||
'destination' => '广州',
|
||||
'transport' => '飞机',
|
||||
'trip_type' => 'roundtrip',
|
||||
'remark' => '参加行业展会',
|
||||
'created_at' => date('Y-m-d H:i:s', strtotime('-7 days')),
|
||||
'updated_at' => date('Y-m-d H:i:s', strtotime('-7 days')),
|
||||
]);
|
||||
|
||||
$order7 = Db::table('approval_order')->insertGetId([
|
||||
'business_type' => 'business_trip',
|
||||
'business_id' => $trip7,
|
||||
'title' => '出差申请 - 广州展会',
|
||||
'applicant_id' => $userId2,
|
||||
'status' => 'approved',
|
||||
'current_nodes' => json_encode([]),
|
||||
'flow_id' => $flow->id,
|
||||
'form_data' => json_encode(['reason' => '广州展会', 'approvers' => [$userId1]]),
|
||||
'created_at' => date('Y-m-d H:i:s', strtotime('-7 days')),
|
||||
'updated_at' => date('Y-m-d H:i:s', strtotime('-6 days')),
|
||||
]);
|
||||
|
||||
Db::table('business_trip')->where('id', $trip7)->update(['approval_order_id' => $order7]);
|
||||
|
||||
Db::table('approval_record')->insert([
|
||||
'order_id' => $order7,
|
||||
'node_id' => 1,
|
||||
'node_type' => 'single_approve',
|
||||
'approver_id' => $userId2,
|
||||
'action' => 'submit',
|
||||
'created_at' => date('Y-m-d H:i:s', strtotime('-7 days')),
|
||||
'updated_at' => date('Y-m-d H:i:s', strtotime('-7 days')),
|
||||
]);
|
||||
|
||||
Db::table('approval_record')->insert([
|
||||
'order_id' => $order7,
|
||||
'node_id' => 1,
|
||||
'node_type' => 'single_approve',
|
||||
'approver_id' => $userId1,
|
||||
'action' => 'approve',
|
||||
'comment' => '同意参加展会,注意控制预算',
|
||||
'created_at' => date('Y-m-d H:i:s', strtotime('-6 days')),
|
||||
'updated_at' => date('Y-m-d H:i:s', strtotime('-6 days')),
|
||||
]);
|
||||
|
||||
echo "Order $order7: 已通过 - 广州展会(用户1已审批)\n";
|
||||
|
||||
// 8. 用户1已审批(已拒绝)- 用户3发起
|
||||
$trip8 = Db::table('business_trip')->insertGetId([
|
||||
'user_id' => $userId3,
|
||||
'reason' => '深圳培训',
|
||||
'start_date' => '2026-03-05',
|
||||
'end_date' => '2026-03-06',
|
||||
'destination' => '深圳',
|
||||
'estimated_cost' => 2000,
|
||||
'status' => 'rejected',
|
||||
'created_at' => date('Y-m-d H:i:s', strtotime('-10 days')),
|
||||
'updated_at' => date('Y-m-d H:i:s', strtotime('-9 days')),
|
||||
]);
|
||||
|
||||
Db::table('business_trip_itinerary')->insert([
|
||||
'business_trip_id' => $trip8,
|
||||
'start_time' => '2026-03-05 08:00:00',
|
||||
'end_time' => '2026-03-06 18:00:00',
|
||||
'departure' => '郑州',
|
||||
'destination' => '深圳',
|
||||
'transport' => '高铁',
|
||||
'trip_type' => 'roundtrip',
|
||||
'remark' => '参加产品培训',
|
||||
'created_at' => date('Y-m-d H:i:s', strtotime('-10 days')),
|
||||
'updated_at' => date('Y-m-d H:i:s', strtotime('-10 days')),
|
||||
]);
|
||||
|
||||
$order8 = Db::table('approval_order')->insertGetId([
|
||||
'business_type' => 'business_trip',
|
||||
'business_id' => $trip8,
|
||||
'title' => '出差申请 - 深圳培训',
|
||||
'applicant_id' => $userId3,
|
||||
'status' => 'rejected',
|
||||
'current_nodes' => json_encode([]),
|
||||
'flow_id' => $flow->id,
|
||||
'form_data' => json_encode(['reason' => '深圳培训', 'approvers' => [$userId1]]),
|
||||
'created_at' => date('Y-m-d H:i:s', strtotime('-10 days')),
|
||||
'updated_at' => date('Y-m-d H:i:s', strtotime('-9 days')),
|
||||
]);
|
||||
|
||||
Db::table('business_trip')->where('id', $trip8)->update(['approval_order_id' => $order8]);
|
||||
|
||||
Db::table('approval_record')->insert([
|
||||
'order_id' => $order8,
|
||||
'node_id' => 1,
|
||||
'node_type' => 'single_approve',
|
||||
'approver_id' => $userId3,
|
||||
'action' => 'submit',
|
||||
'created_at' => date('Y-m-d H:i:s', strtotime('-10 days')),
|
||||
'updated_at' => date('Y-m-d H:i:s', strtotime('-10 days')),
|
||||
]);
|
||||
|
||||
Db::table('approval_record')->insert([
|
||||
'order_id' => $order8,
|
||||
'node_id' => 1,
|
||||
'node_type' => 'single_approve',
|
||||
'approver_id' => $userId1,
|
||||
'action' => 'reject',
|
||||
'comment' => '工作繁忙,暂不安排培训',
|
||||
'created_at' => date('Y-m-d H:i:s', strtotime('-9 days')),
|
||||
'updated_at' => date('Y-m-d H:i:s', strtotime('-9 days')),
|
||||
]);
|
||||
|
||||
echo "Order $order8: 已拒绝 - 深圳培训(用户1已审批)\n";
|
||||
|
||||
echo "\n========== 数据汇总 ==========\n";
|
||||
echo "用户1(李佳博)发起的订单:\n";
|
||||
echo " - 订单$order1: 审批中(杭州客户洽谈)\n";
|
||||
echo " - 订单$order2: 已通过(南京技术调研)\n";
|
||||
echo " - 订单$order3: 已拒绝(成都展会参观)\n";
|
||||
echo " - 订单$order4: 已撤回(西安供应商考察)\n";
|
||||
echo "\n待用户1审批的订单:\n";
|
||||
echo " - 订单$order5: 闫继光 - 北京技术交流\n";
|
||||
echo " - 订单$order6: 孟启 - 上海客户拜访\n";
|
||||
echo "\n用户1已审批的订单:\n";
|
||||
echo " - 订单$order7: 已通过 - 闫继光(广州展会)\n";
|
||||
echo " - 订单$order8: 已拒绝 - 孟启(深圳培训)\n";
|
||||
echo "\n=== 测试数据创建完成 ===\n";
|
||||
Reference in New Issue
Block a user