Commit Graph

104 Commits

Author SHA1 Message Date
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 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 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
hailin 81050767da feat(admin-web): add ESLint flat config with Clean Architecture layer boundary enforcement
- eslint.config.mjs: ESLint 9 flat config with per-layer no-restricted-imports rules
- Domain: no outward deps; Infrastructure: domain only; Application: domain+infra;
  Store: domain only; Presentation: no direct infra access
- Fix no-explicit-any in use-upload.ts (use unknown + type assertion)
- Add lint:boundaries npm script for CI enforcement

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-06 11:23:00 -08:00
hailin e92059fc75 refactor(admin-web): add Presentation hooks layer for app-versions
- useVersionList: React Query + Use Case, select guard, invalidate helper
- useVersionMutations: toggle/delete wrapped with onSuccess callback
- useUpload: parse+upload flow extracted from modal component
- AppVersionManagementPage: purely declarative, zero business logic in JSX

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-06 11:17:26 -08:00
hailin 3765e8e6b1 refactor(admin-web): strict Clean Architecture for app-versions feature
Domain → Infrastructure → Application (Use Cases) → Presentation

- Domain: fix AppVersion entity fields; add IVersionRepository interface
- Infrastructure: VersionRepository implements IVersionRepository via HttpClient
- Application: 6 Use Case classes (ListVersions/Parse/Upload/Update/Toggle/Delete)
- Presentation: RTK version.slice (filters/modal state) + Zustand upload.store (form state)
- Page: zero direct apiClient calls; React Query queryFn calls use cases

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-06 11:11:56 -08:00
hailin dca2031a38 fix(http-client): delete Content-Type in request interceptor when data is FormData
Instance-level default Content-Type: application/json was overriding
browser's auto-generated multipart/form-data boundary. Remove it for
FormData so browser sets correct Content-Type with boundary.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-06 11:03:00 -08:00
hailin fc85983b43 fix(upload): remove explicit Content-Type header so browser sets multipart boundary
Without boundary multer receives undefined file. Also add guards in
backend parse/upload to avoid crash if file is missing.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-06 10:56:58 -08:00
hailin 30393c2867 fix(admin-web): guard versions list with Array.isArray + bump parse timeout to 300s
- Prevent TypeError if useApi returns non-array shape
- Add HttpClient.get logging to trace raw vs unwrapped response
- Parse timeout: 120s → 300s (matches upload, avoids timeout on large files)
- Show hint for large files (>30MB) during parse

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-06 10:48:29 -08:00
hailin e011eacbe6 fix(admin-web): add Content-Type multipart/form-data to parse and upload calls
Same pattern as rwadurian mobile-upgrade version-repository-impl.ts

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-06 10:22:09 -08:00
hailin 07c171ce22 fix(admin-web): auto token refresh + restore APK parse with warnings
- auth.store: persist refreshToken alongside accessToken
- http.client: on 401, auto-refresh token and retry original request
  with mutex lock to prevent concurrent refresh calls; only redirect
  to /login if refresh itself fails
- upload modal: restore auto-parse on file select; show warning if
  parse fails; add console logs for debugging; fix button disabled
  during parsing

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-06 10:18:31 -08:00
hailin 9a40769e0d fix(admin-web): remove double-upload on app version page
- Remove auto-parse on file select (was uploading 48MB twice, took 100+ sec)
- Backend /upload already parses APK internally, version fields are now optional
- Show file name + size after selection
- Show progress hint during upload
- Better error extraction from API response
- Clear error when new file is selected

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-06 10:11:07 -08:00
hailin 4309e9e645 fix(admin-web): remove manual Content-Type header for multipart upload
Manually setting Content-Type: multipart/form-data without the boundary
causes the server to reject the request. Axios automatically sets the
correct header with boundary when FormData is passed.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-06 09:53:52 -08:00
hailin 4369aecf60 fix: 修复上传版本500错误 + 优化App冷启动通知请求
- fix(admin-service): versionCode 兜底从 Date.now() 改为 1,避免超出 PostgreSQL integer 范围
- fix(genex-mobile): NotificationBadgeManager 加登录检查,未登录跳过API请求
- fix(genex-mobile): 将通知徽章初始化移至首帧后执行,消除冷启动DNS竞争

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-06 08:48:09 -08:00
hailin ce9cc5b72e feat(telemetry): 完整实现 presence-service 迁移 + Flutter 遥测接入
## 后端 (backend/services/telemetry-service)

### 修复骨架中的关键缺陷
- 创建本地 JwtAuthGuard / AdminGuard,替换不存在的 @genex/common 包
- 修复 req.user.sub → req.user.id(与 JwtStrategy.validate() 返回值对齐)
- 重写 Dockerfile,移除对不存在的 packages/common 目录的引用
- 更新 telemetry.module.ts,将 JwtAuthGuard / AdminGuard 注册为 providers
- admin-telemetry.controller.ts 改用本地 AdminGuard(检查 role === 'admin')

### 新增功能
- GET /api/v1/telemetry/config:公开配置下发接口
  Flutter TelemetryConfig.syncFromRemote() 每小时拉取,返回全局开关、采样率、
  心跳间隔等配置,所有字段支持环境变量覆盖(TELEMETRY_GLOBAL_ENABLED 等)
- 设备字段(deviceBrand/deviceModel/deviceOs/appVersion/locale)提升为顶层列
  BatchEventsDto / TelemetryEvent 实体 / recordEvents() 全链路补齐
  原因:JSONB 内字段无法走 B-tree 索引,千万级数据分组查询需独立列

### DB 迁移
- 050_add_device_fields_to_telemetry_events.sql:为已有 telemetry_events 表
  新增 5 个设备字段列和 device_brand / app_version 索引

### docker-compose 环境变量
- telemetry-service 新增 TELEMETRY_GLOBAL_ENABLED / SAMPLING_RATE /
  HEARTBEAT_INTERVAL / CONFIG_VERSION 环境变量,支持生产环境热调整

## 前端

### genex-mobile (Flutter 消费者端)
- 复制 lib/core/telemetry/ 模块(11个文件):TelemetryService、
  HeartbeatService、SessionManager、TelemetryUploader、TelemetryStorage 等
- 修正 API 路径:presence/heartbeat → telemetry/heartbeat,
  analytics/events → telemetry/events
- pubspec.yaml 新增依赖:uuid ^4.3.3、equatable ^2.0.5、device_info_plus ^10.1.0
- main.dart:initState 首帧回调初始化 TelemetryService(需 BuildContext 采集设备信息)
  已登录时自动注入 accessToken
- auth_service.dart:_setAuth() 登录成功后注入 userId + accessToken;
  _clearAuth() 退出时清除(同时覆盖 Token 过期自动清除场景)

### admin-app (Flutter 发行方控制台)
- 复制 lib/core/telemetry/ 模块(同上)
- pubspec.yaml 新增依赖:uuid、equatable、device_info_plus、shared_preferences
- IssuerLoginPage:initState 首帧初始化 + 登录成功后注入 userId/token
  使用 api.gogenex.cn(与 UpdateService 域名一致)
- settings_page.dart:退出登录时调用 clearUserId() + clearAccessToken()

## 架构说明
- 在线人数:Redis Sorted Set (genex:presence:online),心跳 60s/次,180s 窗口判定在线
- DAU:app_session_start 事件写入 telemetry_events,每天凌晨 1 点聚合到
  daily_active_stats 表,同时每小时滚动更新当日 DAU
- 设备字段采用 Amplitude 风格:前端本地队列存 properties 内,
  toServerJson() 上传时自动提升为顶层字段,后端写入独立索引列
- 心跳需要 JWT 认证(未登录用户自动跳过,不报错)
- 遥测完全异步,任何失败只打 debug 日志,不影响主流程

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-05 23:33:13 -08:00
hailin 8f3d0f5d17 fix: comprehensive API compatibility fixes across issuer/user/ai services and Flutter
Backend:
- issuer-service: fix credit-metric entity column names to match DB schema
  (breakage_ratio, market_tenure_months, user_satisfaction, computed_score, etc.)
- issuer-service: add page/limit to ListStoresQueryDto and ListEmployeesQueryDto
- issuer-service: add period field to ReconciliationQueryDto
- issuer-service: fix IssuerStatsController to use req.user.id→issuerId lookup
- issuer-service: add analytics stubs (users/demographics/repurchase/ai-insights)
- issuer-service: add issuers/me/coupons endpoint
- user-service: add GET /users/payment-methods stub before /:id route
- ai-service: add GET /ai/sessions/current/messages stub endpoint

Flutter:
- genex-mobile: fix /users/kyc/status → /users/kyc
- genex-mobile: fix announcements offset→page param
- genex-mobile: fix trading paths (/trading/* → /trades/*)
- admin-app: fix announcements offset→page param

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-05 09:29:44 -08:00
hailin 3180114469 fix(admin-app): use correct desugar_jdk_libs artifact
Wrong: com.android.tools.build:desugaring-api:2.0.4
Correct: com.android.tools:desugar_jdk_libs:2.1.4 (matches genex-mobile)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-04 23:34:59 -08:00
hailin 4a2e6182be fix(admin-app): enable core library desugaring for flutter_local_notifications
flutter_local_notifications requires coreLibraryDesugaringEnabled = true
and the desugaring-api dependency to build on Android.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-04 23:33:09 -08:00
hailin 89dbdb55b8 fix(admin-app): fix dynamic→int type errors in service files
- notification_service: cast total/unreadCount in NotificationListResponse,
  cast count in getUnreadCount/getAnnouncementUnreadCount (Future<int>)
- auth_service: fix unsafe (value ?? 300) as int → (value as int?) ?? 300

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-04 23:31:35 -08:00
hailin af5aba8efe fix(admin-app): fix 4 compile errors
- credit_page.dart: remove const from Padding containing context.t() call
- issuer_coupon_service/redemption_service/issuer_finance_service:
  cast inner['total'] to int? to match named record return type

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-04 23:23:39 -08:00
hailin 3d0223d10a fix: add passport-jwt deps to 3 services + fix messages page UI
Backend:
- Add passport/passport-jwt/@types/passport-jwt to clearing-service,
  compliance-service, notification-service package.json (missing deps
  caused 'Cannot find module passport-jwt' build failure)

Flutter:
- MessagePage: automaticallyImplyLeading: false (no back btn on tab page)
- TabBar: isScrollable+tabAlignment.start to prevent 'Announcements' truncation

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-04 23:15:00 -08:00
hailin 9a4c2c1c19 fix: add missing issuer-service JwtStrategy + fix home search bar overflow
- Add issuer-service/infrastructure/strategies/jwt.strategy.ts (was omitted
  from previous commit, causing build failure)
- Wrap search hint Text in Expanded to prevent Row overflow

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-04 23:11:10 -08:00
hailin 5113975c72 fix(genex-mobile): 首页移除返回键并修复搜索栏溢出
SliverAppBar 添加 automaticallyImplyLeading: false,
防止 Tab 导航进入时显示返回键,并修复返回键占位导致的搜索栏 overflow 6px。

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-04 23:02:34 -08:00
hailin 75ed31cc04 fix: 全服务注册 JwtStrategy + 修复微信 WxApi 未配置 crash
**后端**
- packages/common: 新增并导出 JwtStrategy(共享 Passport JWT 策略)
- 6 个服务模块(user/issuer/clearing/compliance/notification/telemetry)
  均缺少 JwtStrategy provider,导致所有受保护接口返回 500
  "Unknown authentication strategy jwt"
- 统一修复:各模块 providers 添加 JwtStrategy

**Flutter**
- welcome_page: _onWechatTap() 的 isWeChatInstalled 调用未设 WECHAT_APP_ID
  时会抛出 PlatformException,catch 后降级为"未安装"提示

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-04 22:24:07 -08:00
hailin 4957b2ef85 feat(genex-mobile): 重构为 Clean Architecture + Riverpod
- 添加 flutter_riverpod ^2.5.1 依赖
- 搭建 core/error/failures.dart 和 core/usecases/usecase.dart 基础骨架
- auth feature 完整 3 层重构:
  - domain: AuthUser + AuthSession 实体, IAuthRepository 接口, 全套 UseCases
  - data: AuthRepositoryImpl(Strangler Fig,委托给 AuthService 保留 token 刷新逻辑)
  - presentation: AuthNotifier + authProvider + currentUserProvider + isAuthenticatedProvider
- 4 个 auth 页面升级为 ConsumerStatefulWidget,使用 ref.read(authProvider.notifier)
- main.dart: ProviderScope 包裹 GenexConsumerApp,改为 ConsumerStatefulWidget
  - 桥接 AuthService ValueNotifier → Riverpod authProvider(会话过期自动导航)
- 12 个 feature 全部创建 Riverpod providers(FutureProvider/NotifierProvider)
- 修复 my_coupons_page.dart 中 EmptyState/StatusTags 缺少 BuildContext 的预存在错误

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-04 20:12:50 -08:00
hailin 332a8dafe8 fix(admin-web): 补回 AdminLayout useState import
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-04 19:20:05 -08:00
hailin 4feea2667c refactor(admin-web): 实现 Clean Architecture + Zustand + Redux Toolkit
按要求重构架构,从扁平的 React Context + useState 升级为大厂标准模式:

Clean Architecture 分层:
  domain/entities/          — 业务实体 (AdminUser/User/Issuer/AppVersion)
  domain/repositories/      — Repository 接口(契约层)
  infrastructure/http/      — HttpClient(替代旧 api-client.ts)
  infrastructure/repositories/ — Repository 实现(AuthRepository/UserRepository)

状态管理(大厂混合模式):
  Zustand useAuthStore      — 轻量客户端状态:登录会话 + localStorage 持久化
  Zustand useUIStore        — UI 偏好:sidebar 折叠状态持久化
  Redux uiSlice             — 全局通知队列、globalLoading
  Redux usersSlice          — 用户列表筛选/分页 client state
  React Query               — 服务端数据 fetching/缓存(保留)

更新:
  providers.tsx             — 加入 Redux Provider,移除旧 AuthProvider
  auth-context.tsx          — 向下兼容层,re-export Zustand store
  api-client.ts             — 向下兼容层,re-export httpClient
  AdminLayout.tsx           — 使用 Zustand auth/ui store

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-04 19:18:20 -08:00
hailin 96dad278ea fix(admin-web): 修复 React #310 — useState 必须在条件 return 前调用
根因:expandedKeys 的 useState 调用位于 "if (!isAuthenticated) return null"
之后,违反 Rules of Hooks。认证状态变化时 hooks 调用数量不一致,
React 报 #310 "Cannot update a component while rendering a different component"。

修复:将 activeKey 计算和 expandedKeys useState 全部移至条件 return 之前,
确保每次渲染 hooks 调用顺序完全一致。

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-04 19:05:34 -08:00
hailin 300d55ff14 fix(admin-web): 部署版本守卫,彻底防止 stale bundle 崩溃
方案:
- next.config.ts 构建时注入 NEXT_PUBLIC_BUILD_TIME 时间戳
- 新增 /api/version 路由,返回服务器当前 build 时间戳
- DeployGuard 组件每 3 分钟轮询 /api/version,
  发现版本变化立即 window.location.reload(),用户完全无感知
- global-error / admin error 边界仅做最后兜底(静默 reload)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-04 18:57:12 -08:00
hailin 36b899d7ed fix(admin-web): 简化错误边界为静默 reload,去掉多余 UI
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-04 18:55:19 -08:00
hailin f642ef1d56 fix(admin-web): 添加 global-error.tsx 修复部署后 Server Action 崩溃
问题:每次重新部署容器后,浏览器持有旧 bundle,Next.js App Router
内部 Server Action ID("r"/"multi")与新服务器不匹配,导致客户端抛出
未捕获异常,触发全屏 "Application error"(周期性崩溃根因)。

修复:
- 添加 src/app/global-error.tsx(根级错误边界),检测到 stale bundle
  相关错误时自动调用 window.location.reload(),无感知恢复
- 添加 src/app/(admin)/error.tsx(admin 路由段错误边界),同样自动刷新
- 两个边界均提供「立即刷新」「重试」按钮,防止极端情况下自动刷新失效

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-04 18:50:22 -08:00
hailin 4878449f8c fix(genex-mobile): WXEntryActivity 完整转发微信回调 Intent
用 Intent(intent) 拷贝构造完整复制原始 Intent(action / data / extras),
再通过 setClass 重定向到 MainActivity,确保 fluwx 5.x 的
WXAPiHandler.handleIntent() 能读取到完整的微信授权回调内容。

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-04 18:05:01 -08:00
hailin 5d72d9bd0b fix(genex-mobile): 修复 WXEntryActivity fluwx 5.x 编译错误
fluwx 5.x 将包名从 com.jarvanmo.fluwx 改为 com.jarvan.fluwx,
并移除了 WXEntryActivity 基类,导致编译报错:
  Unresolved reference 'jarvanmo'
  Cycle in supertypes detected

改为「中继(relay)」模式:
- WXEntryActivity 继承 Activity(不再继承 fluwx 基类)
- onCreate 时将微信回调 Intent extras 转发给 MainActivity
- fluwx 5.x 的 FluwxPlugin 实现了 PluginRegistry.NewIntentListener,
  MainActivity.onNewIntent() 会触发 FluwxPlugin 解析微信回调

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-04 18:03:16 -08:00
hailin 828770add8 fix(alipay): 适配 tobias 5.x 新 auth API,后端生成签名 authString
tobias 3.x+ 移除了顶层函数 aliPayAuth(appId, scope),
改为需要后端预签名的 Tobias().auth(authString)。

变更:
- alipay.provider.ts: 新增 generateMobileAuthString(scope) 方法,
  用 RSA2 私钥生成符合 Alipay SDK 格式的签名授权字符串
- auth.controller.ts: 新增 GET /auth/alipay/auth-string 接口
- pubspec.yaml: tobias ^3.0.0 → ^5.0.0
- auth_service.dart: 新增 getAlipayAuthString() 方法
- welcome_page.dart: 更新支付宝登录流程,先获取 authString 再调用 tobias

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-04 09:26:30 -08:00
hailin a27baa1181 docs: 完善 fluwx 5.x 迁移与 AdminLayout 跳转备注
- main.dart: 说明 fluwx 5.x 实例共享机制与迁移前后对比
- welcome_page.dart: 说明 addSubscriber/NormalAuth/WeChatAuthResponse 用法,
  以及 fluwx 3.x → 5.x API 对照
- AdminLayout.tsx: 说明 useEffect 跳转的原因(React #310 错误根因)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-04 09:00:18 -08:00
hailin 2e66db08ef fix(genex-mobile): 升级 fluwx 3.x → 5.x,适配新版 API
fluwx 3.x 不含 Android namespace,导致新版 AGP 构建失败。
升级至 5.x 并迁移 API:
- registerWxApi → Fluwx().registerApi()
- weChatResponseEventHandler → fluwx.addSubscriber()
- sendWeChatAuth → fluwx.authBy(NormalAuth)
- isWeChatInstalled → fluwx.isWeChatInstalled
- WXAuthResp → WeChatAuthResponse

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-04 08:42:56 -08:00
hailin 7aea49a0c9 fix(admin-web): 修复 AdminLayout 渲染期间调用 router.replace 导致 React #310 错误
将未登录跳转逻辑从 render 函数移至 useEffect,
避免在渲染子组件时触发父组件状态更新。

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-04 05:43:58 -08:00
hailin 91f06890a5 docs(auth): 完善三方登录模块注释 — 含申请步骤与算法说明
AlipayProvider:
- callGateway: 补充完整请求参数结构说明
- sign: 详细说明 RSA2 签名5步骤(字典序→拼接→SHA256WithRSA→Base64)
- chunkBase64: 说明 PEM 格式每64字符换行要求

AppleProvider:
- verifyIdentityToken: 详细说明 claims 验证逻辑和签名验证原理
- getAppleJWKS: 说明 JWKS 缓存策略和 Apple 密钥轮换机制
- jwkToPublicKeyPem: 说明 JWK→SPKI PEM 转换过程
- base64urlDecode: 说明 Base64URL 与标准 Base64 的区别

GoogleProvider:
- verifyIdToken: 说明 tokeninfo vs JWKS 本地验证的权衡,补充响应字段说明

welcome_page.dart:
- 类头注释: 补充平台配置要求和各登录方式申请前提
- _onAlipayTap: 补充 tobias v3 API 结构、result 格式解析、scope 含义、iOS/Android 配置要求
- _parseParam: 说明 URI query 解析的原因
- _onGoogleTap: 补充 signIn 返回 null 的含义、idToken 说明、Android 配置
- _onAppleTap: 补充 Apple 3 项重要限制(姓名/邮箱只在首次授权时返回)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-04 04:56:11 -08:00
hailin 1fc0fcb95e feat(auth): 支付宝 + Google + Apple 三方登录
后端 (auth-service):
- 新增 AlipayProvider (RSA2签名) + AlipayService + POST /auth/alipay
- 新增 GoogleProvider (tokeninfo验证) + GoogleService + POST /auth/google
- 新增 AppleProvider (JWKS验证ES256 JWT) + AppleService + POST /auth/apple
- SocialProvider 枚举新增 ALIPAY
- .env.example 补充三方登录申请步骤文档

Flutter (genex-mobile):
- pubspec.yaml: 新增 tobias / google_sign_in / sign_in_with_apple
- auth_service.dart: loginByAlipay / loginByGoogle / loginByApple
- welcome_page.dart: Android=微信+支付宝+Google, iOS=+Apple
- AndroidManifest: 添加支付宝包名查询
- Info.plist: 支付宝 URL Scheme + alipay/alipays queries
- i18n: 4 语言补充失败提示文案

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-04 04:40:31 -08:00
hailin 2790d4c226 fix(genex-mobile): Apple 登录按钮仅在 iOS 上显示
Apple Sign In 是 iOS/macOS 生态专属功能,Android 用户不存在
Apple ID 账号体系,不应在 Android 上展示该入口。

使用 defaultTargetPlatform == TargetPlatform.iOS 条件渲染:
- Android: WeChat + Google(2个按钮)
- iOS:     WeChat + Google + Apple(3个按钮)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-04 04:21:34 -08:00
hailin 44de21a733 docs(auth): 完善微信登录模块注释 — 含申请移动应用完整步骤
wechat.provider.ts:
  - 补充微信开放平台申请移动应用的完整流程(企业资质、创建应用、
    获取 AppID/AppSecret、启用 unionid、获取 Android 签名 MD5)
  - 说明常见 errcode 含义(40029/42003/40163)
  - 安全注意事项(AppSecret 保密、服务端换 token、防重放建议)

wechat.service.ts:
  - 完整业务流程注释(6步,含新老用户分支逻辑)
  - unionid 优先策略原理(跨 App 唯一性,防重复注册)
  - 每次登录同步微信信息说明
  - 自动生成账号字段说明(nickname/email/phone/password/kycLevel)

.env.example:
  - 微信开放平台申请步骤(注册/认证/创建/获取/启用 unionid)
  - Android 签名 MD5 获取命令
  - Flutter --dart-define 构建参数说明
  - Universal Links 说明

WXEntryActivity.kt:
  - 包名路径规范说明(applicationId vs namespace 区别)
  - AndroidManifest 配置要求
  - 回调未触发的排查步骤(签名/包名/debug包名)
  - 获取 Android 签名 MD5 命令

main.dart:
  - registerWxApi 传参说明(--dart-define 用法)
  - 未配置 WECHAT_APP_ID 时的降级行为
  - Universal Links 配置说明

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-04 04:04:26 -08:00
hailin d68d48cb95 feat(auth): 微信登录 / 注册完整实现 — social_accounts + fluwx 全链路
后端:
- 新增 social_accounts 表 Entity/Repository/Migration (049)
- WechatProvider: code ↔ access_token / 获取用户信息 (native https)
- WechatService: unionid 优先查找 → 自动登录/注册 → 发布事件
- POST /auth/wechat 端点 (WechatLoginDto, referralCode 支持)
- auth.module.ts 注册 SocialAccount、WechatProvider、WechatService

Flutter (genex-mobile):
- pubspec.yaml: 添加 fluwx ^3.10.0
- main.dart: registerWxApi 初始化 (WECHAT_APP_ID via --dart-define)
- AuthService: loginByWechat(code, referralCode?, deviceInfo?)
- WelcomePage: 改为 StatefulWidget,监听 weChatResponseEventHandler
  微信按钮触发 sendWeChatAuth,授权成功后自动登录 → /main
  未安装微信 / 登录失败均有 SnackBar 提示
- 4语言 i18n: wechatNotInstalled / wechatLoginFailed

Android:
- AndroidManifest: WXEntryActivity + queries(com.tencent.mm)
- WXEntryActivity.kt: 继承 fluwx 提供的基类,无额外代码
- proguard-rules.pro: keep WeChat SDK 类

iOS:
- Info.plist: CFBundleURLTypes (wx${WECHAT_APP_ID}) + LSApplicationQueriesSchemes

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-04 03:37:51 -08:00
hailin 9473512530 feat(auth): 邮箱注册完整实现 — Gmail SMTP + 邮件验证码全链路
后端 (auth-service):
- 新增 EmailVerification / EmailLog 实体 + TypeORM 映射
- Email 值对象:格式校验、小写归一化、脱敏展示
- Gmail SMTP Provider (nodemailer) + ConsoleEmailProvider (dev)
- EmailCodeService:Redis 缓存快速路径,与 SmsCodeService 对称
- EmailService:sendCode/verifyCode + 日限额 + 业务规则校验
- 新增端点:POST /auth/email/send / register-email / login-email / reset-password-email
- EMAIL_ENABLED 环境变量切换真实/控制台发送
- 数据库迁移:048_create_email_verifications.sql

前端 (genex-mobile):
- AuthService 新增 sendEmailCode / registerByEmail / loginByEmail / resetPasswordByEmail
- RegisterPage 根据 isEmail 参数自动切换 SMS/Email API 调用
- WelcomePage 邮箱注册按钮传递 isEmail:true 参数
- i18n 新增 register.errorEmailRequired(4语种)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-04 02:27:43 -08:00
hailin a893dbdb1b feat(genex-mobile): Token 持久化 — 登录状态跨重启保持
## 核心变更

### 新增 SessionStorage (lib/core/storage/session_storage.dart)
- flutter_secure_storage 封装层
- Android: EncryptedSharedPreferences(AES-256,Android Keystore 托管密钥)
- iOS: Keychain(AccessibilityFirstUnlock)
- 存储键: genex_access_token / genex_refresh_token / genex_user_json
- API: save() / load() / updateTokens() / getRefreshToken() / clear()

### 升级 ApiClient (lib/core/network/api_client.dart)
- 新增 Dio 错误拦截器(_buildAuthInterceptor)
- 401 自动触发 Token 静默刷新,成功后无感重试原始请求
- Completer 并发锁:多个 401 同时发生只执行一次刷新,其余等待结果
- 跳过重试的端点:/auth/refresh、/auth/login、/auth/register 等
- configureAuthCallbacks():注册 onTokenRefreshed / onSessionExpired 反向回调

### 升级 AuthService (lib/core/services/auth_service.dart)
- _setAuth():登录/注册后 await 写入 SecureStorage
- _clearAuth():登出/Token 过期后 await 清除 SecureStorage
- restoreSession():App 冷启动时从 SecureStorage 恢复 Token + 注册 ApiClient 回调
- refreshToken():主动刷新(正常由拦截器自动触发,无需手动调用)

### 升级 main.dart
- await AuthService.instance.restoreSession() 在 runApp 前执行
- initialRoute 动态判断:isLoggedIn → '/main',否则 '/'
- 全局 NavigatorKey(_navigatorKey)挂载到 MaterialApp
- 监听 authState ValueNotifier:Token 过期后自动导航回 '/'(pushNamedAndRemoveUntil)

## 用户体验
- 登录后关闭 App 再打开:直接进主界面,无需重新登录
- Access Token 过期:ApiClient 自动静默刷新,用户无感知
- Refresh Token 过期:清除本地会话,跳回欢迎页,提示重新登录
- 主动登出:清除 SecureStorage,跳回欢迎页

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-04 02:01:27 -08:00
hailin 1c36c849e2 docs(genex-mobile): 完善邀请分享模块注释与说明
SharePage:
  - 文件头注释:完整功能概述、支持的分享场景、数据来源、URL格式、依赖包说明
  - 类注释:生命周期描述、Widget 树结构图(ASCII)
  - 状态变量:详细说明 _info/_loading/_error/_baseInviteUrl
  - _inviteLink:注释已加载/未加载两种输出示例
  - _loadInfo:成功/失败两条路径说明
  - _showCopied:SnackBar 样式描述
  - _copyCode/_copyLink:示例复制内容
  - _shareNative:iOS/Android 行为说明 + 文案模板示例
  - _buildHeroCard:视觉层次注释 + QR 参数说明
  - _buildStatsCard:布局描述 + 数据来源注释
  - _buildShareActions:三项操作的点击行为说明

ReferralService:
  - 文件头:完整端点一览、推荐码格式、推荐链规则
  - ReferralInfo:字段含义 + 后端响应 JSON 示例
  - getMyInfo:登录要求说明
  - validateCode:用途说明 + 返回值降级策略
  - getDirectReferrals:分页参数范围 + 响应 JSON 示例

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-04 01:42:47 -08:00
hailin 46d2404d19 feat(genex-mobile): 邀请好友 — 分享二维码页面全链路实现
- 新增 SharePage:推荐码 QR 码 + 邀请进度 + 一键复制/原生分享
- ProfilePage 添加「邀请好友」渐变横幅入口
- 新增 ReferralService(getMyInfo / getDirectReferrals)
- pubspec.yaml 引入 qr_flutter ^4.1.0、share_plus ^10.0.2
- 路由 /share 注册
- i18n:4 语言新增 share.* 共 20 个翻译键(zh-CN / zh-TW / en / ja)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-04 01:28:22 -08:00
hailin 0ecf295c35 feat(referral): 推荐服务全链路实现
Backend:
- referral-service: 全新微服务 (NestJS + DDD + Clean Architecture)
  - ReferralProfile 聚合根 (TypeORM entity)
  - ReferralCode / ReferralChain 值对象
  - 仓储接口 + 实现
  - ReferralService 应用服务: 创建档案/验证码/查询直推
  - UserRegisteredHandler: 订阅 genex.user.registered Kafka 事件
  - REST: GET /referral/my, GET /referral/validate, GET /referral/direct
  - 内网: POST /internal/referral/profiles
  - 端口 3013 (外部 4013)
- auth-service: RegisterDto 增加 referralCode 字段
- auth-service: UserRegisteredEvent 携带 referralCode
- auth-service: EventPublisherService 实际接入 Kafka (之前是 stub)
- migrations: 047_create_referral_profiles.sql
- docker-compose: 新增 referral-service 服务块

Frontend (genex-mobile):
- 注册页添加推荐码输入框 (可选),输入时实时验证推荐码有效性
- AuthService.register() 增加 referralCode 参数
- AuthService.validateReferralCode() 新增方法
- i18n: zh_CN/zh_TW/en/ja 各新增 4 个推荐码相关 key

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-04 00:58:28 -08:00
hailin abb358100d fix(genex-mobile): 还原 api.gogenex.com + 增加更新检测日志
.cn 域名因未 ICP 备案被 ISP 拦截,继续使用 api.gogenex.com (HK IP)。
增加 [UpdateService] 和 [VersionChecker] 详细日志便于调试。

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-03 23:17:12 -08:00