diff --git a/openspec/changes/add-iccid-login-check-digit-retry/proposal.md b/openspec/changes/add-iccid-login-check-digit-retry/proposal.md new file mode 100644 index 0000000..46797e6 --- /dev/null +++ b/openspec/changes/add-iccid-login-check-digit-retry/proposal.md @@ -0,0 +1,19 @@ +# Change: Retry 19-digit ICCID login with a Luhn check digit + +## Why + +Some IoT cards are entered or scanned as a 19-digit ICCID prefix. The asset-verification endpoint requires the 20-digit ICCID, so the first verification cannot return an asset token even though the identifier can be deterministically completed. + +## What Changes + +- When a login identifier is exactly 19 numeric characters and starts with `89`, call `/api/c/v1/auth/verify-asset` with the entered value first. +- If that first request fails or does not return `asset_token`, compute the twentieth digit with the ISO/IEC 7812 Luhn (mod-10) check-digit algorithm and retry verification once using the completed ICCID. +- Keep the first failure entirely silent to the customer. Only the retry failure is handled by the existing login error flow. +- Persist and use the successful 20-digit ICCID for the authenticated session without mutating the text in the input field during the silent retry. + +## Impact + +- Affected capability: `iccid-login-check-digit-retry` (new) +- Affected code: `pages/login/login.vue` +- Affected API: `POST /api/c/v1/auth/verify-asset` +- No backend API, request payload shape, or shared error handling is changed. diff --git a/openspec/changes/add-iccid-login-check-digit-retry/specs/iccid-login-check-digit-retry/spec.md b/openspec/changes/add-iccid-login-check-digit-retry/specs/iccid-login-check-digit-retry/spec.md new file mode 100644 index 0000000..6a1c9f2 --- /dev/null +++ b/openspec/changes/add-iccid-login-check-digit-retry/specs/iccid-login-check-digit-retry/spec.md @@ -0,0 +1,31 @@ +## ADDED Requirements + +### Requirement: Silent 19-digit ICCID verification retry + +When an asset login identifier matches `^89\\d{17}$`, the H5 client SHALL first verify the entered 19-digit value. If that attempt fails or does not return a non-empty `asset_token`, it SHALL calculate the ISO/IEC 7812 Luhn mod-10 check digit, append it as the twentieth digit, and verify the completed ICCID exactly once. + +#### Scenario: Completed ICCID succeeds + +- **WHEN** a customer enters a 19-digit numeric identifier beginning with `89` +- **AND** the initial verification does not provide an asset token +- **AND** verification of the Luhn-completed 20-digit ICCID returns an asset token +- **THEN** the customer continues through login without seeing an error from the first verification +- **AND** the authenticated session uses the 20-digit ICCID + +#### Scenario: Initial verification succeeds + +- **WHEN** a customer enters a 19-digit numeric identifier beginning with `89` +- **AND** initial verification returns an asset token +- **THEN** the client SHALL not calculate or submit a second identifier + +#### Scenario: Completed ICCID fails + +- **WHEN** the Luhn-completed retry does not return an asset token +- **THEN** the client SHALL invoke the existing login failure presentation once using the retry failure + +#### Scenario: Identifier is not a 19-digit ICCID prefix + +- **WHEN** an identifier does not match `^89\\d{17}$` +- **AND** verification fails or does not return an asset token +- **THEN** the client SHALL not issue a retry +- **AND** SHALL retain the existing login failure presentation diff --git a/openspec/changes/add-iccid-login-check-digit-retry/tasks.md b/openspec/changes/add-iccid-login-check-digit-retry/tasks.md new file mode 100644 index 0000000..cdfd9fd --- /dev/null +++ b/openspec/changes/add-iccid-login-check-digit-retry/tasks.md @@ -0,0 +1,10 @@ +## 1. Login fallback + +- [x] 1.1 Add a pure ISO/IEC 7812 Luhn mod-10 check-digit helper for a 19-digit ICCID prefix. +- [x] 1.2 Retry asset verification once and silently only when the initial identifier matches `^89\\d{17}$` and the first verification fails or lacks `asset_token`. +- [x] 1.3 Persist the completed ICCID only after the retry succeeds; retain the existing visible error path for all final failures and non-matching identifiers. + +## 2. Verification + +- [x] 2.1 Verify known Luhn vectors plus first-attempt success, fallback success, fallback failure, and non-ICCID failure behavior. +- [x] 2.2 Run the H5 production build. diff --git a/pages/login/login.vue b/pages/login/login.vue index 691db1b..e6d709b 100644 --- a/pages/login/login.vue +++ b/pages/login/login.vue @@ -258,6 +258,50 @@ return (params.get('identifier') || '').trim(); }; + const isNineteenDigitIccidPrefix = (value) => /^89\d{17}$/.test(value); + + const appendIccidLuhnCheckDigit = (prefix) => { + let sum = 0; + + for (let index = prefix.length - 1, offset = 0; index >= 0; index--, offset++) { + let digit = Number(prefix[index]); + if (offset % 2 === 0) { + digit *= 2; + if (digit > 9) digit -= 9; + } + sum += digit; + } + + return `${prefix}${(10 - (sum % 10)) % 10}`; + }; + + const verifyAssetToken = async (assetIdentifier) => { + const verifyData = await authApi.verifyAsset(assetIdentifier); + if (!verifyData?.asset_token) { + throw { msg: '资产校验未返回有效登录凭证' }; + } + return verifyData; + }; + + const verifyAssetWithIccidRetry = async (enteredIdentifier) => { + try { + return { + verifyData: await verifyAssetToken(enteredIdentifier), + verifiedIdentifier: enteredIdentifier + }; + } catch (firstError) { + if (!isNineteenDigitIccidPrefix(enteredIdentifier)) { + throw firstError; + } + + const completedIccid = appendIccidLuhnCheckDigit(enteredIdentifier); + return { + verifyData: await verifyAssetToken(completedIccid), + verifiedIdentifier: completedIccid + }; + } + }; + const doLogin = async () => { loading.value = true; // 丢弃上一轮未完成授权留下的临时凭证,避免校验失败时继续复用。 @@ -265,13 +309,11 @@ sessionStorage.removeItem('assetToken'); } try { - const verifyData = await authApi.verifyAsset(identifier.value); - if (!verifyData?.asset_token) { - throw { msg: '资产校验未返回有效登录凭证' }; - } + const enteredIdentifier = identifier.value; + const { verifyData, verifiedIdentifier } = await verifyAssetWithIccidRetry(enteredIdentifier); userStore.setAssetToken(verifyData.asset_token); - userStore.setIdentifier(identifier.value); + userStore.setIdentifier(verifiedIdentifier); await redirectToWxAuth(verifyData.asset_token); } catch (e) {