- Add AppStoreService.clearStoreReview() to remove a store's review entry
from storeReviewStatus JSON (used for false-positive cleanup)
- refreshStoreReviewStatus: when existing state is APPROVED but polled
onlineVersionCode < submittedCode, clear the stale state instead of
leaving the misleading nonCurrentRelease flag in place
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Require @AuthenticationPrincipal tenantId in parseLicenseFile endpoint
and verify the decrypted appKey belongs to the current tenant before
returning license contents. Returns 403 "权限不足无法展示" for
cross-tenant license files.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Add compareVersionCodes() helper to compare numeric versionCodes safely
- preflightStoreSubmission: block only when onlineVersionCode >= submittedCode;
allow submit when online < submitted (normal new release)
- refreshStoreReviewStatus: only write preExisting=true when online >= submitted
- pollStoreReviewStatus: same guard for UNDER_REVIEW/REJECTED → ONLINE transitions
- All per-store query methods (Huawei, Xiaomi, OPPO, VIVO, Honor): only set
nonCurrentRelease=true when onlineVersionCode > submittedCode
- Fix pre-existing compilation errors: replace findByAppKeyAndStoreType with
findTopByAppKeyAndStoreTypeOrderByUpdatedAtDesc
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Add composite unique constraint (app_key, device_id) on DeviceEntity
- Remove global unique constraint from device_id column
- Update DeviceRepository: findByAppKeyAndDeviceId returns Optional,
findByDeviceId returns List for multi-app lookups
- Update DeviceService.register/verify to scope lookups by appKey
so same physical device can register independently for each app
- Update LicenseInternalController.getDevice to return list
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- 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>
- 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>
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>
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>
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>
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>
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>
- 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>
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>
/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>
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>
- 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>
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>
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>
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>
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>