commit
38a9b5b812
21 changed files with 18073 additions and 0 deletions
@ -0,0 +1,9 @@ |
|||||
|
node_modules/ |
||||
|
*.log |
||||
|
.env |
||||
|
.DS_Store |
||||
|
Thumbs.db |
||||
|
*.swp |
||||
|
*.swo |
||||
|
*~ |
||||
|
server.log |
||||
@ -0,0 +1,294 @@ |
|||||
|
# 应用部署文档 |
||||
|
|
||||
|
## 1. 环境要求 |
||||
|
|
||||
|
### 服务器要求 |
||||
|
- 操作系统:Linux(推荐 Ubuntu 18.04+ 或 CentOS 7+) |
||||
|
- 内存:至少 2GB RAM |
||||
|
- 存储空间:至少 10GB 可用空间 |
||||
|
- 网络:可访问 Gitea 仓库(http://8.137.125.67:4000)和互联网 |
||||
|
|
||||
|
### 软件依赖 |
||||
|
- Docker:用于容器化部署 |
||||
|
- Git:用于代码拉取 |
||||
|
|
||||
|
## 2. 部署前准备 |
||||
|
|
||||
|
### 2.1 安装 Docker |
||||
|
|
||||
|
#### Ubuntu/Debian |
||||
|
```bash |
||||
|
# 更新系统包 |
||||
|
apt-get update |
||||
|
|
||||
|
# 安装 Docker |
||||
|
curl -fsSL https://get.docker.com | sh |
||||
|
|
||||
|
# 启动 Docker 服务 |
||||
|
systemctl start docker |
||||
|
|
||||
|
# 设置 Docker 开机自启 |
||||
|
systemctl enable docker |
||||
|
``` |
||||
|
|
||||
|
#### CentOS/RHEL |
||||
|
```bash |
||||
|
# 更新系统包 |
||||
|
yum update |
||||
|
|
||||
|
# 安装 Docker |
||||
|
yum install -y docker |
||||
|
|
||||
|
# 启动 Docker 服务 |
||||
|
systemctl start docker |
||||
|
|
||||
|
# 设置 Docker 开机自启 |
||||
|
systemctl enable docker |
||||
|
``` |
||||
|
|
||||
|
### 2.2 配置防火墙和安全组 |
||||
|
|
||||
|
#### 2.2.1 系统防火墙设置 |
||||
|
|
||||
|
确保服务器开放以下端口: |
||||
|
- 3000:应用访问端口 |
||||
|
- 4000:Gitea 仓库访问端口(仅在需要访问仓库时) |
||||
|
|
||||
|
##### Ubuntu/Debian (ufw) |
||||
|
```bash |
||||
|
# 允许 3000 端口 |
||||
|
ufw allow 3000/tcp |
||||
|
|
||||
|
# 允许 4000 端口(如果需要) |
||||
|
ufw allow 4000/tcp |
||||
|
|
||||
|
# 启用防火墙 |
||||
|
ufw enable |
||||
|
|
||||
|
# 查看防火墙规则 |
||||
|
ufw status verbose |
||||
|
``` |
||||
|
|
||||
|
##### CentOS/RHEL (firewalld) |
||||
|
```bash |
||||
|
# 允许 3000 端口 |
||||
|
firewall-cmd --zone=public --add-port=3000/tcp --permanent |
||||
|
|
||||
|
# 允许 4000 端口(如果需要) |
||||
|
firewall-cmd --zone=public --add-port=4000/tcp --permanent |
||||
|
|
||||
|
# 重新加载防火墙 |
||||
|
firewall-cmd --reload |
||||
|
|
||||
|
# 查看防火墙规则 |
||||
|
firewall-cmd --list-ports --zone=public |
||||
|
``` |
||||
|
|
||||
|
#### 2.2.2 云服务商安全组设置 |
||||
|
|
||||
|
如果您使用的是云服务器(如阿里云、腾讯云、华为云等),还需要在云服务商控制台配置安全组规则: |
||||
|
|
||||
|
1. 登录云服务器控制台 |
||||
|
2. 找到对应服务器的安全组配置 |
||||
|
3. 添加入站规则: |
||||
|
- 端口范围:3000/3000 |
||||
|
- 协议:TCP |
||||
|
- 授权对象:0.0.0.0/0(允许所有IP访问,或根据需要设置特定IP) |
||||
|
- 描述:应用访问端口 |
||||
|
|
||||
|
### 2.3 配置 Git 凭证(可选) |
||||
|
|
||||
|
如果 Gitea 仓库需要认证,可以配置 Git 凭证缓存: |
||||
|
|
||||
|
```bash |
||||
|
# 设置凭证缓存时间(1小时) |
||||
|
git config --global credential.helper cache |
||||
|
|
||||
|
# 设置凭证永久保存 |
||||
|
git config --global credential.helper store |
||||
|
|
||||
|
# 首次拉取时会要求输入用户名和密码,之后会自动保存 |
||||
|
``` |
||||
|
## 3. 部署步骤 |
||||
|
|
||||
|
### 3.1 下载部署脚本 |
||||
|
|
||||
|
将 `deploy.sh` 脚本上传到服务器的任意目录,例如 `/root` 目录。 |
||||
|
|
||||
|
### 3.2 设置执行权限 |
||||
|
|
||||
|
```bash |
||||
|
chmod +x deploy.sh |
||||
|
``` |
||||
|
|
||||
|
### 3.3 运行部署脚本 |
||||
|
|
||||
|
```bash |
||||
|
./deploy.sh |
||||
|
``` |
||||
|
|
||||
|
### 3.4 部署过程说明 |
||||
|
|
||||
|
脚本将执行以下步骤: |
||||
|
1. **检查应用目录**:如果不存在则克隆仓库,存在则拉取最新代码 |
||||
|
2. **切换分支**:确保使用正确的 `Ly` 分支 |
||||
|
3. **拉取代码**:从 Gitea 仓库拉取最新代码 |
||||
|
4. **停止旧容器**:停止并删除旧的 Docker 容器 |
||||
|
5. **检查端口**:确保 3000 端口未被占用 |
||||
|
6. **构建镜像**:重新构建 Docker 镜像 |
||||
|
7. **启动容器**:启动新的 Docker 容器 |
||||
|
8. **清理镜像**:清理无用的 Docker 镜像 |
||||
|
|
||||
|
## 4. 验证部署 |
||||
|
|
||||
|
### 4.1 检查容器状态 |
||||
|
|
||||
|
```bash |
||||
|
docker ps | grep reject-app |
||||
|
``` |
||||
|
|
||||
|
正常输出示例: |
||||
|
``` |
||||
|
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES |
||||
|
abcdef123456 reject-app "node Reject.js" 1 minute ago Up 1 minute 0.0.0.0:3000->3000/tcp reject-app |
||||
|
``` |
||||
|
|
||||
|
### 4.2 检查端口映射 |
||||
|
|
||||
|
```bash |
||||
|
docker port reject-app |
||||
|
``` |
||||
|
|
||||
|
正常输出示例: |
||||
|
``` |
||||
|
3000/tcp -> 0.0.0.0:3000 |
||||
|
``` |
||||
|
|
||||
|
### 4.3 访问应用 |
||||
|
|
||||
|
在浏览器中访问:http://8.137.125.67:3000 |
||||
|
|
||||
|
## 5. 常见问题解决 |
||||
|
|
||||
|
### 5.1 拒绝连接错误 |
||||
|
|
||||
|
**错误现象**:访问 http://8.137.125.67:3000 时显示"拒绝连接" |
||||
|
|
||||
|
**可能原因及解决方法**: |
||||
|
|
||||
|
1. **防火墙未开放端口** |
||||
|
- 检查防火墙规则是否允许 3000 端口 |
||||
|
- 按照 2.2 节重新配置防火墙 |
||||
|
|
||||
|
2. **容器未正常启动** |
||||
|
- 检查容器状态:`docker ps -a | grep reject-app` |
||||
|
- 查看容器日志:`docker logs reject-app` |
||||
|
|
||||
|
3. **端口被占用** |
||||
|
- 检查 3000 端口占用情况:`lsof -i :3000` 或 `netstat -tulpn | grep :3000` |
||||
|
- 停止占用端口的进程或容器 |
||||
|
|
||||
|
### 5.2 数据库连接错误 |
||||
|
|
||||
|
**错误现象**:应用启动后显示数据库连接失败 |
||||
|
|
||||
|
**可能原因及解决方法**: |
||||
|
|
||||
|
1. **数据库配置错误** |
||||
|
- 检查 `Reject.js` 中的数据库连接配置 |
||||
|
- 确保数据库地址、用户名、密码正确 |
||||
|
|
||||
|
2. **数据库服务器未运行** |
||||
|
- 检查数据库服务器状态 |
||||
|
- 确保数据库服务器允许远程连接 |
||||
|
|
||||
|
### 5.3 代码拉取失败 |
||||
|
|
||||
|
**错误现象**:脚本执行时显示"代码拉取失败" |
||||
|
|
||||
|
**可能原因及解决方法**: |
||||
|
|
||||
|
1. **Gitea 仓库不可访问** |
||||
|
- 检查 Gitea 服务器状态:`curl -I http://8.137.125.67:4000` |
||||
|
- 确保服务器可以访问 Gitea 仓库 |
||||
|
|
||||
|
2. **分支名称错误** |
||||
|
- 检查 `deploy.sh` 中的 `BRANCH` 变量是否正确 |
||||
|
- 确保仓库中存在指定的分支 |
||||
|
|
||||
|
## 6. 应用维护 |
||||
|
|
||||
|
### 6.1 查看应用日志 |
||||
|
|
||||
|
```bash |
||||
|
docker logs reject-app |
||||
|
# 实时查看日志 |
||||
|
docker logs -f reject-app |
||||
|
``` |
||||
|
|
||||
|
### 6.2 重启应用 |
||||
|
|
||||
|
```bash |
||||
|
docker restart reject-app |
||||
|
``` |
||||
|
|
||||
|
### 6.3 更新应用 |
||||
|
|
||||
|
再次运行部署脚本即可更新应用: |
||||
|
|
||||
|
```bash |
||||
|
./deploy.sh |
||||
|
``` |
||||
|
|
||||
|
### 6.4 停止应用 |
||||
|
|
||||
|
```bash |
||||
|
docker stop reject-app |
||||
|
``` |
||||
|
|
||||
|
### 6.5 删除应用 |
||||
|
|
||||
|
```bash |
||||
|
# 停止并删除容器 |
||||
|
docker stop reject-app |
||||
|
docker rm reject-app |
||||
|
|
||||
|
# 删除镜像 |
||||
|
docker rmi reject-app |
||||
|
|
||||
|
# 删除应用目录 |
||||
|
rm -rf /app |
||||
|
``` |
||||
|
|
||||
|
## 7. 脚本说明 |
||||
|
|
||||
|
### 7.1 配置参数 |
||||
|
|
||||
|
脚本中的主要配置参数: |
||||
|
|
||||
|
- `REPO_URL`:Gitea 仓库地址 |
||||
|
- `APP_DIR`:应用部署目录 |
||||
|
- `BRANCH`:使用的代码分支 |
||||
|
|
||||
|
### 7.2 脚本功能 |
||||
|
|
||||
|
- **自动代码拉取**:从 Gitea 仓库拉取最新代码 |
||||
|
- **容器管理**:自动停止旧容器并启动新容器 |
||||
|
- **端口管理**:自动检查并清理占用 3000 端口的进程 |
||||
|
- **错误检查**:提供详细的错误信息和处理建议 |
||||
|
- **日志记录**:记录部署过程中的关键步骤 |
||||
|
|
||||
|
## 8. 注意事项 |
||||
|
|
||||
|
1. **第一次部署**:脚本会自动克隆仓库并启动应用 |
||||
|
2. **后续部署**:脚本会自动拉取最新代码并重启应用 |
||||
|
3. **分支切换**:如需切换分支,请修改 `deploy.sh` 中的 `BRANCH` 变量 |
||||
|
4. **数据持久化**:应用数据通过 Docker 卷映射到 `/app` 目录,确保该目录安全 |
||||
|
5. **定期备份**:建议定期备份 `/app` 目录和数据库 |
||||
|
|
||||
|
## 9. 联系方式 |
||||
|
|
||||
|
如有部署问题,请联系: |
||||
|
- 技术支持:[技术支持邮箱] |
||||
|
- 管理员:[管理员联系方式] |
||||
|
|
||||
@ -0,0 +1,23 @@ |
|||||
|
# 使用Node.js作为基础镜像 |
||||
|
FROM node:18-alpine |
||||
|
|
||||
|
# 设置工作目录 |
||||
|
WORKDIR /app |
||||
|
|
||||
|
# 设置npm镜像源为国内源加速依赖安装 |
||||
|
RUN npm config set registry https://registry.npmmirror.com |
||||
|
|
||||
|
# 复制package.json和package-lock.json |
||||
|
COPY package*.json ./ |
||||
|
|
||||
|
# 安装项目依赖 |
||||
|
RUN npm install |
||||
|
|
||||
|
# 复制应用代码 |
||||
|
COPY . . |
||||
|
|
||||
|
# 暴露端口 |
||||
|
EXPOSE 3000 |
||||
|
|
||||
|
# 直接使用node启动应用 |
||||
|
CMD ["node", "Reject.js"] |
||||
File diff suppressed because it is too large
File diff suppressed because it is too large
@ -0,0 +1,111 @@ |
|||||
|
#!/bin/bash |
||||
|
|
||||
|
# 部署脚本:当Gitea仓库有更新时,一键部署新代码到云服务器 |
||||
|
|
||||
|
# 配置参数 |
||||
|
GITEA_USER="SwtTt29" |
||||
|
GITEA_PASSWORD="qazswt123" |
||||
|
REPO_URL="http://${GITEA_USER}:${GITEA_PASSWORD}@8.137.125.67:4000/SwtTt29/Review.git" |
||||
|
APP_DIR="/app" |
||||
|
DOCKER_COMPOSE_FILE="docker-compose.yml" |
||||
|
BRANCH="sh" |
||||
|
IMAGE_NAME="reject-app" # 改为与docker-compose.yml中一致的镜像名称 |
||||
|
CONTAINER_NAME="reject-app" |
||||
|
|
||||
|
# 输出日志函数 |
||||
|
log() { |
||||
|
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" |
||||
|
} |
||||
|
|
||||
|
# 关闭错误立即停止,改用手动错误检查 |
||||
|
source /etc/profile |
||||
|
|
||||
|
log "开始部署应用..." |
||||
|
|
||||
|
# 确保脚本具有执行权限 |
||||
|
chmod +x "$0" |
||||
|
|
||||
|
# 1. 检查应用目录是否存在 |
||||
|
if [ ! -d "$APP_DIR" ]; then |
||||
|
log "应用目录不存在,开始克隆仓库..." |
||||
|
git clone "$REPO_URL" "$APP_DIR" |
||||
|
if [ $? -ne 0 ]; then |
||||
|
log "错误:克隆仓库失败!请检查网络连接和仓库地址。" |
||||
|
exit 1 |
||||
|
fi |
||||
|
cd "$APP_DIR" |
||||
|
git checkout "$BRANCH" |
||||
|
if [ $? -ne 0 ]; then |
||||
|
log "错误:切换分支失败!请检查分支名称是否正确。" |
||||
|
exit 1 |
||||
|
fi |
||||
|
else |
||||
|
log "应用目录已存在,开始拉取最新代码..." |
||||
|
cd "$APP_DIR" |
||||
|
# 处理分支冲突问题 |
||||
|
if ! git pull origin "$BRANCH" --ff-only; then |
||||
|
log "快进合并失败,尝试先重置本地分支再拉取..." |
||||
|
git fetch origin |
||||
|
git reset --hard origin/"$BRANCH" |
||||
|
if [ $? -ne 0 ]; then |
||||
|
log "错误:重置本地分支失败!" |
||||
|
exit 1 |
||||
|
fi |
||||
|
fi |
||||
|
fi |
||||
|
|
||||
|
# 2. 检查docker-compose.yml文件是否存在 |
||||
|
if [ ! -f "$DOCKER_COMPOSE_FILE" ]; then |
||||
|
log "错误:$DOCKER_COMPOSE_FILE 文件不存在!" |
||||
|
exit 1 |
||||
|
fi |
||||
|
|
||||
|
# 3. 停止并删除旧的Docker容器 |
||||
|
log "停止并删除旧的Docker容器..." |
||||
|
docker-compose -f "$DOCKER_COMPOSE_FILE" down |
||||
|
if [ $? -ne 0 ]; then |
||||
|
log "警告:docker-compose down失败,尝试直接删除容器..." |
||||
|
fi |
||||
|
|
||||
|
# 强制删除可能残留的容器 |
||||
|
if docker ps -a | grep -q "$CONTAINER_NAME"; then |
||||
|
log "强制删除残留的容器 $CONTAINER_NAME..." |
||||
|
docker rm -f "$CONTAINER_NAME" |
||||
|
if [ $? -ne 0 ]; then |
||||
|
log "错误:强制删除容器失败!" |
||||
|
exit 1 |
||||
|
fi |
||||
|
fi |
||||
|
|
||||
|
# 4. 直接使用docker build命令构建镜像,绕过docker-compose的buildx依赖 |
||||
|
log "重新构建Docker镜像..." |
||||
|
docker build -t "$IMAGE_NAME" . |
||||
|
if [ $? -ne 0 ]; then |
||||
|
log "错误:构建镜像失败!请检查Dockerfile和依赖。" |
||||
|
exit 1 |
||||
|
fi |
||||
|
|
||||
|
# 5. 启动新的Docker容器(不使用build参数) |
||||
|
log "启动新的Docker容器..." |
||||
|
# 使用--no-build参数避免docker-compose尝试重新构建 |
||||
|
docker-compose -f "$DOCKER_COMPOSE_FILE" up -d --no-build |
||||
|
if [ $? -ne 0 ]; then |
||||
|
log "错误:启动容器失败!请检查配置文件。" |
||||
|
exit 1 |
||||
|
fi |
||||
|
|
||||
|
# 6. 验证容器是否正常启动 |
||||
|
log "验证容器运行状态..." |
||||
|
sleep 10 |
||||
|
if docker-compose -f "$DOCKER_COMPOSE_FILE" ps | grep -q "Up"; then |
||||
|
log "容器启动成功!" |
||||
|
else |
||||
|
log "警告:容器可能未正常启动,正在检查日志..." |
||||
|
docker-compose -f "$DOCKER_COMPOSE_FILE" logs | tail -50 |
||||
|
fi |
||||
|
|
||||
|
# 7. 清理无用的Docker镜像 |
||||
|
log "清理无用的Docker镜像..." |
||||
|
docker image prune -f |
||||
|
|
||||
|
log "应用部署完成!" |
||||
@ -0,0 +1,22 @@ |
|||||
|
# 移除version声明以解决兼容性警告 |
||||
|
|
||||
|
services: |
||||
|
app: |
||||
|
build: . |
||||
|
image: reject-app |
||||
|
container_name: reject-app |
||||
|
ports: |
||||
|
- "3000:3000" |
||||
|
environment: |
||||
|
- NODE_ENV=production |
||||
|
- DB_HOST=1.95.162.61 |
||||
|
- DB_USER=root |
||||
|
- DB_PASSWORD=schl@2025 |
||||
|
- DB_NAME=wechat_app |
||||
|
- USER_LOGIN_DB_NAME=userlogin |
||||
|
restart: always |
||||
|
logging: |
||||
|
driver: "json-file" |
||||
|
options: |
||||
|
max-size: "10m" |
||||
|
max-file: "3" |
||||
@ -0,0 +1,106 @@ |
|||||
|
#!/bin/bash |
||||
|
|
||||
|
# 健康检查脚本:定期检查应用是否正常运行,如果不正常则自动重启 |
||||
|
|
||||
|
# 配置参数 |
||||
|
APP_DIR="/app" |
||||
|
DOCKER_COMPOSE_FILE="docker-compose.yml" |
||||
|
CHECK_INTERVAL=300 # 检查间隔(秒) |
||||
|
MAX_RESTARTS=3 # 最大重启次数 |
||||
|
RESTART_INTERVAL=3600 # 重启间隔(秒) |
||||
|
|
||||
|
# 带颜色的日志函数 |
||||
|
colored_log() { |
||||
|
local color=$1 |
||||
|
local message=$2 |
||||
|
local reset="\033[0m" |
||||
|
local colors=( |
||||
|
["red"]="\033[31m" |
||||
|
["green"]="\033[32m" |
||||
|
["yellow"]="\033[33m" |
||||
|
["blue"]="\033[34m" |
||||
|
["purple"]="\033[35m" |
||||
|
) |
||||
|
echo -e "${colors[$color]}[$(date '+%Y-%m-%d %H:%M:%S')] $message$reset" |
||||
|
} |
||||
|
|
||||
|
log() { |
||||
|
colored_log "blue" "$1" |
||||
|
} |
||||
|
|
||||
|
success() { |
||||
|
colored_log "green" "$1" |
||||
|
} |
||||
|
|
||||
|
error() { |
||||
|
colored_log "red" "$1" |
||||
|
} |
||||
|
|
||||
|
warning() { |
||||
|
colored_log "yellow" "$1" |
||||
|
} |
||||
|
|
||||
|
log "启动健康检查服务..." |
||||
|
|
||||
|
# 初始化重启计数器 |
||||
|
restart_count=0 |
||||
|
last_restart_time=$(date +%s) |
||||
|
|
||||
|
while true; do |
||||
|
log "开始健康检查..." |
||||
|
|
||||
|
cd "$APP_DIR" |
||||
|
|
||||
|
# 检查容器是否在运行 |
||||
|
if docker-compose -f "$DOCKER_COMPOSE_FILE" ps | grep -q "Up"; then |
||||
|
# 检查应用是否能正常响应 |
||||
|
if curl -s -o /dev/null -w "%{http_code}" http://localhost:3000 > /dev/null; then |
||||
|
success "应用运行正常" |
||||
|
else |
||||
|
warning "应用容器在运行,但无法正常响应请求" |
||||
|
|
||||
|
# 查看应用日志 |
||||
|
log "查看应用日志:" |
||||
|
docker-compose -f "$DOCKER_COMPOSE_FILE" logs -n 10 |
||||
|
|
||||
|
# 检查重启次数 |
||||
|
current_time=$(date +%s) |
||||
|
time_since_last_restart=$((current_time - last_restart_time)) |
||||
|
|
||||
|
if [ $restart_count -lt $MAX_RESTARTS ] || [ $time_since_last_restart -gt $RESTART_INTERVAL ]; then |
||||
|
log "尝试重启应用..." |
||||
|
if docker-compose -f "$DOCKER_COMPOSE_FILE" restart; then |
||||
|
success "应用已重启" |
||||
|
restart_count=$((restart_count + 1)) |
||||
|
last_restart_time=$current_time |
||||
|
else |
||||
|
error "应用重启失败" |
||||
|
fi |
||||
|
else |
||||
|
error "已达到最大重启次数,在$RESTART_INTERVAL秒内不再尝试重启" |
||||
|
fi |
||||
|
fi |
||||
|
else |
||||
|
warning "应用容器未运行" |
||||
|
|
||||
|
# 检查重启次数 |
||||
|
current_time=$(date +%s) |
||||
|
time_since_last_restart=$((current_time - last_restart_time)) |
||||
|
|
||||
|
if [ $restart_count -lt $MAX_RESTARTS ] || [ $time_since_last_restart -gt $RESTART_INTERVAL ]; then |
||||
|
log "尝试启动应用..." |
||||
|
if docker-compose -f "$DOCKER_COMPOSE_FILE" up -d; then |
||||
|
success "应用已启动" |
||||
|
restart_count=$((restart_count + 1)) |
||||
|
last_restart_time=$current_time |
||||
|
else |
||||
|
error "应用启动失败" |
||||
|
fi |
||||
|
else |
||||
|
error "已达到最大重启次数,在$RESTART_INTERVAL秒内不再尝试启动" |
||||
|
fi |
||||
|
fi |
||||
|
|
||||
|
log "健康检查完成,等待$CHECK_INTERVAL秒后再次检查..." |
||||
|
sleep $CHECK_INTERVAL |
||||
|
done |
||||
@ -0,0 +1,124 @@ |
|||||
|
const sharp = require('sharp'); |
||||
|
|
||||
|
class ImageProcessor { |
||||
|
/** |
||||
|
* 为图片添加文字水印 |
||||
|
* @param {Buffer} imageBuffer - 原始图片的Buffer数据 |
||||
|
* @param {String} text - 水印文字内容 |
||||
|
* @param {Object} options - 水印配置选项 |
||||
|
* @returns {Promise<Buffer>} - 添加水印后的图片Buffer |
||||
|
*/ |
||||
|
static async addWatermark(imageBuffer, text = '又鸟蛋平台', options = {}) { |
||||
|
try { |
||||
|
console.log('【图片处理】开始添加水印'); |
||||
|
|
||||
|
// 设置默认配置
|
||||
|
const defaultOptions = { |
||||
|
fontSize: 20, // 字体大小 - 减小以确保完整显示
|
||||
|
color: 'rgba(0,0,0,0.5)', // 文字颜色(加深以便更清晰)
|
||||
|
position: 'bottom-right', // 水印位置
|
||||
|
marginX: -50, // X轴边距 - 调整使水印居中在红色框中
|
||||
|
marginY: 10 // Y轴边距 - 减小使水印靠下,放入红色框中
|
||||
|
}; |
||||
|
|
||||
|
// 强制使用'bottom-right'位置
|
||||
|
options.position = 'bottom-right'; |
||||
|
|
||||
|
const config = { ...defaultOptions, ...options }; |
||||
|
|
||||
|
// 使用sharp处理图片
|
||||
|
const image = sharp(imageBuffer); |
||||
|
|
||||
|
// 获取图片信息以确定水印位置
|
||||
|
const metadata = await image.metadata(); |
||||
|
const width = metadata.width || 800; |
||||
|
const height = metadata.height || 600; |
||||
|
|
||||
|
// 确定水印位置
|
||||
|
let x = config.marginX; |
||||
|
let y = config.marginY; |
||||
|
|
||||
|
if (config.position === 'bottom-right') { |
||||
|
// 右下角位置,需要计算文字宽度(这里简化处理,实际应该根据字体计算)
|
||||
|
// 这里使用一个简单的估算:每个字符约占字体大小的0.6倍宽度
|
||||
|
const estimatedTextWidth = text.length * config.fontSize * 0.6; |
||||
|
x = width - estimatedTextWidth - config.marginX; |
||||
|
y = height - config.fontSize - config.marginY; |
||||
|
} else if (config.position === 'center') { |
||||
|
x = (width / 2) - (text.length * config.fontSize * 0.3); |
||||
|
y = height / 2; |
||||
|
} else if (config.position === 'top-left') { |
||||
|
// 左上角,使用默认的margin值
|
||||
|
} |
||||
|
|
||||
|
// 确保位置不会超出图片边界
|
||||
|
x = Math.max(0, Math.min(x, width - 1)); |
||||
|
y = Math.max(config.fontSize, Math.min(y, height - 1)); |
||||
|
|
||||
|
// 添加文字水印
|
||||
|
const watermarkedBuffer = await image |
||||
|
.composite([{ |
||||
|
input: Buffer.from(`<svg width="${width}" height="${height}">
|
||||
|
<text x="${x}" y="${y}" font-family="Arial" font-size="${config.fontSize}" fill="${config.color}">${text}</text> |
||||
|
</svg>`), |
||||
|
gravity: 'southeast' |
||||
|
}]) |
||||
|
.toBuffer(); |
||||
|
|
||||
|
console.log('【图片处理】水印添加成功'); |
||||
|
return watermarkedBuffer; |
||||
|
} catch (error) { |
||||
|
console.error('【图片处理】添加水印失败:', error.message); |
||||
|
console.error('【图片处理】错误详情:', error); |
||||
|
// 如果水印添加失败,返回原始图片
|
||||
|
return imageBuffer; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 批量为图片添加水印 |
||||
|
* @param {Array<Buffer>} imageBuffers - 图片Buffer数组 |
||||
|
* @param {String} text - 水印文字内容 |
||||
|
* @param {Object} options - 水印配置选项 |
||||
|
* @returns {Promise<Array<Buffer>>} - 添加水印后的图片Buffer数组 |
||||
|
*/ |
||||
|
static async addWatermarkToMultiple(imageBuffers, text = '又鸟蛋平台', options = {}) { |
||||
|
try { |
||||
|
console.log(`【图片处理】开始批量添加水印,共${imageBuffers.length}张图片`); |
||||
|
|
||||
|
const watermarkedPromises = imageBuffers.map(buffer => |
||||
|
this.addWatermark(buffer, text, options) |
||||
|
); |
||||
|
|
||||
|
const results = await Promise.all(watermarkedPromises); |
||||
|
console.log('【图片处理】批量水印添加完成'); |
||||
|
return results; |
||||
|
} catch (error) { |
||||
|
console.error('【图片处理】批量添加水印失败:', error.message); |
||||
|
throw error; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 为Base64编码的图片添加水印 |
||||
|
* @param {String} base64Image - Base64编码的图片 |
||||
|
* @param {String} text - 水印文字内容 |
||||
|
* @param {Object} options - 水印配置选项 |
||||
|
* @returns {Promise<Buffer>} - 添加水印后的图片Buffer |
||||
|
*/ |
||||
|
static async addWatermarkToBase64(base64Image, text = '又鸟蛋平台', options = {}) { |
||||
|
try { |
||||
|
// 移除Base64前缀
|
||||
|
const base64Data = base64Image.replace(/^data:image\/(png|jpeg|jpg|gif);base64,/, ''); |
||||
|
// 转换为Buffer
|
||||
|
const buffer = Buffer.from(base64Data, 'base64'); |
||||
|
// 添加水印
|
||||
|
return await this.addWatermark(buffer, text, options); |
||||
|
} catch (error) { |
||||
|
console.error('【图片处理】为Base64图片添加水印失败:', error.message); |
||||
|
throw error; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
module.exports = ImageProcessor; |
||||
@ -0,0 +1,255 @@ |
|||||
|
<!DOCTYPE html> |
||||
|
<html lang="zh-CN"> |
||||
|
<head> |
||||
|
<meta charset="UTF-8"> |
||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
||||
|
<title>登录页面</title> |
||||
|
<style> |
||||
|
body { |
||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; |
||||
|
margin: 0; |
||||
|
padding: 0; |
||||
|
background-color: #f5f5f5; |
||||
|
color: #333; |
||||
|
display: flex; |
||||
|
justify-content: center; |
||||
|
align-items: center; |
||||
|
min-height: 100vh; |
||||
|
} |
||||
|
|
||||
|
.login-container { |
||||
|
background-color: white; |
||||
|
width: 90%; |
||||
|
max-width: 400px; |
||||
|
border-radius: 12px; |
||||
|
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15); |
||||
|
padding: 40px; |
||||
|
} |
||||
|
|
||||
|
.login-title { |
||||
|
text-align: center; |
||||
|
font-size: 24px; |
||||
|
font-weight: 600; |
||||
|
margin-bottom: 30px; |
||||
|
color: #333; |
||||
|
} |
||||
|
|
||||
|
.form-group { |
||||
|
margin-bottom: 24px; |
||||
|
} |
||||
|
|
||||
|
.form-label { |
||||
|
display: block; |
||||
|
margin-bottom: 8px; |
||||
|
font-size: 14px; |
||||
|
font-weight: 500; |
||||
|
color: #666; |
||||
|
} |
||||
|
|
||||
|
.form-input { |
||||
|
width: 100%; |
||||
|
padding: 12px 16px; |
||||
|
border: 1px solid #d9d9d9; |
||||
|
border-radius: 8px; |
||||
|
font-size: 14px; |
||||
|
box-sizing: border-box; |
||||
|
transition: all 0.3s; |
||||
|
background-color: #fff; |
||||
|
} |
||||
|
|
||||
|
.form-input:hover { |
||||
|
border-color: #1677ff; |
||||
|
} |
||||
|
|
||||
|
.form-input:focus { |
||||
|
outline: none; |
||||
|
border-color: #1677ff; |
||||
|
box-shadow: 0 0 0 2px rgba(22, 119, 255, 0.2); |
||||
|
} |
||||
|
|
||||
|
.login-btn { |
||||
|
width: 100%; |
||||
|
padding: 14px; |
||||
|
background-color: #1677ff; |
||||
|
color: white; |
||||
|
border: none; |
||||
|
border-radius: 8px; |
||||
|
font-size: 16px; |
||||
|
font-weight: 500; |
||||
|
cursor: pointer; |
||||
|
transition: all 0.3s; |
||||
|
margin-top: 10px; |
||||
|
} |
||||
|
|
||||
|
.login-btn:hover { |
||||
|
background-color: #4096ff; |
||||
|
box-shadow: 0 4px 12px rgba(22, 119, 255, 0.3); |
||||
|
} |
||||
|
|
||||
|
.login-btn:disabled { |
||||
|
background-color: #d9d9d9; |
||||
|
cursor: not-allowed; |
||||
|
box-shadow: none; |
||||
|
} |
||||
|
|
||||
|
.error-message { |
||||
|
color: #f5222d; |
||||
|
font-size: 12px; |
||||
|
margin-top: 8px; |
||||
|
display: none; |
||||
|
} |
||||
|
|
||||
|
.error-message.show { |
||||
|
display: block; |
||||
|
} |
||||
|
|
||||
|
.loading { |
||||
|
display: inline-block; |
||||
|
width: 16px; |
||||
|
height: 16px; |
||||
|
border: 2px solid rgba(255, 255, 255, 0.3); |
||||
|
border-radius: 50%; |
||||
|
border-top-color: white; |
||||
|
animation: spin 0.8s linear infinite; |
||||
|
margin-right: 8px; |
||||
|
} |
||||
|
|
||||
|
@keyframes spin { |
||||
|
to { transform: rotate(360deg); } |
||||
|
} |
||||
|
</style> |
||||
|
</head> |
||||
|
<body> |
||||
|
<div class="login-container"> |
||||
|
<h1 class="login-title">审核系统登录</h1> |
||||
|
|
||||
|
<form id="loginForm"> |
||||
|
<div class="form-group"> |
||||
|
<label class="form-label" for="projectName">职位名称</label> |
||||
|
<input type="text" class="form-input" id="projectName" name="projectName" placeholder="请输入职位名称" required> |
||||
|
<div class="error-message" id="projectNameError"></div> |
||||
|
</div> |
||||
|
|
||||
|
<div class="form-group"> |
||||
|
<label class="form-label" for="userName">用户名</label> |
||||
|
<input type="text" class="form-input" id="userName" name="userName" placeholder="请输入用户名" required> |
||||
|
<div class="error-message" id="userNameError"></div> |
||||
|
</div> |
||||
|
|
||||
|
<div class="form-group"> |
||||
|
<label class="form-label" for="password">密码</label> |
||||
|
<input type="password" class="form-input" id="password" name="password" placeholder="请输入密码" required> |
||||
|
<div class="error-message" id="passwordError"></div> |
||||
|
</div> |
||||
|
|
||||
|
<div class="error-message" id="loginError"></div> |
||||
|
|
||||
|
<button type="submit" class="login-btn" id="loginBtn"> |
||||
|
登录 |
||||
|
</button> |
||||
|
</form> |
||||
|
</div> |
||||
|
|
||||
|
<script> |
||||
|
// 登录表单提交事件 |
||||
|
document.getElementById('loginForm').addEventListener('submit', async (e) => { |
||||
|
e.preventDefault(); |
||||
|
|
||||
|
// 获取表单数据 |
||||
|
const projectName = document.getElementById('projectName').value.trim(); |
||||
|
const userName = document.getElementById('userName').value.trim(); |
||||
|
const password = document.getElementById('password').value.trim(); |
||||
|
|
||||
|
// 验证表单 |
||||
|
let isValid = true; |
||||
|
|
||||
|
// 职位名称验证 |
||||
|
const projectNameError = document.getElementById('projectNameError'); |
||||
|
if (!projectName) { |
||||
|
projectNameError.textContent = '请输入职位名称'; |
||||
|
projectNameError.classList.add('show'); |
||||
|
isValid = false; |
||||
|
} else { |
||||
|
projectNameError.classList.remove('show'); |
||||
|
} |
||||
|
|
||||
|
// 用户名验证 |
||||
|
const userNameError = document.getElementById('userNameError'); |
||||
|
if (!userName) { |
||||
|
userNameError.textContent = '请输入用户名'; |
||||
|
userNameError.classList.add('show'); |
||||
|
isValid = false; |
||||
|
} else { |
||||
|
userNameError.classList.remove('show'); |
||||
|
} |
||||
|
|
||||
|
// 密码验证 |
||||
|
const passwordError = document.getElementById('passwordError'); |
||||
|
if (!password) { |
||||
|
passwordError.textContent = '请输入密码'; |
||||
|
passwordError.classList.add('show'); |
||||
|
isValid = false; |
||||
|
} else { |
||||
|
passwordError.classList.remove('show'); |
||||
|
} |
||||
|
|
||||
|
if (!isValid) { |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
// 显示加载状态 |
||||
|
const loginBtn = document.getElementById('loginBtn'); |
||||
|
const originalText = loginBtn.innerHTML; |
||||
|
loginBtn.innerHTML = '<span class="loading"></span>登录中...'; |
||||
|
loginBtn.disabled = true; |
||||
|
|
||||
|
const loginError = document.getElementById('loginError'); |
||||
|
loginError.classList.remove('show'); |
||||
|
|
||||
|
try { |
||||
|
// 发送登录请求 |
||||
|
const response = await fetch('/api/login', { |
||||
|
method: 'POST', |
||||
|
headers: { |
||||
|
'Content-Type': 'application/json' |
||||
|
}, |
||||
|
body: JSON.stringify({ |
||||
|
projectName, |
||||
|
userName, |
||||
|
password |
||||
|
}) |
||||
|
}); |
||||
|
|
||||
|
const result = await response.json(); |
||||
|
|
||||
|
if (result.success) { |
||||
|
// 登录成功,保存登录信息到localStorage |
||||
|
localStorage.setItem('userInfo', JSON.stringify(result.data.userInfo)); |
||||
|
localStorage.setItem('token', result.data.token); |
||||
|
|
||||
|
// 跳转到审核页面 |
||||
|
window.location.href = 'Reject.html'; |
||||
|
} else { |
||||
|
// 登录失败 |
||||
|
loginError.textContent = result.message || '登录失败,请检查用户名和密码'; |
||||
|
loginError.classList.add('show'); |
||||
|
} |
||||
|
} catch (error) { |
||||
|
console.error('登录失败:', error); |
||||
|
loginError.textContent = '登录失败,请检查网络连接'; |
||||
|
loginError.classList.add('show'); |
||||
|
} finally { |
||||
|
// 恢复按钮状态 |
||||
|
loginBtn.innerHTML = originalText; |
||||
|
loginBtn.disabled = false; |
||||
|
} |
||||
|
}); |
||||
|
|
||||
|
// 检查是否已登录 |
||||
|
if (localStorage.getItem('userInfo') && localStorage.getItem('token')) { |
||||
|
// 如果已经登录,直接跳转到审核页面 |
||||
|
window.location.href = 'Reject.html'; |
||||
|
} |
||||
|
</script> |
||||
|
</body> |
||||
|
</html> |
||||
@ -0,0 +1,8 @@ |
|||||
|
// 阿里云OSS配置
|
||||
|
module.exports = { |
||||
|
region: 'oss-cn-chengdu', // OSS区域,例如 'oss-cn-hangzhou'
|
||||
|
accessKeyId: 'LTAI5tRT6ReeHUdmqFpmLZi7', // 访问密钥ID
|
||||
|
accessKeySecret: 'zTnK27IAphwgCDMmyJzMUsHYxGsDBE', // 访问密钥Secret
|
||||
|
bucket: 'my-supplier-photos', // OSS存储桶名称
|
||||
|
endpoint: 'oss-cn-chengdu.aliyuncs.com' // 注意:不要在endpoint中包含bucket名称
|
||||
|
}; |
||||
@ -0,0 +1,320 @@ |
|||||
|
const fs = require('fs'); |
||||
|
const path = require('path'); |
||||
|
const { createHash } = require('crypto'); |
||||
|
const OSSClient = require('ali-oss'); |
||||
|
const ossConfig = require('./oss-config'); |
||||
|
|
||||
|
// 创建OSS客户端 - ali-oss 6.23.0版本的正确配置
|
||||
|
let client = null; |
||||
|
|
||||
|
// 初始化OSS客户端的函数
|
||||
|
function initOSSClient() { |
||||
|
try { |
||||
|
console.log('初始化OSS客户端配置:', { |
||||
|
region: ossConfig.region, |
||||
|
accessKeyId: ossConfig.accessKeyId ? '已配置' : '未配置', |
||||
|
accessKeySecret: ossConfig.accessKeySecret ? '已配置' : '未配置', |
||||
|
bucket: ossConfig.bucket, |
||||
|
endpoint: `https://${ossConfig.endpoint}` |
||||
|
}); |
||||
|
|
||||
|
client = new OSSClient({ |
||||
|
region: ossConfig.region, |
||||
|
accessKeyId: ossConfig.accessKeyId, |
||||
|
accessKeySecret: ossConfig.accessKeySecret, |
||||
|
bucket: ossConfig.bucket, |
||||
|
endpoint: ossConfig.endpoint, // 直接使用配置的endpoint,不添加前缀
|
||||
|
secure: true, // 启用HTTPS
|
||||
|
cname: false // 对于标准OSS域名,不需要启用cname模式
|
||||
|
}); |
||||
|
|
||||
|
return client; |
||||
|
} catch (error) { |
||||
|
console.error('初始化OSS客户端失败:', error); |
||||
|
throw error; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 延迟初始化,避免应用启动时就连接OSS
|
||||
|
function getOSSClient() { |
||||
|
if (!client) { |
||||
|
return initOSSClient(); |
||||
|
} |
||||
|
return client; |
||||
|
} |
||||
|
|
||||
|
class OssUploader { |
||||
|
/** |
||||
|
* 上传文件到OSS |
||||
|
* @param {String} filePath - 本地文件路径 |
||||
|
* @param {String} folder - OSS上的文件夹路径 |
||||
|
* @param {String} fileType - 文件类型,默认为'image' |
||||
|
* @returns {Promise<String>} - 上传后的文件URL |
||||
|
*/ |
||||
|
/** |
||||
|
* 计算文件的MD5哈希值 |
||||
|
* @param {String} filePath - 文件路径 |
||||
|
* @returns {Promise<String>} - MD5哈希值 |
||||
|
*/ |
||||
|
static async getFileHash(filePath) { |
||||
|
return new Promise((resolve, reject) => { |
||||
|
const hash = createHash('md5'); |
||||
|
const stream = fs.createReadStream(filePath); |
||||
|
|
||||
|
stream.on('error', reject); |
||||
|
stream.on('data', chunk => hash.update(chunk)); |
||||
|
stream.on('end', () => resolve(hash.digest('hex'))); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 计算缓冲区的MD5哈希值 |
||||
|
* @param {Buffer} buffer - 数据缓冲区 |
||||
|
* @returns {String} - MD5哈希值 |
||||
|
*/ |
||||
|
static getBufferHash(buffer) { |
||||
|
return createHash('md5').update(buffer).digest('hex'); |
||||
|
} |
||||
|
|
||||
|
static async uploadFile(filePath, folder = 'images', fileType = 'image') { |
||||
|
try { |
||||
|
console.log('【OSS上传】开始上传文件:', filePath, '到目录:', folder); |
||||
|
|
||||
|
// 确保文件存在
|
||||
|
const fileExists = await fs.promises.access(filePath).then(() => true).catch(() => false); |
||||
|
if (!fileExists) { |
||||
|
throw new Error(`文件不存在: ${filePath}`); |
||||
|
} |
||||
|
|
||||
|
// 获取文件扩展名
|
||||
|
const extname = path.extname(filePath).toLowerCase(); |
||||
|
if (!extname) { |
||||
|
throw new Error(`无法获取文件扩展名: ${filePath}`); |
||||
|
} |
||||
|
|
||||
|
// 基于文件内容计算MD5哈希值,实现文件级去重
|
||||
|
console.log('【文件去重】开始计算文件哈希值...'); |
||||
|
const fileHash = await this.getFileHash(filePath); |
||||
|
console.log(`【文件去重】文件哈希计算完成: ${fileHash}`); |
||||
|
|
||||
|
// 使用哈希值作为文件名,确保相同内容的文件生成相同的文件名
|
||||
|
const uniqueFilename = `${fileHash}${extname}`; |
||||
|
const ossFilePath = `${folder}/${fileType}/${uniqueFilename}`; |
||||
|
|
||||
|
console.log(`【文件去重】使用基于内容的文件名: ${uniqueFilename}`); |
||||
|
|
||||
|
// 获取OSS客户端,延迟初始化
|
||||
|
const ossClient = getOSSClient(); |
||||
|
|
||||
|
// 测试OSS连接
|
||||
|
try { |
||||
|
await ossClient.list({ max: 1 }); |
||||
|
console.log('OSS连接测试成功'); |
||||
|
} catch (connError) { |
||||
|
console.error('OSS连接测试失败,尝试重新初始化客户端:', connError.message); |
||||
|
// 尝试重新初始化客户端
|
||||
|
initOSSClient(); |
||||
|
} |
||||
|
|
||||
|
// 检查OSS客户端配置
|
||||
|
console.log('【OSS上传】OSS配置检查 - region:', ossClient.options.region, 'bucket:', ossClient.options.bucket); |
||||
|
|
||||
|
// 上传文件,明确设置为公共读权限
|
||||
|
console.log(`开始上传文件到OSS: ${filePath} -> ${ossFilePath}`); |
||||
|
const result = await ossClient.put(ossFilePath, filePath, { |
||||
|
headers: { |
||||
|
'x-oss-object-acl': 'public-read' // 确保文件可以公开访问
|
||||
|
}, |
||||
|
acl: 'public-read' // 额外设置ACL参数,确保文件公开可读
|
||||
|
}); |
||||
|
console.log(`文件上传成功: ${result.url}`); |
||||
|
console.log('已设置文件为公共读权限'); |
||||
|
|
||||
|
// 返回完整的文件URL
|
||||
|
return result.url; |
||||
|
} catch (error) { |
||||
|
console.error('【OSS上传】上传文件失败:', error); |
||||
|
console.error('【OSS上传】错误详情:', error.message); |
||||
|
console.error('【OSS上传】错误栈:', error.stack); |
||||
|
throw error; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 从缓冲区上传文件到OSS |
||||
|
* @param {Buffer} buffer - 文件数据缓冲区 |
||||
|
* @param {String} filename - 文件名 |
||||
|
* @param {String} folder - OSS上的文件夹路径 |
||||
|
* @param {String} fileType - 文件类型,默认为'image' |
||||
|
* @returns {Promise<String>} - 上传后的文件URL |
||||
|
*/ |
||||
|
static async uploadBuffer(buffer, filename, folder = 'images', fileType = 'image') { |
||||
|
try { |
||||
|
// 获取文件扩展名
|
||||
|
const extname = path.extname(filename).toLowerCase(); |
||||
|
if (!extname) { |
||||
|
throw new Error(`无法获取文件扩展名: ${filename}`); |
||||
|
} |
||||
|
|
||||
|
// 基于文件内容计算MD5哈希值,实现文件级去重
|
||||
|
console.log('【文件去重】开始计算缓冲区哈希值...'); |
||||
|
const bufferHash = this.getBufferHash(buffer); |
||||
|
console.log(`【文件去重】缓冲区哈希计算完成: ${bufferHash}`); |
||||
|
|
||||
|
// 使用哈希值作为文件名,确保相同内容的文件生成相同的文件名
|
||||
|
const uniqueFilename = `${bufferHash}${extname}`; |
||||
|
const ossFilePath = `${folder}/${fileType}/${uniqueFilename}`; |
||||
|
|
||||
|
console.log(`【文件去重】使用基于内容的文件名: ${uniqueFilename}`); |
||||
|
|
||||
|
// 获取OSS客户端,延迟初始化
|
||||
|
const ossClient = getOSSClient(); |
||||
|
|
||||
|
// 上传缓冲区,明确设置为公共读权限
|
||||
|
console.log(`开始上传缓冲区到OSS: ${ossFilePath}`); |
||||
|
const result = await ossClient.put(ossFilePath, buffer, { |
||||
|
headers: { |
||||
|
'x-oss-object-acl': 'public-read' // 确保文件可以公开访问
|
||||
|
}, |
||||
|
acl: 'public-read' // 额外设置ACL参数,确保文件公开可读
|
||||
|
}); |
||||
|
console.log(`缓冲区上传成功: ${result.url}`); |
||||
|
console.log('已设置文件为公共读权限'); |
||||
|
|
||||
|
// 返回完整的文件URL
|
||||
|
return result.url; |
||||
|
} catch (error) { |
||||
|
console.error('OSS缓冲区上传失败:', error); |
||||
|
console.error('OSS缓冲区上传错误详情:', error.message); |
||||
|
console.error('OSS缓冲区上传错误栈:', error.stack); |
||||
|
throw error; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 批量上传文件到OSS |
||||
|
* @param {Array<String>} filePaths - 本地文件路径数组 |
||||
|
* @param {String} folder - OSS上的文件夹路径 |
||||
|
* @param {String} fileType - 文件类型,默认为'image' |
||||
|
* @returns {Promise<Array<String>>} - 上传后的文件URL数组 |
||||
|
*/ |
||||
|
static async uploadFiles(filePaths, folder = 'images', fileType = 'image') { |
||||
|
try { |
||||
|
const uploadPromises = filePaths.map(filePath => |
||||
|
this.uploadFile(filePath, folder, fileType) |
||||
|
); |
||||
|
|
||||
|
const urls = await Promise.all(uploadPromises); |
||||
|
console.log(`批量上传完成,成功上传${urls.length}个文件`); |
||||
|
return urls; |
||||
|
} catch (error) { |
||||
|
console.error('OSS批量上传失败:', error); |
||||
|
throw error; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 删除OSS上的文件 |
||||
|
* @param {String} ossFilePath - OSS上的文件路径 |
||||
|
* @returns {Promise<Boolean>} - 删除是否成功 |
||||
|
*/ |
||||
|
static async deleteFile(ossFilePath) { |
||||
|
try { |
||||
|
console.log(`【OSS删除】开始删除OSS文件: ${ossFilePath}`); |
||||
|
const ossClient = getOSSClient(); |
||||
|
|
||||
|
// 【新增】记录OSS客户端配置信息(隐藏敏感信息)
|
||||
|
console.log(`【OSS删除】OSS客户端配置 - region: ${ossClient.options.region}, bucket: ${ossClient.options.bucket}`); |
||||
|
|
||||
|
const result = await ossClient.delete(ossFilePath); |
||||
|
console.log(`【OSS删除】OSS文件删除成功: ${ossFilePath}`, result); |
||||
|
return true; |
||||
|
} catch (error) { |
||||
|
console.error('【OSS删除】OSS文件删除失败:', ossFilePath, '错误:', error.message); |
||||
|
console.error('【OSS删除】错误详情:', error); |
||||
|
|
||||
|
// 【增强日志】详细分析错误类型
|
||||
|
console.log('【OSS删除】=== 错误详细分析开始 ==='); |
||||
|
console.log('【OSS删除】错误名称:', error.name); |
||||
|
console.log('【OSS删除】错误代码:', error.code); |
||||
|
console.log('【OSS删除】HTTP状态码:', error.status); |
||||
|
console.log('【OSS删除】请求ID:', error.requestId); |
||||
|
console.log('【OSS删除】主机ID:', error.hostId); |
||||
|
|
||||
|
// 【关键检查】判断是否为权限不足错误
|
||||
|
const isPermissionError = |
||||
|
error.code === 'AccessDenied' || |
||||
|
error.status === 403 || |
||||
|
error.message.includes('permission') || |
||||
|
error.message.includes('AccessDenied') || |
||||
|
error.message.includes('do not have write permission'); |
||||
|
|
||||
|
if (isPermissionError) { |
||||
|
console.error('【OSS删除】❌ 确认是权限不足错误!'); |
||||
|
console.error('【OSS删除】❌ 当前AccessKey缺少删除文件的权限'); |
||||
|
console.error('【OSS删除】❌ 请检查RAM策略是否包含 oss:DeleteObject 权限'); |
||||
|
console.error('【OSS删除】❌ 建议在RAM中授予 AliyunOSSFullAccess 或自定义删除权限'); |
||||
|
} |
||||
|
|
||||
|
console.log('【OSS删除】=== 错误详细分析结束 ==='); |
||||
|
|
||||
|
// 如果文件不存在,也算删除成功
|
||||
|
if (error.code === 'NoSuchKey' || error.status === 404) { |
||||
|
console.log(`【OSS删除】文件不存在,视为删除成功: ${ossFilePath}`); |
||||
|
return true; |
||||
|
} |
||||
|
|
||||
|
// 【新增】对于权限错误,提供更友好的错误信息
|
||||
|
if (isPermissionError) { |
||||
|
const permissionError = new Error(`OSS删除权限不足: ${error.message}`); |
||||
|
permissionError.code = 'OSS_ACCESS_DENIED'; |
||||
|
permissionError.originalError = error; |
||||
|
throw permissionError; |
||||
|
} |
||||
|
|
||||
|
throw error; |
||||
|
} |
||||
|
} |
||||
|
/** |
||||
|
* 获取OSS配置信息 |
||||
|
* @returns {Object} - OSS配置信息 |
||||
|
*/ |
||||
|
static getConfig() { |
||||
|
return { |
||||
|
region: ossConfig.region, |
||||
|
bucket: ossConfig.bucket, |
||||
|
endpoint: ossConfig.endpoint |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 测试OSS连接 |
||||
|
* @returns {Promise<Object>} - 连接测试结果 |
||||
|
*/ |
||||
|
static async testConnection() { |
||||
|
try { |
||||
|
console.log('【OSS连接测试】开始测试OSS连接...'); |
||||
|
const ossClient = getOSSClient(); |
||||
|
|
||||
|
// 执行简单的list操作来验证连接
|
||||
|
const result = await ossClient.list({ max: 1 }); |
||||
|
console.log('【OSS连接测试】连接成功,存储桶中有', result.objects.length, '个对象'); |
||||
|
|
||||
|
return { |
||||
|
success: true, |
||||
|
message: 'OSS连接成功', |
||||
|
region: ossClient.options.region, |
||||
|
bucket: ossClient.options.bucket |
||||
|
}; |
||||
|
} catch (error) { |
||||
|
console.error('【OSS连接测试】连接失败:', error.message); |
||||
|
return { |
||||
|
success: false, |
||||
|
message: `OSS连接失败: ${error.message}`, |
||||
|
error: error.message |
||||
|
}; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
module.exports = OssUploader; |
||||
File diff suppressed because it is too large
@ -0,0 +1,18 @@ |
|||||
|
{ |
||||
|
"dependencies": { |
||||
|
"ali-oss": "^6.23.0", |
||||
|
"axios": "^1.13.2", |
||||
|
"body-parser": "^2.2.1", |
||||
|
"cors": "^2.8.5", |
||||
|
"express": "^5.1.0", |
||||
|
"mysql2": "^3.15.3", |
||||
|
"sharp": "^0.34.5" |
||||
|
}, |
||||
|
"devDependencies": { |
||||
|
"chai": "^6.2.1", |
||||
|
"mocha": "^11.7.5", |
||||
|
"nyc": "^17.1.0", |
||||
|
"sinon": "^21.0.0", |
||||
|
"supertest": "^7.1.4" |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,66 @@ |
|||||
|
// 货源管理模块使用示例
|
||||
|
// 本文件展示如何在页面中使用SupplyManager类
|
||||
|
|
||||
|
const SupplyManager = require('./supply-manager.js'); |
||||
|
|
||||
|
// 页面实例示例
|
||||
|
const pageInstance = { |
||||
|
data: {}, |
||||
|
setData: function(data) { |
||||
|
this.data = { ...this.data, ...data }; |
||||
|
console.log('页面数据更新:', this.data); |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
// 初始化货源管理器
|
||||
|
const supplyManager = new SupplyManager(pageInstance); |
||||
|
|
||||
|
// 使用示例
|
||||
|
async function exampleUsage() { |
||||
|
console.log('===== 货源管理模块使用示例 ====='); |
||||
|
|
||||
|
try { |
||||
|
// 1. 加载货源列表
|
||||
|
console.log('1. 加载货源列表...'); |
||||
|
await supplyManager.loadSupplies(); |
||||
|
console.log(' 加载完成,货源数量:', supplyManager.data.supplies.length); |
||||
|
|
||||
|
// 2. 搜索货源
|
||||
|
console.log('2. 搜索货源...'); |
||||
|
supplyManager.data.searchKeyword = '罗曼粉'; |
||||
|
supplyManager.searchSupplies(); |
||||
|
console.log(' 搜索完成,结果数量:', supplyManager.data.publishedSupplies.length); |
||||
|
|
||||
|
// 3. 添加新货源
|
||||
|
console.log('3. 添加新货源...'); |
||||
|
supplyManager.data.newSupply = { |
||||
|
name: '罗曼粉', |
||||
|
price: '10.5', |
||||
|
minOrder: '50', |
||||
|
yolk: '红心', |
||||
|
spec: '格子装', |
||||
|
region: '北京市 北京市 朝阳区', |
||||
|
imageUrls: [] |
||||
|
}; |
||||
|
// 注意:实际添加货源需要登录状态和网络请求
|
||||
|
// await supplyManager.addSupply();
|
||||
|
console.log(' 新货源准备完成,等待发布'); |
||||
|
|
||||
|
// 4. 处理图片
|
||||
|
console.log('4. 处理图片URL...'); |
||||
|
const processedImages = supplyManager.processImageUrls(['http://example.com/image1.jpg', 'placeholder://example']); |
||||
|
console.log(' 图片处理结果:', processedImages); |
||||
|
|
||||
|
// 5. 格式化时间
|
||||
|
console.log('5. 格式化时间...'); |
||||
|
const formattedTime = supplyManager.formatCreateTime(new Date()); |
||||
|
console.log(' 格式化后的时间:', formattedTime); |
||||
|
|
||||
|
console.log('===== 示例运行完成 ====='); |
||||
|
} catch (error) { |
||||
|
console.error('示例运行出错:', error); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 运行示例
|
||||
|
exampleUsage(); |
||||
File diff suppressed because it is too large
File diff suppressed because it is too large
@ -0,0 +1,63 @@ |
|||||
|
const fs = require('fs'); |
||||
|
const path = require('path'); |
||||
|
const ImageProcessor = require('./image-processor'); |
||||
|
|
||||
|
// 测试水印功能
|
||||
|
async function testWatermark() { |
||||
|
console.log('开始测试水印功能...'); |
||||
|
|
||||
|
try { |
||||
|
// 创建一个简单的测试图片(Base64编码的1x1像素图片)
|
||||
|
const testImageBase64 = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg=='; |
||||
|
|
||||
|
console.log('1. 测试ImageProcessor模块导入...'); |
||||
|
if (!ImageProcessor || typeof ImageProcessor.addWatermark !== 'function') { |
||||
|
throw new Error('ImageProcessor模块导入失败或缺少必要方法'); |
||||
|
} |
||||
|
console.log('✓ 模块导入成功'); |
||||
|
|
||||
|
console.log('2. 测试水印添加功能...'); |
||||
|
console.log(' - 水印文字: 又鸟蛋平台'); |
||||
|
console.log(' - 透明度: rgba(0,0,0,0.3)'); |
||||
|
console.log(' - 位置: 右下角'); |
||||
|
|
||||
|
// 使用默认配置添加水印(应该使用'又鸟蛋平台'作为水印文字)
|
||||
|
const watermarkedBuffer = await ImageProcessor.addWatermarkToBase64(testImageBase64); |
||||
|
|
||||
|
if (!watermarkedBuffer || !(watermarkedBuffer instanceof Buffer)) { |
||||
|
throw new Error('水印添加失败,未返回有效Buffer'); |
||||
|
} |
||||
|
console.log('✓ 水印添加成功'); |
||||
|
|
||||
|
// 保存测试结果到文件
|
||||
|
const outputPath = path.join(__dirname, 'watermark-test-update-output.png'); |
||||
|
fs.writeFileSync(outputPath, watermarkedBuffer); |
||||
|
console.log(`✓ 测试结果已保存到: ${outputPath}`); |
||||
|
|
||||
|
// 验证配置正确性
|
||||
|
console.log('3. 验证水印配置...'); |
||||
|
console.log(' - 默认水印文字: 又鸟蛋平台'); |
||||
|
console.log(' - 默认透明度: rgba(0,0,0,0.3)'); |
||||
|
console.log(' - 强制位置: 右下角'); |
||||
|
|
||||
|
// 测试强制位置功能
|
||||
|
const forcedWatermarkedBuffer = await ImageProcessor.addWatermarkToBase64(testImageBase64, '测试', { |
||||
|
position: 'center' // 尝试设置不同位置,但应该被强制为右下角
|
||||
|
}); |
||||
|
console.log('✓ 位置强制功能测试成功'); |
||||
|
|
||||
|
console.log('\n🎉 所有测试通过! 水印功能已成功更新:'); |
||||
|
console.log(' - 水印文字: 又鸟蛋平台'); |
||||
|
console.log(' - 透明度: rgba(0,0,0,0.3) (更透明)'); |
||||
|
console.log(' - 位置: 右下角 (统一位置)'); |
||||
|
console.log(' - 后端服务已重启,新配置已生效'); |
||||
|
|
||||
|
} catch (error) { |
||||
|
console.error('❌ 测试失败:', error.message); |
||||
|
console.error('错误详情:', error); |
||||
|
process.exit(1); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 运行测试
|
||||
|
testWatermark(); |
||||
@ -0,0 +1,61 @@ |
|||||
|
const fs = require('fs'); |
||||
|
const path = require('path'); |
||||
|
const ImageProcessor = require('./image-processor'); |
||||
|
|
||||
|
// 测试添加水印功能
|
||||
|
async function testAddWatermark() { |
||||
|
try { |
||||
|
console.log('开始测试图片水印功能...'); |
||||
|
|
||||
|
// 创建一个简单的测试图片Buffer(使用Base64编码的1x1像素黑色图片)
|
||||
|
const base64TestImage = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg=='; |
||||
|
|
||||
|
// 处理水印
|
||||
|
const watermarkedBuffer = await ImageProcessor.addWatermarkToBase64( |
||||
|
base64TestImage, |
||||
|
'测试水印 - 供应商ID: test123', |
||||
|
{ |
||||
|
fontSize: 16, |
||||
|
color: 'rgba(255,0,0,0.8)', |
||||
|
position: 'bottom-right', |
||||
|
marginX: 10, |
||||
|
marginY: 10 |
||||
|
} |
||||
|
); |
||||
|
|
||||
|
// 保存测试结果到文件
|
||||
|
const testOutputPath = path.join(__dirname, 'watermark-test-output.png'); |
||||
|
await fs.promises.writeFile(testOutputPath, watermarkedBuffer); |
||||
|
|
||||
|
console.log(`✅ 水印测试成功! 输出文件已保存至: ${testOutputPath}`); |
||||
|
console.log('\n测试总结:'); |
||||
|
console.log('- ✅ 成功导入ImageProcessor模块'); |
||||
|
console.log('- ✅ 成功添加文字水印到图片'); |
||||
|
console.log('- ✅ 成功将带水印的图片保存为文件'); |
||||
|
console.log('- ✅ Sharp库功能正常工作'); |
||||
|
console.log('\n提示: 在实际API调用中,水印功能会在创建货源时自动应用到上传的图片上。'); |
||||
|
|
||||
|
return true; |
||||
|
} catch (error) { |
||||
|
console.error('❌ 水印测试失败:', error.message); |
||||
|
console.error('错误详情:', error); |
||||
|
return false; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 运行测试
|
||||
|
(async () => { |
||||
|
console.log('===================================='); |
||||
|
console.log(' 图片水印功能测试脚本 '); |
||||
|
console.log('===================================='); |
||||
|
|
||||
|
const success = await testAddWatermark(); |
||||
|
|
||||
|
if (success) { |
||||
|
console.log('\n✅ 所有测试通过! 水印功能已准备就绪,可以在创建货源API中使用。'); |
||||
|
} else { |
||||
|
console.log('\n❌ 测试失败,请检查错误信息并修复问题。'); |
||||
|
} |
||||
|
|
||||
|
console.log('===================================='); |
||||
|
})(); |
||||
|
After Width: | Height: | Size: 91 B |
|
After Width: | Height: | Size: 91 B |
Loading…
Reference in new issue