提交图

152 次代码提交

作者 SHA1 备注 提交日期
XuqmGroup
843ed69f3c license: fix device re-register appKey update, add license file parser
- DeviceService.register(): update appKey when device switches to a different app
  and adjust registered device counters for old/new appKey
- LicenseAdminController: fix updateAppLicense parameter count mismatch
- AppController: add POST /api/apps/license/parse endpoint for license file decryption
- SecurityCenterView: add License file parser UI with upload and paste support
- appApi: add parseLicenseFile() method

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 18:37:46 +08:00
XuqmGroup
ccb976c605 tenant: auto-generate license file on app creation, decouple from license service
- AppEntity: add licenseFileContent field to store pre-generated encrypted license
- AppService: generate license file content on create/update with normalized baseUrl
- AppController: read license file content from entity instead of generating on-the-fly
- Web: remove license download v-if serviceEnabled check, always show download button

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 17:56:12 +08:00
XuqmGroup
8c9bfb6acd feat: license 文件作为通用凭证支持所有服务 SDK 初始化
- LicenseFileCrypto 移至 common 模块并新增 decrypt() 方法
- LicenseFileCrypto.LicensePayload 携带 appKey / packageName / iosBundleId / harmonyBundleName,matchesPackageName() 支持三端包名任一匹配
- tenant-service downloadLicenseFile:去掉"License 服务已开通"限制,app 创建即可下载;payload 新增 iosBundleId / harmonyBundleName
- im / push / update / license 四个服务 SDK 初始化端点均支持双模式:
  · licenseFile 模式:解密文件取 appKey,比对 packageName(无需调 tenant-service)
  · appKey 模式:调 tenant-service 取 platformInfo 比对 packageName(原有逻辑)
- appKey 参数由必填改为可选(与 licenseFile 二选一)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 16:47:30 +08:00
XuqmGroup
0a267c5f70 feat: 校验 SDK 初始化时 packageName 与平台配置的 appKey 是否匹配
- im/push/update 三个服务登录/检查更新接口新增必填参数 packageName
- 调用对应服务的 tenant-service 内部接口获取 platformInfo,与传入包名比对,不匹配返回 403
- update 服务按 platform 字段精确匹配(ANDROID/IOS/HARMONY 各用对应字段)
- im/push 服务对三端包名任一匹配即通过
- ImAppSecretClient / PushAppSecretClient 新增 getPlatformInfo 缓存方法
- 新增 UpdateTenantClient 用于 update-service 调用 tenant-service platformInfo 接口

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 16:41:17 +08:00
XuqmGroup
4c0db6e9b7 feat: validate packageName against appKey on SDK and license init
SdkConfigController: require packageName param; reject with 403 if it doesn't
match the platform-specific name registered for the app (skipped when app has
no name configured yet).

LicensePublicController: add required packageName to register/verify requests.
DeviceService: validatePackageName() checks against android/ios/harmony names
stored on AppLicenseEntity; rejects if any are configured and none match.
AppLicenseEntity: add android_package_name, ios_bundle_id, harmony_bundle_name
columns (auto-migrated via ddl-auto=update).
LicenseInternalController/AppLicenseService: accept and persist package names
via upsert endpoint.
LicenseServiceClient/FeatureServiceManager: pass app package names when syncing
license records to license-service.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 16:31:50 +08:00
XuqmGroup
138360b760 fix(update): rewrite file-service URL to internal address for private deployments
UpdateAssetService: add FILE_BASE_URL / FILE_SERVICE_INTERNAL_URL config; any URL
starting with FILE_BASE_URL is rewritten to the internal file-service address instead
of going through the external domain, fixing APK inspect timeout on private deployments.

SystemUpdateService: add patchDockerComposeUpdateService() to inject FILE_BASE_URL and
FILE_SERVICE_INTERNAL_URL into existing customers' docker-compose.yml on update.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 16:03:09 +08:00
XuqmGroup
32aa3c0eef feat(tenant): split update/reset ops, remove bootstrap app auto-creation
- SystemUpdateService: split runUpdate() (pull+recreate) and runReset() (recreate only)
- SystemUpdateController: add POST /api/system/reset endpoint
- SdkAppProvisioningService: remove ensureBootstrapApp/ensureApp/ensureFeatureDefaults; resolveApp now throws 404 instead of auto-creating
- SdkAppInitializer: remove ensureBootstrapApp call; only runs one-time migration marking existing system apps as isDefault=true
- PrivateTenantBootstrapInitializer: remove bootstrap app creation; only ensures admin tenant account exists

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 15:33:20 +08:00
XuqmGroup
9728dbb002 fix: suppress duplicate-result errors and hide system apps from private deployment
update-service:
- AppPublishConfigRepository/AppStoreConfigRepository: change Optional-returning
  findBy methods to findTopBy...OrderByUpdatedAtDesc to tolerate duplicate rows in
  public DB and avoid IncorrectResultSizeDataAccessException
- Revert GlobalExceptionHandler to safe "服务器内部错误" (debug details removed)

tenant-service:
- SdkAppInitializer: skip Demo Chat creation on DEPLOYMENT_MODE=PRIVATE;
  migrate existing system apps (ak_demo_chat, IM platform app) to is_default=true
- SdkAppProvisioningService.ensureApp: mark all platform-provisioned apps as
  is_default=true, deletable=false so they don't appear in user's app list
- PrivateTenantBootstrapInitializer: migrate existing private bootstrap apps to
  is_default=true on upgrade
- AppService.listByTenant: filter out is_default=true system apps from the list

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 14:24:33 +08:00
XuqmGroup
855b17ef0e fix(update): 修复版本列表排序并改进Android包上传功能
- 将版本查询排序从versionCode改为createdAt以正确显示最新版本
- 为Android包上传表单添加已上传文件显示区域
- 实现删除已上传文件的功能以便重新上传
- 添加上传组件引用以支持文件清除操作
- 增加Document和Delete图标导入用于文件管理界面
- 添加已上传文件信息的样式和布局支持
2026-05-21 18:20:06 +08:00
XuqmGroup
24e11794bc refactor(update): 移除版本重复检查并修改发布状态逻辑
- 移除了应用包名和版本号的重复上传检查逻辑
- 修改了发布立即生效时的状态变更机制,改为将其他已发布版本标记为废弃状态
- 新增了按应用键、平台和发布状态查询的方法
- 简化了版本上传时的验证流程,移除了APK文件MD5比较相关代码
2026-05-21 18:08:21 +08:00
XuqmGroup
e5d9e0da0c debug(update-service): 在错误响应中暴露异常信息(临时)
用于排查上传接口 500 错误的根本原因,确认后将恢复。

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-21 17:47:51 +08:00
XuqmGroup
d49d0297cf fix(update-service): 非灰度版本对匿名用户可见
原逻辑在 allowAnonymousCheck=false 且 userId=null 时直接返回
needsUpdate=false,导致无登录流程的应用(如 clinical-android)
永远收不到更新提示。

修正为:只有灰度版本才需要 userId;非灰度已发布版本对所有调用
方可见,allowAnonymousCheck=false 仅在非灰度场景下补充拦截。

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-21 17:22:59 +08:00
XuqmGroup
c9c50038bf fix(tenant-service): 自动修复 nginx 更新接口 60s 超时
patchNginxUpdateTimeout 为 /api/system/update 注入精确匹配 location,
proxy_read_timeout 设为 600s,避免 docker pull 静默期断连。

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-21 17:19:55 +08:00
XuqmGroup
4a38147cb9 feat(tenant-service): 一键更新自动修复配置文件
- 更新前执行幂等配置修复:nginx location /file/ → /api/file/,
  docker-compose.yml 补齐 FILE_UPLOAD_DIR 和 FILE_BASE_URL
- nginx 移至 OTHER_SERVICES 末尾,最后重启以应用修复后的配置
- docker login 读取 .env 中的仓库凭据,解决私有镜像拉取 403

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-21 17:08:01 +08:00
XuqmGroup
7a530eb35b fix(license): 新设备注册时初始化 lastVerifiedAt 字段
新设备首次注册成功后,lastVerifiedAt 原本为 null,
导致控制台"最后验证时间"始终为空直到缓存过期后的首次 verify 调用。
注册即视为首次验证,同步写入 lastVerifiedAt。

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-21 16:39:30 +08:00
XuqmGroup
b0e7f198db feat(license): 支持修改 License 过期时间 + 修复一键更新三个问题
License 过期时间:
- LicenseAdminController PATCH 接口增加 expiresAt 字段
- AppLicenseService.update() 移除"一旦设置不可修改"限制,支持清空(永久)或更新日期

一键更新 (SystemUpdateService) 修复:
1. 改用 docker compose (v2) 替换 docker-compose (v1)
2. isRunning/getCurrentImage 去掉 project=xuqm 标签过滤
   (deploy.sh 不传 -p 参数,实际 project 标签为目录名)
3. 拉取前读取 deployRoot/.env 中的 REGISTRY 凭据并执行 docker login

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-21 16:26:01 +08:00
XuqmGroup
aece1fd08d fix(system-update): 用 compose label 查询容器,修复 isRunning 和自更新助手镜像
- isRunning() 改用 docker ps --filter label=com.docker.compose.service
  兼容 Compose v1 (xuqm_svc_1) 和 v2 (xuqm-svc-1) 命名格式
- 自更新助手镜像改用 getCurrentImage() 从运行中容器的 label 获取,
  不再依赖容器环境变量 REGISTRY/IMAGE_TAG(容器内未注入这两个变量)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-21 15:46:39 +08:00
XuqmGroup
cc132c7ce7 feat(license): license 文件新增 serverUrl 字段,私有化部署自动写入
私有化模式下生成的 license 文件包含 serverUrl,SDK 通过
XuqmSDK.autoInitialize() 读取后可自动配置所有服务端点,
无需在 App 层硬编码 appKey 或 serverUrl。

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-21 15:25:13 +08:00
XuqmGroup
a98dbca26d fix(system-update): 用独立助手容器替代 CompletableFuture 实现 tenant-service 自重建
原方案:CompletableFuture 延迟调用 docker-compose up。
问题:docker-compose 发出 stop 指令后,容器内全部进程(含 CompletableFuture 线程)
     被立即杀死,rm/create/start 步骤永远不会执行,tenant-service 停在停止状态。

新方案:先用 docker run -d 启动独立助手容器(xuqm-self-updater),
     它不依附于 tenant-service,不会随之终止;8 秒后执行 force-recreate。

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-21 14:52:36 +08:00
XuqmGroup
f2e126e2d0 feat(tenant-service): 一键更新接口 + Dockerfile 添加 docker-compose
- 新增 SystemUpdateController POST /api/system/update(PRIVATE 模式)
- SystemUpdateService 通过 docker-compose 拉镜像并逐服务重建容器
- Dockerfile 添加 docker-cli + docker-compose(用于容器内调用 Docker API)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-21 14:46:40 +08:00
XuqmGroup
8a3c41d5ff feat(license): 租户自主管理最大设备数,ops 彻底移除 license 管理
license-service:
- LicenseAdminController: 新增 PATCH /api/license/admin/apps/{appKey},
  租户可直接修改 maxDevices / isActive / remark

tenant-service:
- OpsController: 移除 GET /api/ops/apps/{appKey}/license 和
  PUT /api/ops/apps/{appKey}/license/max-devices 两个端点,
  同时移除 licenseServiceClient 字段注入
- LicenseServiceClient: 移除 updateMaxDevices() 和 getAppLicenseStatus()

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-21 12:45:33 +08:00
XuqmGroup
af922ae420 fix(ci): 串行化生产部署防止并发 docker pull 竞争
多服务同时构建时 Deploy 阶段并发向同一台生产机 docker pull,
containerd content store 写入共享 layer 产生文件竞争导致 rename 失败。
加 lock('prod-deploy') 确保所有服务按序部署,并加 retry(3) 容忍偶发抖动。

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-21 10:58:08 +08:00
XuqmGroup
25e9bef745 chore(version): 更新版本号到 2026.05.20-private.3
- 添加新版本号文件 VERSION
- 版本号设置为 2026.05.20-private.3
2026-05-21 10:44:59 +08:00
XuqmGroup
02ad5aad06 fix(private): 私有化部署 CORS 放开所有 Origin
私有化部署时客户使用自定义域名,原硬编码的 *.xuqinmin.com 白名单
导致 WebSocket 握手和跨域请求被 Spring Security CORS 过滤器拒绝(426/403)。
检测 DEPLOYMENT_MODE=PRIVATE 环境变量,私有化模式下允许所有 Origin。
影响范围:im-service / file-service / license-service / update-service。

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-21 10:44:33 +08:00
XuqmGroup
897326ff0f feat(private): 新增内部维护接口自动处理积压 PENDING 申请
- SecurityConfig: 放开 /api/private/admin/** 无需 JWT
- FeatureServiceManager.autoApproveAllPending(): 批量审批所有 PENDING 记录
- OpsController: POST /api/private/admin/approve-pending-requests
  仅私有化模式可用,upgrade.sh 重启后自动调用,无需手动操作

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-20 18:45:07 +08:00
XuqmGroup
6ca0dcbe74 fix(private): 私有化模式下存量 PENDING 服务申请自动开通
之前的自动开通逻辑在重复申请检查之后,导致已有 PENDING 记录时
直接抛 400 而不进入自动开通流程。
现在私有化模式下检测到 PENDING 记录时直接 approveRequest,
不再返回"请等待运营人员处理"错误。

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-20 18:32:32 +08:00
XuqmGroup
f9957143da feat(private): 私有化部署增强 — 服务自动开通、屏蔽 Ops 功能
- FeatureServiceManager: 私有化模式下服务开通申请跳过审核,直接自动激活
- OpsController: 私有化模式下 /api/auth/ops/login 返回 404,屏蔽运营登录
- OpsAdminInitializer: 私有化模式下跳过默认运营管理员账号的初始化

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-20 18:24:43 +08:00
XuqmGroup
4432c7dc28 fix(oppo): remap audit_status=5 to UNDER_REVIEW; restore REJECTED→UNDER_REVIEW in poll
OPPO returns audit_status=5 for versions currently under review (审核中);
previously this was mapped to REJECTED causing a state mismatch. Only 444
is now treated as the rejected aggregate code.

Also extend the scheduled poll to restore REJECTED→UNDER_REVIEW for any
store (not just Xiaomi) when the vendor API reports the version is under
review, covering cases where the submission succeeded after our platform
recorded an exception.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-20 10:59:52 +08:00
XuqmGroup
4d2faa33de fix(oppo): strip empty params from submit body to fix sign mismatch (errno=800004)
OPPO server includes all received body params in server-side signature computation;
sending empty-string optional params while excluding them from the client-side sign
produces a mismatch. Remove empty entries from params map before building both the
api_sign and the POST body so both sides operate on identical param sets.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-20 10:16:41 +08:00
XuqmGroup
501d7e09ab fix(update): fix OPPO token expiry, sign empty params, and MI already-live detection
- OPPO: re-obtain access token after APK upload (prevents expiry during long uploads)
- OPPO: exclude empty-string params from api_sign computation per OPPO spec
- OPPO: include errno in error message for easier debugging
- MI: detect already-live version via packageInfo.versionCode before upload;
  throw VersionAlreadyLiveException → executor marks state APPROVED instead of REJECTED

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 19:11:13 +08:00
XuqmGroup
450a44de68 fix: isolate private deployment databases and fix store review refresh bugs
application.yml — all services:
- Replace hardcoded jdbc:mysql://39.107.53.187 with ${SPRING_DATASOURCE_URL:fallback}
- Same for SPRING_DATASOURCE_USERNAME/PASSWORD
- im-service: replace hardcoded redisdev.xuqinmin.com with ${SPRING_DATA_REDIS_*}
  This ensures docker-compose environment overrides take effect; without these
  placeholders, Spring Boot's relaxed binding couldn't override the YAML values
  and the private deployment connected to production databases.

StoreSubmissionService.refreshStoreReviewStatus — two bugs fixed:
1. MI/UNDER_REVIEW_XIAOMI branch now guards against downgrading APPROVED state.
   Xiaomi's poll API returns UNDER_REVIEW_XIAOMI when the submitted version is
   not yet the live version, even after the store approves it. Previously this
   caused the manual refresh to overwrite a webhook-confirmed APPROVED with
   UNDER_REVIEW on every click.
2. When the poll returns APPROVED but currentSubmissionLive=false (another version
   is live on the store), no longer overwrite an existing APPROVED (from webhook)
   with nonCurrentRelease=true. The webhook is authoritative; the live version
   difference just means distribution is pending, not that this is a non-current
   release. Only adds nonCurrentRelease when transitioning FROM a non-APPROVED
   state (true pre-existing detection).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 18:25:50 +08:00
XuqmGroup
9771663f00 fix(tenant): correct import endpoint path in SecurityConfig
/api/private/migrate/import → /api/private/deployment/migrate/import
to match PrivateDeploymentController's @RequestMapping("/api/private/deployment")

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 16:07:39 +08:00
XuqmGroup
d007072ea9 fix(update): send webhook on store live detection (Xiaomi 已上架)
updateStoreReviewLive was calling IM notifier but not sendWebhook,
so polling-detected APPROVED events (e.g. Xiaomi going live) never
triggered the tenant's configured webhook URL.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 15:57:51 +08:00
XuqmGroup
f97201e3e3 feat(tenant): API-based tenant migration for private deployment
- Add MigrateController: request-code / generate-key / export endpoints
  with one-time pmk_ key (SHA-256 hashed, 24h expiry)
- Add PrivateDeploymentController import endpoint for private mode only
- Add MigrateKeyEntity / MigrateKeyRepository for key lifecycle
- Add MigrateExportData DTO (tenant + apps + feature services)
- Add AppEntity.isDefault / deletable fields
- Add AppRepository.deleteAllExcept / FeatureServiceRepository.deleteAllExcept
- Permit /api/migrate/export and /api/private/migrate/import in SecurityConfig

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 15:11:49 +08:00
XuqmGroup
e5f0e7faea feat: private deployment server-side capabilities (P2)
- PrivateDeploymentProperties: DEPLOYMENT_MODE/ENABLE_*/TENANT_BOOTSTRAP_ENABLED config binding
- PrivateTenantBootstrapInitializer: auto-create main tenant and app from env vars when PRIVATE mode, idempotent
- AuthService: block registration with XUQM_PRIVATE_2001 when TENANT_REGISTER_ENABLED=false
- EmailService: block REGISTER email verification in private mode
- SdkConfigController: intersect DB feature flags with ENABLE_* deployment flags for runtime degradation
- PrivateDeploymentController: GET /api/private/deployment/status public endpoint
- SecurityConfig: permit /api/private/deployment/status without auth
- application.yml: add deployment.* and tenant.bootstrap.* config sections with env var bindings

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-18 20:49:46 +08:00
XuqmGroup
4d54d2a4a4 docs(private): 更新私有化SDK设计文档添加配置管理和错误码规范
- Flutter包名格式从xuqm_private_flutter_*改为xuqm_private_*
- 添加configVersion字段用于追踪配置变更,格式为YYYY.MM.DD-序号
- 添加配置版本管理说明,包含schemaVersion兼容性策略
- 添加配置热更新机制,支持冷加载和热加载两种方式
- 添加多环境配置支持,可通过activeProfile指定生效环境
- 添加统一错误码规范,覆盖配置缺失、格式错误、版本不兼容等场景
- 更新验收标准,包含热更新、多环境切换、错误码一致性要求
- 添加私有化开发计划中的错误码规范、文档同步、数据迁移等任务
- 补充MySQL运维细节、证书自动续期、监控告警、日志收集等部署要求
- 添加性能基准指标,包含HTTP API、WebSocket、文件传输等性能要求
- 修复应用商店审核状态轮询中的逻辑错误,添加小米商店特殊处理
- 更新前端界面显示审核版本信息,优化状态刷新逻辑
2026-05-18 19:17:44 +08:00
XuqmGroup
93fdb31cdc docs(private): 完善私有化部署开发计划和设计规范
- 增加实时进度和交接规则,定义任务状态枚举和更新格式
- 创建任务进度台账,涵盖P0-P5阶段全部开发任务
- 补充部署仓库交付边界确认和进度审计规范
- 完善MySQL/Redis双模式支持,增加external/managed选项
- 增加离线部署、安全治理、可观测性等完整交付能力
- 更新仓库结构设计,增加secrets.env、observability、data目录
- 补充健康检查、诊断脚本、升级回滚、备份恢复详细要求
- 优化应用商店审核状态查询逻辑,增加手动刷新接口
- 修复小米和VIVO商店状态查询中的版本匹配逻辑错误
- 增加缓存键版本隔离,防止不同版本状态混淆
- 优化厂商API连通性检查和审核状态轮询机制
2026-05-18 19:00:38 +08:00
XuqmGroup
87edb316a5 feat(private-deploy): 支持 MySQL/Redis 外部连接和托管模式部署
- 添加 external 和 managed 两种数据库/缓存模式支持
- 实现 MySQL/Redis 托管安装脚本和配置向导
- 支持客户自备连接或部署脚本新建基础设施
- 更新部署文档说明不同模式的配置和验证要求
- 添加应用版本防重复上传和删除功能
- 实现应用商店预提交检查和发布计划功能
2026-05-18 18:37:10 +08:00
XuqmGroup
e309a41ed0 docs(deploy): 移除 Jenkins 配置和 Android Demo 计划文档
- 删除 jenkins-setup.md 完整的 Jenkins 服务配置指南
- 更新 README.md 部署文档标题为公有化部署文档
- 添加私有化部署说明章节和相关设计文档链接
- 从 REST API 设计文档中移除 demo-service 相关描述
- 更新推送架构图中业务服务端描述为客户端服务器
- 删除 android-demo-plan.md Android Demo 开发计划文档
- 删除 multi-platform-im-roadmap.md 多平台 IM 路线图文档
- 删除 java-im-server-sdk-plan.md Java IM 服务端 SDK 计划文档
2026-05-18 17:57:05 +08:00
XuqmGroup
b3b33dbb7b fix: remove unreliable post-failure live-check; restrict REJECTED poll to HUAWEI
Post-failure live check called pollStoreSingleReviewState on submission
failure, but for MI/VIVO/OPPO/HONOR the poll returns APPROVED for ANY
live version (not version-specific). This caused false positives: if an
older version was live on Xiaomi, the poll would return APPROVED and
incorrectly mark the new submission as 已上线(直接上传).

Changes:
- Remove post-failure live check from executeSinglePlan and sequential
  failure handler — submission failures are just marked REJECTED
- Restrict the REJECTED-store poll to HUAWEI only, since it is the only
  store where pollStoreSingleReviewState is version-specific
  (compares onShelfVersionCode against v.getVersionCode())
- Log full VIVO data response to capture versionCode field name for
  future version-specific detection

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-18 17:52:53 +08:00
XuqmGroup
8d46d21726 fix: store resubmission, Xiaomi curl, and live-on-store detection
- Dockerfile: add curl to Alpine runtime image (required by Xiaomi/Vivo
  APK upload which uses ProcessBuilder curl for HTTP/1.1 multipart)
- AppStoreService: mark previously-APPROVED stores as WITHDRAWN on
  resubmission so executeSubmitAsync calls the store cancel API first;
  guard updateStoreReview from stale APPROVED webhooks on PENDING/WITHDRAWN
  stores; add updateStoreReviewLive() to record pre-existing live versions
  with liveOnStore/preExisting metadata
- StoreSubmissionService: call cancelAtStore for WITHDRAWN stores before
  resubmitting; expand poll query to include REJECTED stores and call
  updateStoreReviewLive when a REJECTED store is found live; after any
  submission failure check if the store already has the version live
  (catches Huawei/Vivo pre-existing direct uploads at submission time)
- AppVersionRepository: add findAllWithUnderReviewOrRejectedStores query

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-18 17:30:26 +08:00
XuqmGroup
8de0338b93 fix: withdraw previously-approved store reviews before re-submitting same version
When re-submitting a version to stores where the previous review was APPROVED:

1. markSubmitted now sets those stores to WITHDRAWN (not PENDING), preserving
   the old batchId/submittedAt for traceability. This signals to executeSubmitAsync
   that the store cancel API must be called before a new submission is attempted.

2. executeSubmitAsync detects the WITHDRAWN state and calls cancelAtStore first,
   then falls through to the normal submission path. This revokes the old approval
   on the store's side so no stale webhook or poll cycle can fire APPROVED for the
   old review after re-submission.

3. updateStoreReview now rejects APPROVED transitions from PENDING or WITHDRAWN
   states (stale webhook guard). A valid approval can only arrive after the store
   has seen the new submission (i.e. current state must be SUBMITTING or UNDER_REVIEW).
   This prevents autoPublishAfterReview from triggering before the new review cycle.

Operation log includes `approvedWithdrawn` list when any store was withdrawn on re-submit.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-18 17:09:31 +08:00
XuqmGroup
ab7f029960 feat(update-service): add PATCH /app/{id}/changelog with audit log
Allows editors to update release notes at any time. Every change is
recorded in update_operation_log with action CHANGELOG_UPDATE and
before/after values in detailJson, satisfying the audit requirement.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-18 16:47:36 +08:00
XuqmGroup
1ec7f2e35d fix(file-service): stream upload to disk to fix OOM on large files
file.getBytes() loaded the entire APK into JVM heap, causing
OutOfMemoryError on files >~50MB. Now streams to a temp file while
computing SHA-256 via DigestInputStream, then atomically moves to the
final path. Zero heap cost regardless of file size.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-18 16:31:15 +08:00
XuqmGroup
9c51e666f8 fix(file-service): permit /error endpoint to avoid 401 on server errors
Spring Boot's /error handler was secured, causing any server-side exception
during upload/serve to redirect clients to login instead of returning an
error message. Permitting /error ensures errors are returned as proper
JSON responses rather than auth challenges.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-18 16:10:49 +08:00
XuqmGroup
8bc9b1ebda fix(file-service): use AntPathRequestMatcher to bypass Spring MVC matching
Spring Security 6 with MVC on classpath resolves requestMatchers(HttpMethod, String)
to MvcRequestMatcher, which fails to match the actual servlet paths for this service.
Switching to explicit AntPathRequestMatcher instances bypasses MVC introspection and
forces pure Ant pattern evaluation, fixing persistent 401 on public upload/serve endpoints.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-18 15:40:11 +08:00
XuqmGroup
b49b67bb1e fix(file-service): use explicit HttpMethod on all requestMatchers to force AntRequestMatcher
Spring Security 6 MvcRequestMatcher (used when no HttpMethod is specified
and Spring MVC is on the classpath) fails to match the upload endpoint,
falling through to anyRequest().authenticated() and returning 401.
Specifying HttpMethod forces AntRequestMatcher which matches reliably.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-18 15:11:13 +08:00
XuqmGroup
61b79465cd fix(file-service): restore public upload by explicitly allowing POST /api/file/upload
The previous commit (GET-only permitAll) inadvertently broke upload by
requiring auth. The original design intentionally allows unauthenticated
upload — explicitly permit POST /api/file/upload to make this clear.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-18 14:49:37 +08:00
XuqmGroup
623656648e fix(file-service): restrict file-serving permitAll to GET requests only
Upload endpoint (POST) was inadvertently matched by the method-less
requestMatchers("/api/file/*") rule. Making it GET-only makes the intent
explicit and ensures upload correctly requires a valid JWT.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-18 14:11:56 +08:00
XuqmGroup
4e54737e72 feat: webhook shows app name; auto-withdraw superseded approved stores
- Webhook notification body shows app display name (resolved from
  tenant-service via internal API with in-memory cache) instead of appKey
- When re-uploading a package with the same versionCode, automatically
  withdraw APPROVED store entries from the older entity before submitting
  the new entity, preventing duplicate active submissions
- tenant-service /internal/sdk/apps/{appKey}/platform-info now includes
  the app 'name' field

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-18 13:31:31 +08:00