Showing Posts From
API
OpenAI Videos API:Sora 2 视频生成 Python 调用指南
Python 调用示例 from openai import OpenAIclient = OpenAI(api_key="YOUR_API_KEY")# 提交视频生成任务(异步) job = client.videos.create( model="sora-2", prompt=""" A cinematic drone shot of Tokyo at night. Neon lights reflecting on wet streets. Ultra realistic. """, seconds=8, size="1280x720" )print(job.id)视频生成是异步任务,需要轮询状态: import timewhile True: result = client.videos.retrieve(job.id) print(result.status) if result.status == "completed": print(result.output_url) break elif result.status == "failed": print("生成失败") break time.sleep(5)图片转视频 已有参考图片时,使用 input_reference 保持人物和风格一致: job = client.videos.create( model="sora-2", prompt="The girl smiles and walks forward.", input_reference=open("girl.png", "rb"), seconds=8 )REST API curl https://api.openai.com/v1/videos \ -H "Authorization: Bearer YOUR_API_KEY" \ -F "model=sora-2" \ -F "prompt=A cute panda drinking coffee" \ -F "seconds=8"参数说明参数 说明model sora-2 或 sora-2-proprompt 视频内容描述(英文效果更好)input_reference 参考图片(可选,用于图生视频)seconds 视频时长:4、8、12 秒size 分辨率,如 1280x720、720x1280(竖屏)、1792x1024价格模型 价格Sora 2 约 $0.10 / 秒Sora 2 Pro 约 $0.30~$0.70 / 秒(随分辨率变化)生成 8 秒 1280x720 视频:Sora 2 约 $0.80,Sora 2 Pro 约 $2.40~5.60。 Prompt 写法参考 视频 prompt 建议包含:镜头类型、光线、风格、质量要求。 # 电影风格 A cinematic shot of a samurai walking through a bamboo forest. Golden sunlight. Slow motion. 35mm film. Ultra realistic.# 产品广告 A luxury mechanical watch rotating on a black reflective table. Studio lighting. 8K. Commercial quality.# 动漫风格 Anime style. Cherry blossoms falling. Girl running on a school campus. Beautiful sunset. Highly detailed.英文 prompt 效果稳定,中文可能出现理解偏差。 后端架构(完整平台) 如果需要支持多用户、异步进度追踪,典型架构: 用户请求 → FastAPI → Celery 任务队列 → OpenAI Videos API ↓ Redis 存储 job.id ↓ WebSocket 推送进度 → 前端 ↓ 完成后存储到 OSS/S3
API 分页设计:Offset vs Cursor(Keyset)怎么选
后端接口拉列表几乎都要分页。最常见的是 current + pageSize: { "current": 1, "pageSize": 20 }简单直观,但数据量大 + 频繁翻页 + 数据在变化的场景下有致命问题。生产上大厂 API(GitHub、Slack、Stripe)都在推 Cursor 分页。 Offset 分页的两个坑 1. 深度分页越来越慢 SQL 长这样: SELECT * FROM products ORDER BY created_at DESC LIMIT 20 OFFSET 1000000;数据库要扫过前 100 万行才能扔掉、再取 20 行。1000 万条数据翻到第 500 页,实测能到秒级。 原因:MySQL 不知道"跳过 100 万行"能不能走索引——即使能,也要真的扫过去。 2. 数据漂移 分页过程中数据在增删——用户看到重复或者漏掉。 例子:翻第 1 页看到 20 条 → 有人删了第 5 条 → 翻第 2 页时,原本第 21 条变成了第 20 条,被跳过。 对于列表实时更新的场景(订单、动态、评论流),Offset 分页几乎必然会有这种漏项。 Cursor 分页(Keyset) 思路:用"上一页最后一条的位置"作为下一页起点,不用 offset。 -- 第一页 SELECT * FROM products ORDER BY created_at DESC, id DESC LIMIT 20;-- 第二页(用上一页最后一条的 created_at + id 作为起点) SELECT * FROM products WHERE (created_at, id) < ('2026-06-04 13:00:00', 998765) ORDER BY created_at DESC, id DESC LIMIT 20;关键点:有索引的排序字段(created_at、id) 元组比较处理并列时间 走索引 range scan,速度不随深度衰减接口设计 Offset 版本: // 请求 { "current": 3, "pageSize": 20 }// 响应 { "list": [...], "total": 12345, "current": 3, "pageSize": 20 }Cursor 版本: // 请求 { "cursor": "eyJ0IjoxNzE3NDg...", "pageSize": 20 }// 响应 { "list": [...], "nextCursor": "eyJ0IjoxNzE3NDg2...", "hasMore": true }cursor 通常是 Base64 编码的 {time, id} 之类的组合,客户端不需要理解内容,直接透传。 Cursor 具体怎么生成 import base64, jsondef encode_cursor(last_row): payload = {"t": last_row.created_at.isoformat(), "id": last_row.id} return base64.urlsafe_b64encode(json.dumps(payload).encode()).decode()def decode_cursor(cursor): return json.loads(base64.urlsafe_b64decode(cursor))# API 侧 if cursor: c = decode_cursor(cursor) rows = db.execute(""" SELECT * FROM products WHERE (created_at, id) < (?, ?) ORDER BY created_at DESC, id DESC LIMIT ? """, [c["t"], c["id"], page_size]) else: rows = db.execute(""" SELECT * FROM products ORDER BY created_at DESC, id DESC LIMIT ? """, [page_size])next_cursor = encode_cursor(rows[-1]) if len(rows) == page_size else None两者对比维度 Offset Cursor简单度 ✅ 直观 需要理解元组比较深度分页性能 ❌ 越来越慢 ✅ 常数时间支持随机跳页 ✅ 可以直接第 N 页 ❌ 只能顺序翻数据变化时稳定 ❌ 漏项 / 重复 ✅ 稳定需要 total 计数 ✅ 好算 ❌ 通常不返回支持排序 任意 排序字段必须唯一 + 有索引什么时候用哪个 用 Offset:后台管理系统的表格(数据不常变、用户会跳页) 数据总量不大(几千到几万条) 客户端要显示"第 5 / 100 页"这种明确导航用 Cursor:时间流列表(订单、聊天、动态、日志) 数据量大(十万+) 客户端做"上拉加载更多"(不需要跳页) 数据实时增删两个都做:GitHub API 大多支持 ?page=N(Offset)和 ?before=cursor(Cursor),场景多的时候都提供。 顺带:total 慢查询 Offset 分页返回 total 时,SELECT COUNT(*) 在千万级表上也会慢。优化:缓存 total(Redis,几分钟一次异步刷新) 估算 total(PostgreSQL pg_class.reltuples、MySQL INFORMATION_SCHEMA.TABLES) 只算前几页的精确 total,之后返回 total: null 或 hasMore: true 就够一句话总结 大数据 + 时间流用 Cursor(速度不衰减、结果稳定)、后台管理小表格用 Offset(简单、支持跳页)。别把 Offset 用在千万级实时数据的 API 上。
Laravel + ClickHouse 统计数据推送飞书卡片
用 Laravel 定时任务对接 ClickHouse 日志,每小时/每天推送应用流量报告到飞书群。 核心架构 ClickHouse (request_logs) → Laravel Artisan Command → 飞书 Webhook → 交互式卡片定时任务两种模式:push:feishu-stats — 每小时统计近 1h / 3h 流量 push:feishu-stats daily — 每天 0 点统计近 24h / 3d / 7dArtisan Command <?phpnamespace App\Console\Commands;use Illuminate\Console\Command; use Illuminate\Support\Facades\Http; use Carbon\Carbon; use App\Services\ClickHouseService;class PushFeishuStats extends Command { protected $signature = 'push:feishu-stats {type=hourly}'; protected $description = '推送应用访问统计数据到飞书群'; protected ClickHouseService $clickHouseService; public function __construct(ClickHouseService $clickHouseService) { parent::__construct(); $this->clickHouseService = $clickHouseService; } public function handle() { $type = $this->argument('type'); try { $type === 'daily' ? $this->handleDailyPush() : $this->handleHourlyPush(); return Command::SUCCESS; } catch (\Throwable $e) { $this->error($e->getMessage()); return Command::FAILURE; } } protected function handleHourlyPush() { $now = Carbon::now(); $oneHourAgo = $now->copy()->subHour()->toDateTimeString(); $threeHoursAgo = $now->copy()->subHours(3)->toDateTimeString(); $sql = " SELECT JSONExtractString(request_params, 'appid') AS appid, COUNTIf(timestamp >= '{$oneHourAgo}') AS count_1h, COUNT(1) AS count_3h FROM request_logs WHERE timestamp >= '{$threeHoursAgo}' GROUP BY appid HAVING count_3h > 10 ORDER BY count_3h DESC LIMIT 20 "; $rows = $this->query($sql); $this->sendToFeishu('📊 每小时应用流量监控', $rows, 'hourly'); } protected function handleDailyPush() { $now = Carbon::now(); $oneDayAgo = $now->copy()->subDay()->toDateTimeString(); $threeDaysAgo = $now->copy()->subDays(3)->toDateTimeString(); $sevenDaysAgo = $now->copy()->subDays(7)->toDateTimeString(); $sql = " SELECT JSONExtractString(request_params, 'appid') AS appid, COUNTIf(timestamp >= '{$oneDayAgo}') AS count_24h, COUNTIf(timestamp >= '{$threeDaysAgo}') AS count_3d, COUNT(1) AS count_7d FROM request_logs WHERE timestamp >= '{$sevenDaysAgo}' GROUP BY appid HAVING count_7d > 50 ORDER BY count_7d DESC LIMIT 20 "; $rows = $this->query($sql); $this->sendToFeishu('📅 每日应用流量大盘', $rows, 'daily'); } protected function query(string $sql): array { return $this->clickHouseService->getClient()->select($sql)->rows(); } protected function sendToFeishu(string $title, array $rows, string $type = 'hourly') { $webhookUrl = env('FEISHU_WEBHOOK'); if (!$webhookUrl) { $this->error('未配置 FEISHU_WEBHOOK'); return; } $elements = [ ['tag' => 'markdown', 'content' => '**🕒 生成时间:** ' . now()->format('Y-m-d H:i:s')], ['tag' => 'markdown', 'content' => '**📈 应用数量:** ' . count($rows)], ['tag' => 'hr'], ]; foreach ($rows as $index => $row) { $rank = $index + 1; $appid = $row['appid'] ?: 'unknown'; if ($type === 'hourly') { $count1h = (int) $row['count_1h']; $count3h = (int) $row['count_3h']; $avg = max(1, $count3h / 3); $rate = round(($count1h / $avg) * 100); $trend = '🟢'; $status = '正常'; if ($rate >= 200) { $trend = '🚨'; $status = '流量暴涨'; } elseif ($rate >= 150) { $trend = '🟡'; $status = '流量升高'; } elseif ($rate <= 50) { $trend = '🔵'; $status = '流量下降'; } $content = "### {$trend} TOP {$rank} · {$appid}\n\n> {$status}\n\n" . "- 1小时请求量:**" . number_format($count1h) . "**\n" . "- 3小时请求量:**" . number_format($count3h) . "**\n" . "- 当前热度:**{$rate}%**"; } else { $count24h = (int) $row['count_24h']; $count3d = (int) $row['count_3d']; $count7d = (int) $row['count_7d']; $avg = max(1, $count7d / 7); $rate = round(($count24h / $avg) * 100); $trend = $rate >= 180 ? '🚨' : ($rate >= 130 ? '🟡' : '🟢'); $content = "### {$trend} TOP {$rank} · {$appid}\n\n" . "- 24小时请求量:**" . number_format($count24h) . "**\n" . "- 近3天:**" . number_format($count3d) . "**\n" . "- 近7天:**" . number_format($count7d) . "**\n" . "- 当前热度:**{$rate}%**"; } $elements[] = ['tag' => 'markdown', 'content' => $content]; $elements[] = ['tag' => 'hr']; } $elements[] = ['tag' => 'markdown', 'content' => '⚡ Powered By Laravel + ClickHouse']; $payload = [ 'msg_type' => 'interactive', 'card' => [ 'config' => ['wide_screen_mode' => true], 'header' => [ 'title' => ['tag' => 'plain_text', 'content' => $title], 'template' => 'turquoise', ], 'elements' => $elements, ], ]; $response = Http::timeout(10)->post($webhookUrl, $payload); $response->successful() ? $this->info('飞书推送成功') : $this->error('飞书推送失败: ' . $response->body()); } }注册定时任务 // routes/console.php Schedule::command('push:feishu-stats')->hourly(); Schedule::command('push:feishu-stats daily')->dailyAt('00:00');.env 配置 FEISHU_WEBHOOK=https://open.feishu.cn/open-apis/bot/v2/hook/xxxxxxxxClickHouse 性能建议 当前用 JSONExtractString(request_params, 'appid') 实时解析 JSON,数据量大时 CPU 开销高。建议在 request_logs 表冗余一个 appid String 字段,插入时直接写入,便于索引和 GROUP BY 加速。 飞书卡片效果 📊 每小时应用流量监控 🕒 生成时间:2026-05-28 10:00:00 📈 应用数量:15🚨 TOP 1 · app_main > 流量暴涨 • 1小时请求量:23,412 • 3小时请求量:41,221 • 当前热度:171%🟢 TOP 2 · app_test • 1小时请求量:1,242 • 当前热度:119%比 Markdown 表格可读性高很多,支持趋势颜色标识。
version.json 结构设计:客户端更新检查和热更新
version.json 没有统一标准,但根据用途不同有一套通用的最佳实践字段设计。 最简结构 { "version": "1.0.0" }适合油猴脚本、小工具。只做版本对比时够用。 推荐通用结构 { "version": "1.2.3", "build": 10203, "name": "MyApp", "releaseDate": "2026-05-16T10:00:00Z", "description": "修复登录问题和性能优化", "forceUpdate": false, "downloadUrl": "https://cdn.example.com/app-1.2.3.zip", "sha256": "a1b2c3d4..." }字段 作用version 语义化版本号,方便理解build 数字版本,方便代码比较(newBuild > currentBuild)forceUpdate 是否强制更新(旧版本无法继续使用)sha256 文件校验,防止下载被篡改downloadUrl 客户端直接取用,不用硬编码Electron / 桌面应用更新 { "version": "2.1.0", "minimumVersion": "2.0.0", "forceUpdate": true, "url": "https://cdn.example.com/app-2.1.0.exe", "sha256": "xxxxx", "size": 104857600, "releaseNotes": [ "新增插件系统", "修复内存泄漏问题" ] }minimumVersion 表示低于此版本的客户端必须强制升级,配合 forceUpdate: true 使用。size 可用于计算下载进度。 前端静态资源版本控制 { "version": "2026.05.16", "commit": "8f3ab21", "buildTime": 1747372000, "assets": { "app.js": "a1b2c3", "style.css": "d4e5f6" } }前端通过定期 fetch('/version.json') 检测资源 hash 变化,自动提示用户刷新。commit 和 buildTime 方便追踪具体是哪次 CI 构建。 油猴脚本自动更新 { "version": "1.0.5", "meta": "https://example.com/script.meta.js", "script": "https://example.com/script.user.js" }脚本内: GM_xmlhttpRequest({ url: "https://example.com/version.json", onload(res) { const remote = JSON.parse(res.response); if (remote.version > GM_info.script.version) { GM_notification("有新版本 " + remote.version); } } });远程配置 + Feature Flags { "version": "1.3.0", "env": "production", "api": "https://api.example.com", "featureFlags": { "newUI": true, "betaFeature": false } }这已经接近"远程配置中心",避免了把 API 地址硬编码进客户端。 版本号规范建议 推荐 语义化版本:变更类型 示例不兼容的 API 变更 2.0.0新增功能(向下兼容) 1.3.0Bug 修复 1.3.1避免用纯数字 "version": 15——后期无法区分是大版本还是补丁。 Python 读写 version.json import json# 读取 with open("version.json", "r", encoding="utf-8") as f: data = json.load(f)# 修改后写回(覆盖) data["version"] = "1.2.4" with open("version.json", "w", encoding="utf-8") as f: json.dump(data, f, ensure_ascii=False, indent=2)不要用 open("file", "rw") — Python 不支持 "rw" 模式,会报 ValueError: must have exactly one of create/read/write/append mode。读写合一用 "r+" 加 f.seek(0) + f.truncate(),但最简单的方案是分开读和写。
平台积分体系设计:Credits(余额)与 Points(积分)的区别与应用
Credits vs Points 是两套独立系统特性 Credits Points本质 消费余额(货币) 生态贡献积分可消费 是(API 调用扣费) 否可提现 是(换回 USDC/法币) 否是否等价法币 基本等价 不等价数量变化 实时增减 只增(或少量减)未来用途 功能消费 Token 空投、VIP、治理Credits:平台货币 用户充值后获得 Credits,Credits 直接用于消费: 充值 100 USDT = 获得 100 Credits 调用 API 消费 2 Credits → 余额 98 Credits 提现 50 Credits → 取回约 50 USDTCredits 必须与真实价值挂钩,用户信任建立在 1 Credit ≈ 1 USD 的基础上。 Points:生态积分 Points 不直接等于钱,是平台的"长期价值记录": 充值 100 USDT → +100 Points API 消费 20 USD → +20 Points 邀请好友充值 → +邀请奖励 Points 每日活跃 → +少量 PointsPoints 积累后可以用于:VIP 等级解锁 折扣和返佣 Token 空投快照 DAO 治理权重 锁仓质押防止套利:提现扣回 Points 如果充值得 Points、提现不扣,用户会利用套利: 1. 充值 1000 USDT → 得 1000 Credits + 1000 Points 2. 立刻提现 1000 Credits 3. 净得:0 Credits(本金拿回)+ 1000 Points(免费)解决方案:提现时扣回相应 Points: 提现 1000 Credits → 余额 -1000 Credits,同时 Points -1000或者更稳健:Points 只按实际消费累计,充值/提现不影响 Points: 充值 → Credits+,Points 不变 消费 1 USD → Points +1 提现 → Credits-,Points 不变数据库设计 CREATE TABLE user_balance ( user_id BIGINT PRIMARY KEY, credits DECIMAL(18, 6) DEFAULT 0, -- 可消费余额 points BIGINT DEFAULT 0 -- 累计积分 );-- 消费记录 CREATE TABLE consumption_log ( id BIGINT AUTO_INCREMENT, user_id BIGINT, credits_used DECIMAL(18, 6), points_earned INT, action VARCHAR(100), created_at DATETIME );Points 用整数(无小数),Credits 用高精度小数(避免浮点误差)。 Credits 扣费保证原子性 Credits 扣费和 Points 增加应在同一事务中完成: @Transactional public void consumeCredits(Long userId, BigDecimal amount, String action) { // 检查余额 UserBalance balance = balanceMapper.selectForUpdate(userId); if (balance.getCredits().compareTo(amount) < 0) { throw new InsufficientCreditsException(); } // 扣 Credits balanceMapper.deductCredits(userId, amount); // 加 Points(按消费比例) long points = amount.longValue(); balanceMapper.addPoints(userId, points); // 记录消费日志 consumptionLogMapper.insert(userId, amount, points, action); }SELECT FOR UPDATE 防止并发扣款导致余额超扣。 首版设计建议Credits 优先实现,这是核心盈利结构 Points 首版只记录,不设兑换规则(避免承诺后反悔) 后期发 Token 时,用 Points 快照作为分配依据 积分比例(1 Credits 消费 = 多少 Points)后期可调,但不能追溯修改历史记录
