Commit Graph

254 Commits

Author SHA1 Message Date
hailin 827b6d03c8 fix(kong): raise rate limit 100→2000 req/min to avoid 429 on admin panel
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-07 20:37:36 -08:00
hailin 856354690d fix(mobile/profile): fix HoldingsSummaryModel import path
Was importing from profile/data/models/ (nonexistent),
should be coupons/data/models/.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-07 20:22:56 -08:00
hailin 4065d586a9 feat(mobile/coupons): complete coupon holdings feature
- wallet_coupons_page: pass full CouponModel to detail route (was coupon.id)
- wallet_coupons_page: fix status filters (in_circulation=可使用, listed=挂售中)
- my_coupon_detail_page: full rewrite — StatefulWidget, accept CouponModel or String ID,
  real QR code via qr_flutter, barcode toggle, real data (faceValue/price/expiry/orderNo/resale),
  conditional action buttons per isTransferable + resaleCount, grey gradient for expired coupons
- profile_page: convert to StatefulWidget, load holdingsSummary on init, show real hold
  count & totalSaved in quick stats (tappable → /wallet/coupons), add "我的持仓" menu entry
- i18n: add myCoupon.switchQr + common.notFound to all 4 locales

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-07 20:09:31 -08:00
hailin c00c48c8bd fix(notification): accept optional type query param to fix 400 error
Backend entity has no type column yet; whitelist the type field in DTO
so forbidNonWhitelisted doesn't reject requests from the mobile app.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-07 11:50:35 -08:00
hailin af7fdfd428 fix(mobile/share): fix referral code overflow, remove team stats & team reward section
- Fix RIGHT OVERFLOWED BY 17 PIXELS: use Flexible on code Text,
  reduce letterSpacing 4→2, reduce horizontal padding
- Remove stats card (direct/team count) from page layout
- Remove 团队层级收益 reward item from reward plan card

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-07 11:46:13 -08:00
hailin 825e619224 fix(referral): auto-create profile for legacy users on getMyInfo
Users registered before referral-service was deployed have no profile row.
getMyInfo() now auto-provisions a profile on first access instead of
throwing NotFoundException.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-07 11:36:58 -08:00
hailin 13ab4ba1f3 fix(mobile/telemetry): add NativeAdapter to heartbeat & uploader, fix 2xx status check
- HeartbeatService: add NativeAdapter on Android to fix DNS via Java stack;
  fix status check from == 200 to >= 200 && < 300
- TelemetryUploader: same two fixes (refactor to _buildDio static method)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-07 11:29:36 -08:00
hailin 9e368cbf37 fix(mobile+backend): fix notification DNS, message page API bugs, add read-all
Android DNS fix (native_dio_adapter):
- Dart's getaddrinfo() fails on some Android 12+ devices when browser works
  fine — root cause is Dart native socket using different DNS path than
  Android Java stack; fix by using NativeAdapter (OkHttp) on Android
- Add native_dio_adapter ^1.5.1 to pubspec; enable in ApiClient for Android

Message page bugs (5 fixes):
- Fix offset->page: Flutter sent ?offset=0 but backend NotificationQueryDto
  uses page/limit (1-based); now sends ?page=1&limit=50
- Fix announcement tab API: Tab 2 now calls /api/v1/announcements (separate
  resource) instead of /api/v1/notifications?type=ANNOUNCEMENT
- Fix markAllAsRead routing: calls correct API based on active tab
- Fix markAsRead routing: announcement items use announcements API
- Fix detail navigation: passes NotificationItem as route argument

Backend notification-service:
- Add PUT /api/v1/notifications/read-all endpoint
- Add markAllReadByUserId() to repository interface + TypeORM implementation
- Add markAllRead() to NotificationService

i18n (4 languages): add time.justNow/minutesAgo/hoursAgo/daysAgo keys

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-07 11:15:03 -08:00
hailin 918489e4e5 fix(mobile/telemetry): buffer pre-init events instead of dropping them
Root cause: TelemetryService.initialize() is async (device info collection,
SharedPreferences I/O, remote config sync), taking several hundred ms to
complete. RouteObserver page_view events and auth events fire synchronously
during the initial navigation before initialization finishes, causing the
"TelemetryService not initialized, event ignored" drops seen in logs.

Fix: add _preInitBuffer (max 50 events). logEvent() now enqueues events
instead of dropping when _isInitialized=false. After initialization
completes, all buffered events are replayed in order via logEvent(),
ensuring no startup events are lost.

The network errors ("Failed host lookup: api.gogenex.com") are unrelated —
they are expected in offline test environments and handled gracefully.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-07 10:42:53 -08:00
hailin 9bcb1f864d feat(mobile): implement invite friends share page with APK QR code & referral marketing
- SharePage: QR code now encodes actual APK download URL from admin-service
  (GET /api/v1/app/version/download/{id} via Kong), referral code shown
  separately below QR for manual entry on registration
- Add referral reward marketing section: direct bonus / team levels (50
  layers) / new user welcome package — drives viral growth
- Show referrer info card when current user was invited via referral code
- Share text updated to marketing copy with APK link + referral code
- VersionChecker.getLatestApkUrl(): fetches latest APK URL regardless of
  needUpdate (passes version=0.0.0 to force server response)
- UpdateService.getLatestApkUrl(): exposes APK URL fetch for UI layer
- i18n (zh-CN/zh-TW/en/ja): add 14 new keys for marketing content,
  APK download hints, reward program descriptions, and referrer info

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-07 10:17:59 -08:00
hailin 192bc476c4 fix(mobile): move _startDownload to didChangeDependencies to avoid initState context crash
context.t() (Localizations) cannot be called during initState() — inherited
widgets are not yet mounted. When download fails instantly (e.g. no network),
setState with context.t() inside _startDownload fired synchronously within
initState, causing:
  dependOnInheritedWidgetOfExactType called before initState() completed

Fix: add _downloadStarted guard flag; call _startDownload() from
didChangeDependencies() instead of initState(), where context is fully ready.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-07 09:57:38 -08:00
hailin d79b7cf500 fix(mobile): fix telemetry import path in self_hosted_updater
../../../ → ../../ (channels/ → updater/ → core/, not lib/)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-07 09:45:20 -08:00
hailin 9519e29b8f feat(mobile/telemetry): collect real network type via connectivity_plus
- Add connectivity_plus: ^6.0.3 to pubspec.yaml
- DeviceInfoCollector._getNetworkType() calls Connectivity().checkConnectivity()
  and maps ConnectivityResult → wifi / mobile / ethernet / vpn / bluetooth / none / unknown
- networkType field in DeviceContext is now populated on every cold start
- All page_view and session events carry the accurate network type in device props

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-07 09:43:04 -08:00
hailin 9b760c56ee feat(mobile/telemetry): add comprehensive event tracking for DAU, real-time presence & funnels
## 架构设计(亿级用户规模)
- 客户端:本地队列(500条) + 批量上传(20条/30s) + 10% 采样率
- 心跳:前台每60s发送,后端3分钟滑动窗口 → 实时在线率
- DAU:app_session_start 事件 + installId 去重
- 后端接入:Kafka → ClickHouse 消费落盘

## 新增 TelemetryRouteObserver (NEW FILE)
- 继承 RouteObserver<PageRoute>,注册到 MaterialApp.navigatorObservers
- 自动记录所有页面 page_view(push/pop/replace),零侵入业务页面
- 属性:page_name, previous_page, nav_action
- 覆盖全部 37 个已注册路由,无需逐页埋点

## main.dart
- 接入 TelemetryRouteObserver.instance
- 新增 FlutterError.onError 全局钩子 → logError 上报 Widget build 异常

## auth_service.dart — 认证漏斗
| 事件 | 触发时机 | method 属性 |
|------|---------|------------|
| register_success | 注册成功 | phone_sms / email_code |
| login_success    | 登录成功 | password / phone_sms / email_code / wechat / alipay / google / apple |
| user_logout      | 主动登出 | — |

## self_hosted_updater.dart — 升级漏斗
update_prompted → update_accepted / update_dismissed
→ update_download_started → update_download_completed / update_download_failed / update_download_cancelled
→ update_install_triggered / update_install_failed

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-07 09:39:48 -08:00
hailin cc966fb022 feat(mobile): check for update on app resume from background
- Mix in WidgetsBindingObserver to detect foreground/background transitions
- On resumed: run a silent version check (no dialog) and only show the
  update dialog if a new version is actually available
- Throttle resume checks to once per 2 minutes to avoid excessive API calls
- Once the update dialog has been shown, skip further checks for the rest
  of the session; user won't be re-prompted until next cold start

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-07 09:29:46 -08:00
hailin d174b74764 debug(mobile): 下载失败诊断日志
添加详细 debugPrint,覆盖:
- URL 检查失败原因
- 实际下载 URL 和保存路径
- HTTP 响应状态码和 content-length/content-range
- DioException 类型、消息、HTTP 状态码
- SHA-256 校验结果(预期值 vs 实际值)
- 文件大小

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-07 08:53:05 -08:00
hailin 62c114c09f fix(kong): admin-service read/write timeout 60s → 600s
下载 53MB APK 在慢速移动网络下超过 60s,Kong 会断开连接。
改为 600s(10分钟)与 Nginx 保持一致。

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-07 08:47:06 -08:00
hailin daf32eaef1 fix(admin-service): Dockerfile 预建 /app/uploads 并 chown node:node
Docker named volume 首次挂载时会继承镜像中目标目录的属主。
若目录不存在或属主为 root,容器以 node(1000) 运行时无写权限。
在 USER node 之前创建目录并设置正确属主即可解决。

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-07 08:27:01 -08:00
hailin 7bb89b87ae fix(admin-service): 本地磁盘存储 + Docker volume 解决 EACCES 权限问题
回退 MinIO 方案,改用本地磁盘存储(与 git 历史及 rwadurian 一致)。
根本原因:容器内无权在 ./uploads 创建目录(EACCES)。
修复方案:
  - UPLOAD_DIR 默认改为 /app/uploads(明确绝对路径)
  - docker-compose 添加命名卷 admin-uploads 挂载到 /app/uploads
  - 挂载卷由 Docker 管理,容器拥有写权限

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-07 08:20:05 -08:00
hailin b9d2393aa1 fix(admin-service): 将文件存储从本地磁盘改为 MinIO
原实现将 APK/IPA 写入容器内 ./uploads 目录,导致 EACCES 权限错误。
改为通过 MinIO SDK 上传到 oss.gogenex.com / app-releases bucket,
与 docker-compose 中已有的 MINIO_* 环境变量保持一致。

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-07 08:15:42 -08:00
hailin 6236ff3632 fix(auth): 将 JWT access token 有效期从 15m 改为 24h
上传大文件(如 53.7MB APK/IPA)时,若 access token 在上传过程中
到期,服务端返回 401,导致前端被迫重传整个文件(极差 UX)。
实际业务场景下 15m 过短,统一改为 24h。

涉及文件:
- backend/services/auth-service/src/application/services/token.service.ts
- backend/services/auth-service/src/auth.module.ts
- backend/services/auth-service/.env.example
- backend/.env.example
- backend/docker-compose.yml(两处)
- backend/deploy.sh
- frontend/admin-web/src/views/compliance/IpoReadinessPage.tsx(移除废弃 insuranceData)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-07 08:06:37 -08:00
hailin 5bce676903 fix(admin-web): route parse/register through use-cases to satisfy no-restricted-imports
- ParsePackageUseCase.execute now accepts onProgress callback
- RegisterVersionUseCase added for the new /register endpoint
- use-upload.ts now imports only from application layer (no direct infra import)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-07 07:34:34 -08:00
hailin 7c8b79161a feat(upload): parse=upload+save+metadata, register=JSON only — no double upload
Previously the flow uploaded the 53MB file twice:
  1. POST /parse  → parse metadata (file discarded)
  2. POST /upload → parse again + save (file sent again)

New flow — file sent exactly once:
  1. POST /parse  → upload file, save to disk, parse metadata
                    returns {versionName, versionCode, minSdkVersion, storageKey, fileSize, fileSha256}
  2. POST /register → JSON only (no file), creates DB record using storageKey

Frontend:
- handleFileChange: async, immediately uploads to /parse with progress bar (0-100%)
- handleSubmit: calls /register with storageKey + form metadata (instant)
- Upload modal: real-time progress bar, "confirm" button disabled until parse complete
- Console logs at every step for debugging

Backend:
- POST /parse: saves file after parsing, returns storageKey in response
- POST /register: new endpoint, accepts JSON + storageKey, creates version record

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-07 07:19:28 -08:00
hailin 5ce4dd2442 fix(admin-web): save new refresh token after token rotation
refreshAccessToken() was discarding the new refresh token returned by
/auth/refresh, reusing the old (now-invalidated) one on next expiry.
This caused the second refresh to return 401, kicking the user to login
after just 15 minutes (two access token lifetimes).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-07 06:55:25 -08:00
hailin b285512c11 fix(admin-web): fix TS no-explicit-any lint error in http client 2026-03-07 06:47:02 -08:00
hailin 7ccbe33f88 fix(admin-web): only logout on explicit 401/403 from refresh endpoint
Previously any refresh failure (network error, service restart, timeout)
would clear localStorage and redirect to /login — kicking active users.
Now only a deliberate token rejection (HTTP 401/403) causes logout.
Transient errors are rejected silently without destroying the session.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-07 06:40:24 -08:00
hailin eca1490c72 debug(admin-service): add timing logs to parse and upload endpoints
Logs file size on receipt, parse duration, file save duration, and
total request time — to pinpoint where upload latency comes from.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-07 06:33:18 -08:00
hailin d48f1df0ff feat(admin-web): restore parse-first UX — disable upload until parse completes
File select now auto-parses the APK/IPA (sends to /parse endpoint),
auto-fills versionName/buildNumber/minOsVersion, and keeps the upload
button disabled (isParsing=true) until parsing finishes — matching
RWADurian's proven UX exactly.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-07 06:21:28 -08:00
hailin a6cb7add60 fix(admin-service): align APK/IPA parser with RWADurian implementation
Replace fragile internal ManifestParser approach with RWADurian's proven pattern:
- APK: write buffer to temp file → ApkReader.open(path) → readManifest() → cleanup
- IPA: unzipper.Open.buffer() → Info.plist → bplist-parser with XML fallback

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-07 06:08:47 -08:00
hailin c70b4bac6a perf(admin-web): remove separate parse request on file select
File was being uploaded twice:
  1. POST /parse on file select (for form auto-fill)
  2. POST /upload on submit

Remove the parse network call entirely. Server already parses the APK/IPA
buffer as part of the upload handler. User fills form fields manually.
Single upload, single public-internet transfer.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-07 06:03:59 -08:00
hailin 7933f3fe4a perf(upload): replace MinIO presigned-URL flow with local-disk storage
Previously the APK/IPA upload required two full public-internet transfers:
  1. POST /parse  → browser → gateway → admin-service (full file, for metadata)
  2. PUT presigned → browser → oss.gogenex.com / MinIO    (full file, to store)

Now follows the same pattern as RWADurian admin-service:
  - Single multipart POST /admin/versions/upload
  - admin-service parses buffer in-memory (yauzl / unzipper)
  - Saves to local disk (UPLOAD_DIR env, default ./uploads)
  - Download served via existing GET /app/version/download/:id (streams local file)

Changes:
  - file-storage.service.ts: drop minio dep, use fs/promises + crypto
  - admin-version.controller.ts: POST upload now accepts multipart file,
    removes GET presigned-url endpoint (no longer needed)
  - version.repository.ts (frontend): single FormData POST, removes
    three-step presigned-URL flow

Result: file crosses public internet once instead of twice.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-07 05:49:36 -08:00
hailin 839df343a4 fix(admin-web): disable backdrop click to close upload modal
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-07 04:48:35 -08:00
hailin b710d416d1 fix(admin-service): restore FileInterceptor imports for parse endpoint
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-07 04:44:04 -08:00
hailin 0f611cf8cd feat(upload): presigned URL — browser uploads directly to MinIO
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-07 04:41:38 -08:00
hailin 3b4ee26a33 fix(admin-service): use oss.gogenex.com (128.14.98.146) for MinIO
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-07 04:16:15 -08:00
hailin 9e3c294e03 fix(admin-service): use oss.gogenex.com domain with extra_hosts for MinIO
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-07 03:59:52 -08:00
hailin 1d611f9a5f fix(kong): use host network mode to reach internal microservices
Kong container uses network_mode:host so it shares the gateway's
network namespace and can reach 192.168.1.222:PORT directly.
Listen on 127.0.0.1:48080 (local only, Nginx proxies externally).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-07 03:52:31 -08:00
hailin 099c1fe49c infra: move Kong to gateway server, fix MinIO internal path
- infrastructure/kong/: Kong declarative config for gateway server
  All service URLs use http://192.168.1.222:PORT (internal server)
  admin-service gets extended timeouts (300s) for large uploads
- docker-compose.yml: admin-service uses MINIO_ENDPOINT=192.168.1.200:9200
  Plain HTTP via Nginx internal proxy (no SSL, no extra_hosts needed)

New upload path:
  Browser → Nginx:443 → Kong:48080 (local) → admin-service(LAN) → MinIO:9200(local)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-07 03:41:45 -08:00
hailin 9e07efc54c fix(mobile): remove back button on MarketPage and fix transfer row overflow
- MarketPage: add automaticallyImplyLeading: false (tab-level page)
- TransferPage: wrap maskedContact in Flexible to prevent RIGHT OVERFLOW

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-07 03:07:50 -08:00
hailin 75ef11ec22 fix(admin-service): use extra_hosts to route MinIO via gateway Nginx
Container maps oss.gogenex.com → 192.168.1.200 (LAN IP) so it
connects to Nginx:443 which proxies to localhost:9100 (MinIO).
Port 443 is already open in UFW; avoids hairpin NAT and raw iptables
drop rules that block direct access to 192.168.1.200:9100.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-07 02:20:16 -08:00
hailin fa64e1863a fix(admin-service): use internal IP for MinIO to avoid hairpin NAT
admin-service runs on 192.168.1.222 (LAN). Connecting to MinIO via
public IP 154.84.135.121 fails with ETIMEDOUT (hairpin NAT). Use
internal gateway IP 192.168.1.200:9100 (no SSL) for S3 API calls.
Public download URLs still use https://oss.gogenex.com.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-07 01:20:08 -08:00
hailin 0c3ef3b598 fix(mobile): fix version check API and platform detection in both apps
genex-mobile/lib/core/updater/version_checker.dart
- platform 从写死 'android' 改为运行时检测 Platform.isIOS ? 'IOS' : 'ANDROID'

admin-app/lib/core/updater/version_checker.dart
- API 路径修正: /api/app/version/check → /api/v1/app/version/check
- 新增 app_type: 'ADMIN_APP'(原缺失,后端默认返回 GENEX_MOBILE 版本)
- platform 从写死 'android' 改为运行时检测
- 响应解析修正: 原逻辑把外层 {code,data} 当版本数据用,
  改为正确取 response.data['data'] 内层对象
- 新增 downloadUrl 相对路径修复(向下兼容旧记录)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-07 00:50:42 -08:00
hailin 7ba5401e2f feat(infra): use oss.gogenex.com for app version download URLs
将应用版本的文件下载链接从 API 代理路径改为直接指向对象存储域名。

backend/admin-service (admin-version.controller.ts)
- uploadVersion 上传成功后,downloadUrl 改为:
    https://oss.gogenex.com/app-releases/<storageKey>
  (原:/api/v1/app/version/download/:id 代理路径)
- 读取 OSS_BASE_URL 环境变量,默认 https://oss.gogenex.com

backend/docker-compose.yml
- admin-service 新增 OSS_BASE_URL=https://oss.gogenex.com

infrastructure/minio/deploy.sh
- app-releases bucket 加入公开下载列表
  (APK/IPA 需被移动端直接下载,无需鉴权)

frontend/admin-web
- .env.production 新增 NEXT_PUBLIC_OSS_URL=https://oss.gogenex.com
- .env.development 新增 NEXT_PUBLIC_OSS_URL=https://oss.gogenex.com

MinIO 现状:
  app-releases bucket 已在服务器上设为 anonymous download

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-07 00:42:05 -08:00
hailin 4c5f5e2cd9 fix(admin-service): update MinIO default endpoint to oss.gogenex.com
将 FileStorageService 的默认连接参数从 localhost 更新为生产域名:
- endPoint 默认值: localhost → oss.gogenex.com
- port 默认值: 9000 → 443
- useSSL 默认值: false → true(?? 运算符保证未设置时默认开启)
- accessKey/secretKey 默认值更新为生产凭证

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-07 00:22:53 -08:00
hailin 11bb88badd feat(infra): configure oss.gogenex.com domain for MinIO object storage
按域名规划文档为 MinIO 配置专属域名访问。

变更内容:

DNS(Namecheap)
- 新增 A 记录:oss.gogenex.com → 154.84.135.121

Nginx(网关服务器 14.215.128.96)
- 新增 /etc/nginx/conf.d/oss.gogenex.com.conf
- 反代配置:oss.gogenex.com → localhost:9100
- client_max_body_size 500m(支持大文件 APK/IPA 上传)
- proxy_request_buffering off(流式上传,不缓冲至磁盘)
- Let's Encrypt SSL 证书已签发

backend/docker-compose.yml
- MINIO_ENDPOINT: 192.168.1.200 → oss.gogenex.com
- MINIO_PORT: 9100 → 443
- 新增 MINIO_USE_SSL=true

infrastructure/minio/docker-compose.yml
- 补充域名访问注释说明

服务器现状:
  admin-service 已重启,MinIO 通过 https://oss.gogenex.com 访问。

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-07 00:16:51 -08:00
hailin 3b60fed078 feat(infra): migrate MinIO object storage to gateway server
将 MinIO 对象存储从内网服务器(192.168.1.222)迁移至入口网关服务器
(192.168.1.200 / 14.215.128.96),作为独立基础设施部署。

变更内容:

backend/docker-compose.yml
- 移除 minio 和 minio-init 服务(不再随后端栈启动)
- admin-service 的 MINIO_ENDPOINT 改为 192.168.1.200,端口改为 9100
- 移除 admin-service 对 minio 服务的 depends_on 依赖
- 删除 minio_data docker volume 声明

infrastructure/minio/docker-compose.yml(新增)
- MinIO 独立部署配置
- S3 API  : 9100(映射容器内 9000)
- Console : 9101(映射容器内 9001)
- 数据持久化到宿主机 /data/minio

infrastructure/minio/deploy.sh(新增)
- 支持 up / down / status / init / logs 命令
- up 时自动创建全部 7 个 bucket:
    app-releases, kyc-documents, coupon-images,
    issuer-documents, sar-reports, avatars, exports
- coupon-images / avatars 设为匿名可下载

部署说明:
  # 在网关服务器上首次部署
  cd infrastructure/minio && ./deploy.sh up

  # 查看状态
  ./deploy.sh status

服务器现状:
  genex-minio 已在 14.215.128.96 上运行,所有 bucket 已初始化。
  admin-service 已重启并指向新地址,文件上传恢复正常。

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-07 00:03:01 -08:00
hailin 9ed0d7e739 fix(kong): revert unnecessary KONG_NGINX_PROXY_PROXY_REQUEST_BUFFERING env var
Root cause was proxy_request_buffering off in Nginx gateway (already removed).
Kong should use default settings, matching IT0 reference architecture.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-06 18:48:27 -08:00
hailin 19ad0d39fe fix(kong): disable request buffering for large file uploads
Add KONG_NGINX_PROXY_PROXY_REQUEST_BUFFERING=off so Kong streams
the request body to admin-service without buffering to disk.

Root cause: Nginx streams (proxy_request_buffering off) → Kong buffers
to disk → Kong returns 400 with empty body when forwarding to upstream.
Fix: Kong also streams, matching Nginx's streaming behavior end-to-end.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-06 12:00:30 -08:00
hailin 5d9e1f7d06 fix(dashboard): handle nested API response shapes for trades and system-health
Backend wraps data in extra layer:
  system-health   → {code:0, data:{services:[...]}}
  realtime-trades → {code:0, data:{items:[...], total:N}}

HttpClient strips outer data but leaves inner object.
Fix: type as {services/items} and access nested arrays.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-06 11:34:49 -08:00
hailin 45a69491d7 fix(admin-web): resolve ESLint CA boundary violations blocking build
- auth.store: eslint-disable with explicit comment for intentional infra access
  (session orchestration is a designated cross-layer boundary)
- Add auth.use-cases.ts (LoginUseCase / LogoutUseCase) for use by views/hooks
- Fix no-explicit-any in AppVersionManagementPage (use unknown + type assertion)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-06 11:29:04 -08:00