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>
- 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>
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>
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>
- 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>
Spring Security's default Http403ForbiddenEntryPoint was returning 403
for all auth failures. Frontend clients treat 403 as a permission error
(not an auth error), so silent loops occurred instead of proper re-login.
Adding a custom AuthenticationEntryPoint that returns 401 makes clients
handle auth failures correctly (show login page on 401).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The MI /devupload/dev/query API returns the currently-published app's status,
not the pending submission. appStatus=3 means the previous version is still
online, not that the new submission was accepted. Use updateVersion=true as
the approval signal; default updateVersion to false to avoid false positives.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- HONOR: use get-app-current-release endpoint (correct), auditResult field (0=review,1=approved,2=rejected)
- HONOR: assertHonorSuccess now accepts both "0" and "0000" success codes
- OPPO: add integer status mapping (111=approved, 444=rejected) from reference impl
- All stores: add full response body logging for diagnosing poll issues
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- StoreSubmissionService: skip UNDER_REVIEW/APPROVED stores in preflight loop to prevent duplicate submissions
- StoreSubmissionService: post-batch sweep marks any still-SUBMITTING stores in same batch as REJECTED
- StoreSubmissionService: @EventListener(ApplicationReadyEvent) clears orphaned SUBMITTING states on startup
- AppVersionRepository: add findAllWithSubmittingStores() native query for startup sweep
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- OpsController/OpsService: POST /api/ops/apps/{appKey}/transfer to move app between tenants
- StoreSubmissionService: read parallelStoreUpload from publish config; conditional parallel vs sequential submission
- AppStoreService: support DINGTALK/WECOM/FEISHU/CUSTOM notify formats in sendWebhook()
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
RestTemplate drops large multipart payloads on the MI API server; switching to
ProcessBuilder curl with Expect:100-continue headers and a 130-minute timeout resolves
upload failures for large APKs.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>