1. Flask应用代码 (app.py)

from flask import Flask, redirect, request, jsonify
import random
import oss2
import configparser
import os
from pathlib import Path

app = Flask(__name__)

def load_config():
    """加载配置文件"""
    config = configparser.ConfigParser()
    config_path = os.path.join(os.getcwd(), 'config.ini')
    if not os.path.exists(config_path):
        raise FileNotFoundError("未找到 config.ini 配置文件")
    config.read(config_path)
    
    # 验证必要配置项
    required_keys = {
        'oss': ['access_key_id', 'access_key_secret', 'endpoint', 'bucket_name']
    }
    for section, keys in required_keys.items():
        if not config.has_section(section):
            raise ValueError(f"配置文件中缺少 [{section}] 部分")
        for key in keys:
            if not config.has_option(section, key):
                raise ValueError(f"配置文件中缺少 [{section}] 部分的 {key} 配置项")
    return config

# 加载配置
try:
    config = load_config()
    OSS_ACCESS_KEY_ID = config.get('oss', 'access_key_id')
    OSS_ACCESS_KEY_SECRET = config.get('oss', 'access_key_secret')
    OSS_ENDPOINT = config.get('oss', 'endpoint')
    OSS_BUCKET_NAME = config.get('oss', 'bucket_name')
    OSS_BASE_PATH = config.get('oss', 'base_path', fallback='images/')
except Exception as e:
    app.logger.error(f'配置加载失败: {str(e)}')
    raise

# 初始化OSS客户端
auth = oss2.Auth(OSS_ACCESS_KEY_ID, OSS_ACCESS_KEY_SECRET)
bucket = oss2.Bucket(auth, OSS_ENDPOINT, OSS_BUCKET_NAME)

def list_all_images(prefix):
    """递归列出指定前缀下的所有图片文件"""
    image_files = []
    # 先列出当前目录下的文件
    for obj in oss2.ObjectIterator(bucket, prefix=prefix, delimiter=''):
        if obj.key.lower().endswith(('.jpg', '.jpeg', '.png', '.gif', '.webp')):
            image_files.append(obj.key)
    # 再递归处理子目录
    for obj in oss2.ObjectIterator(bucket, prefix=prefix, delimiter='/'):
        if obj.is_prefix():  # 这是一个子目录
            subdir = obj.key
            image_files.extend(list_all_images(subdir))
    return image_files

def get_random_image(category=None):
    """从指定分类中获取随机图片URL(支持子目录)"""
    prefix = OSS_BASE_PATH
    if category:
        prefix = f'{OSS_BASE_PATH}{category}/'
    try:
        # 获取所有图片文件(包括子目录中的)
        all_images = list_all_images(prefix)
        if not all_images:
            if category:
                return get_random_image(None)  # 回退到无分类模式
            return None
        # 随机选择一个文件
        random_file = random.choice(all_images)
        return f"https://{OSS_BUCKET_NAME}.{OSS_ENDPOINT}/{random_file}"
    except Exception as e:
        app.logger.error(f"获取随机图片失败: {str(e)}")
        return None

@app.route('/random')
def random_image():
    """随机图片API"""
    category = request.args.get('category')
    image_url = get_random_image(category)
    if image_url:
        return redirect(image_url, code=302)
    else:
        return jsonify({
            "error": "No images found",
            "status": 404
        }), 404

@app.route('/categories')
def list_categories():
    """列出所有一级分类"""
    try:
        categories = set()
        for obj in oss2.ObjectIterator(bucket, prefix=OSS_BASE_PATH, delimiter='/'):
            if obj.is_prefix():
                # 提取一级分类名
                relative_path = obj.key[len(OSS_BASE_PATH):-1]
                first_level = relative_path.split('/')[0]
                categories.add(first_level)
        return jsonify({
            "categories": sorted(list(categories)),
            "status": 200
        })
    except Exception as e:
        app.logger.error(f"获取分类列表失败: {str(e)}")
        return jsonify({
            "error": "Failed to list categories",
            "status": 500
        }), 500

@app.route('/subcategories')
def list_subcategories():
    """列出指定分类下的子分类"""
    try:
        category = request.args.get('category')
        if not category:
            return jsonify({
                "error": "Category parameter is required",
                "status": 400
            }), 400
        prefix = f'{OSS_BASE_PATH}{category}/'
        subcategories = set()
        # 查找一级子目录
        for obj in oss2.ObjectIterator(bucket, prefix=prefix, delimiter='/'):
            if obj.is_prefix():
                # 提取子分类名(相对于当前分类)
                sub_path = obj.key[len(prefix):-1]
                if sub_path:  # 确保不是空字符串
                    subcategories.add(sub_path.split('/')[0])  # 只取第一级子目录
        return jsonify({
            "category": category,
            "subcategories": sorted(list(subcategories)),
            "status": 200
        })
    except Exception as e:
        app.logger.error(f"获取子分类列表失败: {str(e)}")
        return jsonify({
            "error": "Failed to list subcategories",
            "status": 500
        }), 500

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5000, debug=True)

2. 配置文件示例 (config.ini)

[oss]
access_key_id = your_access_key_id
access_key_secret = your_access_key_secret
endpoint = oss-cn-hangzhou.aliyuncs.com
bucket_name = your-bucket-name
base_path = images/  ; 可选,默认为 images/

3. OSS目录结构示例

images/
├── category1/
│   ├── subcategory1/
│   └── subcategory2/
├── category2/
└── category3/
    └── subcategory3/

4. 部署建议

# 1. 创建虚拟环境
python -m venv venv
source venv/bin/activate  # Linux/Mac
venv\Scripts\activate    # Windows

# 2. 安装依赖
pip install flask oss2 configparser

# 3. 创建并编辑配置文件
cp config.example.ini config.ini
# 然后编辑 config.ini 填入你的OSS配置

# 4. 运行应用
python app.py

# 5. 生产环境建议使用WSGI服务器
pip install gunicorn
gunicorn -w 4 -b 0.0.0.0:5000 app:app

5. 安全注意事项

  1. 将config.ini添加到.gitignore中,避免将敏感信息提交到版本控制

  2. 配置文件权限设置为仅当前用户可读:

    chmod 600 config.ini
  3. 定期轮换AccessKey,避免长期使用同一组密钥

6. API使用示例

  1. 获取随机动漫图片(包括所有子目录):

    GET /random?category=dongman
  2. 列出所有一级分类:

    GET /categories
  3. 列出动漫分类下的子分类:

    GET /subcategories?category=dongman

7. 系统服务配置 (Linux Systemd)

[Unit]
Description=Random Image API Service
After=network.target

[Service]
User=your_username
Group=your_groupname
WorkingDirectory=/path/to/your/project
Environment="PATH=/path/to/your/project/venv/bin"
ExecStart=/path/to/your/project/venv/bin/gunicorn --workers 3 --bind 0.0.0.0:5000 app:app
Restart=always
RestartSec=10

[Install]
WantedBy=multi-user.target

8. 日志轮替配置 (Logrotate)

/var/log/randomimgapi/*.log {
    daily
    rotate 7
    compress
    delaycompress
    missingok
    notifempty
    create 0640 ubuntu ubuntu
    sharedscripts
    postrotate
        systemctl reload randomimgapi >/dev/null 2>&1 || true
    endscript
}

9. 维护命令

# Linux系统
sudo systemctl restart randomimgapi  # 重启服务
sudo systemctl stop randomimgapi    # 停止服务
sudo systemctl disable randomimgapi # 禁用开机启动
journalctl -u randomimgapi -f       # 查看日志

# Windows系统
nssm restart randomimgapi  # 重启服务
nssm stop randomimgapi     # 停止服务
nssm remove randomimgapi confirm  # 删除服务

10. 高级配置选项

  1. Gunicorn配置文件示例 (gunicorn_config.py):

bind = "0.0.0.0:5000"
workers = 3
worker_class = "gevent"
timeout = 120
keepalive = 5
loglevel = "info"
accesslog = "/var/log/randomimgapi/access.log"
errorlog = "/var/log/randomimgapi/error.log"
  1. 环境变量配置:

[Service]
EnvironmentFile=/path/to/your/project/.env

管理员用户