Files
Genarrative/deploy/nginx/genarrative.conf
T
suzmii 959ed4fd53 合并 origin/master(51cb05f41,22 个提交)到 feat/game-purchase
- 逐块合并 server-rs/crates/api-server/src/modules/game_distribution.rs:以 master 的 ReleaseAssetResponseInput / 5 参 release_asset_response_with_cache / ETag / 304 / gzip 结构为准,并在其上保留本分支的付费 404 守卫(price_mud_points > 0 仍在 release_package_bytes 读包之前返回);播放会话资源响应改用同一结构(etag None、cache_control no-store、沿用 accept-encoding 协商)。
- 合并 src/components/game-distribution/GamePlayPage.tsx:接入 master 的共享 PlatformGameLoadingSurface 与 PLATFORM_GAME_LOADING_TIMEOUT_MS,删除本地重复常量 GAME_PLAY_STARTUP_TIMEOUT_MS 与裸 div,买断制播放会话准备态改由共享加载面承载。
- 合并 src/components/game-distribution/GameDetailPage.tsx:同时保留 master 的 onOpenCreator 作者插槽与本分支的买断制购买弹窗、onOpenRecharge 充值入口。
- 合并 vite.config.ts:保留 master 的 /api/creators 代理,并保留 play-sessions 前缀清 Cookie 规则(顺序仍在通用 /api/game-distribution 之前)。
- 合并 deploy/nginx/README.md:保留 master 的 SPA allowlist 门禁口径(含 /creators、/creators/connections)与本分支的播放会话前缀章节;三份 nginx 模板的 ^~ play-sessions location 与清 Cookie 原样保留。
- 合并 apps/admin-web/src/pages/AdminGameDistributionReviewPage.test.tsx:保留 master 的加载面/超时用例与本分支的审核价格(冻结价优先、历史按 0)用例。
- 合并 src/components/game-distribution/GameDistributionPages.test.tsx:保留双方 mock,并新增「付费作品在会话签发期间显示共享加载面」用例。
- 合并 docs/【玩法创作】平台入口与玩法链路-2026-05-15.md(保留买断制合同并回填 master 的创作者主页与关注粉丝合同)、decision-log.md、pitfalls.md:追加双方条目,不改写任一侧正文。
- 保留 master 侧新增能力:release_asset_etag / if_none_match_matches / accepts_gzip_encoding / gzip_release_asset / release_asset_not_modified_response、SPA 加载面、创作者主页与关注粉丝(user_follow 表、creator 查询与 author_id 过滤)。
2026-10-06 00:28:36 +08:00

303 lines
10 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 生产域名需要在部署前替换为真实域名,并由 certbot 或等价流程写入 HTTPS 证书配置。
log_format genarrative_upstream
'$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" "$http_user_agent" '
'request_time=$request_time upstream_connect_time=$upstream_connect_time '
'upstream_header_time=$upstream_header_time upstream_response_time=$upstream_response_time '
'upstream_status=$upstream_status request_id=$request_id';
upstream genarrative_api {
server 127.0.0.1:8082;
keepalive 64;
}
limit_conn_zone $binary_remote_addr zone=genarrative_api_conn:10m;
# 中文注释:公开作品架 / 自定义世界画廊的读取 QPS 远高于普通创作接口,单独使用高阈值限流 zone;
# 连接数上限仍复用 genarrative_api_conn,避免绕过 API 侧的整体连接保护。
limit_req_zone $binary_remote_addr zone=genarrative_gallery_rps:10m rate=5000r/s;
limit_req_zone $binary_remote_addr zone=genarrative_api_rps:10m rate=300r/s;
limit_req_zone $binary_remote_addr zone=genarrative_admin_rps:10m rate=30r/s;
# 维护期间允许真实 TCP 内网来源继续访问整站;不信任请求头伪造的客户端地址。
geo $remote_addr $genarrative_internal_client {
default 0;
127.0.0.0/8 1;
10.0.0.0/8 1;
172.16.0.0/12 1;
192.168.0.0/16 1;
169.254.0.0/16 1;
::1 1;
fc00::/7 1;
fe80::/10 1;
}
server {
listen 80;
server_name genarrative.example.com;
access_log /var/log/nginx/genarrative.access.log genarrative_upstream;
error_log /var/log/nginx/genarrative.error.log warn;
limit_conn_status 429;
limit_conn_log_level warn;
limit_req_status 429;
limit_req_log_level warn;
location /.well-known/acme-challenge/ {
root /var/www/html;
}
location / {
return 301 https://$host$request_uri;
}
}
server {
listen 443 ssl http2;
server_name genarrative.example.com;
access_log /var/log/nginx/genarrative.access.log genarrative_upstream;
error_log /var/log/nginx/genarrative.error.log warn;
limit_conn_status 429;
limit_conn_log_level warn;
limit_req_status 429;
limit_req_log_level warn;
gzip on;
gzip_vary on;
gzip_proxied any;
gzip_comp_level 5;
gzip_min_length 1024;
gzip_types
text/plain
text/css
text/javascript
application/javascript
application/json
application/xml
application/xml+rss
image/svg+xml;
# __GENARRATIVE_BROTLI_DIRECTIVES__
ssl_certificate /etc/letsencrypt/live/genarrative.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/genarrative.example.com/privkey.pem;
root /srv/genarrative/web;
index index.html;
include /etc/nginx/snippets/genarrative-maintenance.conf;
location ^~ /admin/api/ {
default_type application/json;
limit_conn genarrative_api_conn 64;
limit_req zone=genarrative_admin_rps burst=16 nodelay;
if ($genarrative_maintenance) {
return 503 '{"ok":false,"error":{"code":"MAINTENANCE","message":"服务维护中"}}';
}
proxy_pass http://genarrative_api/admin/api/;
proxy_http_version 1.1;
proxy_set_header Connection "";
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;
proxy_set_header X-Request-Id $request_id;
}
location = /admin {
error_page 503 /maintenance.html;
if ($genarrative_maintenance) {
return 503;
}
return 301 /admin/;
}
location ^~ /admin/assets/ {
error_page 503 /maintenance.html;
if ($genarrative_maintenance) {
return 503;
}
try_files $uri =404;
}
location ^~ /admin/ {
error_page 503 /maintenance.html;
if ($genarrative_maintenance) {
return 503;
}
try_files $uri $uri/ /admin/index.html;
}
location ^~ /assets/ {
try_files $uri =404;
}
# 中文注释:以下为生产机 host-only 覆盖(内部工具页 / 官网页 / 公开画廊读取限流)。
# 这些路径不属于平台共享发布产物,也不在 Pingora 路由矩阵覆盖范围内,
# 因此独立成 snippet:新增平台路由仍必须走矩阵 + Pingora 门禁。
include /etc/nginx/snippets/genarrative-host-extras.conf;
# 平台付费游戏播放会话入口:`/api/game-distribution/play-sessions/<token>/…` 是 sandbox iframe
# 的 src,包内相对资源沿同一前缀解析。它与通用 `/api` 同口径代理到 api-server(大小上限、
# 限流、超时、维护判断都保持一致),唯一差别是清空 Cookie:播放会话不读账号凭证,而
# api-server 播放网关对带平台 refresh Cookie 的请求返回 403(纵深防御保留)。
# 前缀 location 必须写 `^~`:不加时正则 location `~ ^/api(?:/|$)` 会先命中,Cookie 又会被转发。
# 只匹配带尾斜杠的前缀,创建会话的 `POST /api/game-distribution/play-sessions` 仍走通用 `/api`。
location ^~ /api/game-distribution/play-sessions/ {
default_type application/json;
client_max_body_size 210m;
limit_conn genarrative_api_conn 64;
limit_req zone=genarrative_api_rps burst=64 nodelay;
if ($genarrative_maintenance) {
return 503 '{"ok":false,"error":{"code":"MAINTENANCE","message":"服务维护中"}}';
}
proxy_pass http://genarrative_api;
proxy_http_version 1.1;
proxy_buffering off;
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
add_header X-Accel-Buffering no always;
proxy_set_header Connection "";
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;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Request-Id $request_id;
proxy_set_header Cookie "";
}
# 临时兼容主站仍在使用的 /api/* HTTP facade;前端完成 SpacetimeDB SDK 迁移后删除。
location ~ ^/api(?:/|$) {
default_type application/json;
# 中文注释:创作接口会携带参考图 Data URL,游戏发行包 PUT 更大,Nginx 只负责放行到 api-server;
# 真实大小限制仍由路由 DefaultBodyLimit(发行包 200 MiB + 1 KiB)和业务字节校验负责。
client_max_body_size 210m;
limit_conn genarrative_api_conn 64;
limit_req zone=genarrative_api_rps burst=64 nodelay;
if ($genarrative_maintenance) {
return 503 '{"ok":false,"error":{"code":"MAINTENANCE","message":"服务维护中"}}';
}
proxy_pass http://genarrative_api;
proxy_http_version 1.1;
proxy_buffering off;
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
add_header X-Accel-Buffering no always;
proxy_set_header Connection "";
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;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Request-Id $request_id;
}
# 生产公网不再暴露旧生成资源代理和健康检查入口。
location ~ ^/(generated-|healthz|readyz) {
return 404;
}
# SpacetimeDB 只开放 TypeScript SDK 运行所需的最小公网路由。
location ~ ^/v1/database/[^/]+/subscribe$ {
if ($genarrative_maintenance) {
return 503;
}
proxy_pass http://127.0.0.1:3101;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
proxy_set_header Host $host;
proxy_read_timeout 3600s;
}
location ^~ /v1/identity {
if ($genarrative_maintenance) {
return 503;
}
proxy_pass http://127.0.0.1:3101;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
proxy_set_header Host $host;
}
location ^~ /v1/ {
return 404;
}
# 平台同源路径发行入口:/games/<gameId>/ 与 /games/<gameId>/<asset> 映射到
# api-server 发行网关。游戏文档跑在 iframe sandbox="allow-scripts" 的不透明来源里,
# 离开页面即随 iframe 卸载,因此不再要求独立发行域名与通配证书。
location ~ "^/games/(?<game_id>game_[0-9a-f]{32})(?<game_path>/.*)?$" {
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;
proxy_set_header X-Request-Id $request_id;
proxy_set_header Cookie "";
proxy_pass http://genarrative_api/api/game-distribution/releases/$game_id$game_path;
proxy_read_timeout 60s;
proxy_send_timeout 60s;
}
# BEGIN GENARRATIVE MAIN SPA ROUTES
location = / {
error_page 503 /maintenance.html;
if ($genarrative_maintenance) {
return 503;
}
try_files /index.html =404;
}
location ~* "^/(?:creation|editor/canvas|pay|profile|profile/payment|project|components|design-system|creators|creators/connections|games|games/detail|games/mine|games/play|games/publish)/?$" {
error_page 503 /maintenance.html;
if ($genarrative_maintenance) {
return 503;
}
try_files $uri /index.html =404;
}
# 收银台深链 `/pay/<checkoutToken>`:token 由前端从最后一个路径段读取(payment.rs 生成该链接),
# 只放行裸前缀会让真实收银台链接落到默认 location 变 404;这里只放行「/pay/ + 恰好一个路径段」。
location ~* "^/pay/[^/]+/?$" {
error_page 503 /maintenance.html;
if ($genarrative_maintenance) {
return 503;
}
try_files $uri /index.html =404;
}
# END GENARRATIVE MAIN SPA ROUTES
location / {
error_page 503 /maintenance.html;
error_page 404 /404.html;
if ($genarrative_maintenance) {
return 503;
}
try_files $uri $uri/ =404;
}
}