6 Commits

Author SHA1 Message Date
luo
010b941d0e fix: 登录兼容联通19位 2026-09-08 14:38:44 +08:00
luo
a25ea5954c Revert "feat: 套餐加油包变子"
This reverts commit 8b6ce505b5.
2026-09-08 10:53:00 +08:00
luo
8b6ce505b5 feat: 套餐加油包变子 2026-09-08 09:56:16 +08:00
luo
484a807dce fix: 修复扫码二维码进入同一个设备 2026-09-03 16:40:11 +08:00
luo
6845e97b14 merge develop into main 2026-08-24 09:27:35 +08:00
luo
189d64c290 feat: 设备: 微信和支付宝/卡: 支付宝
All checks were successful
构建并部署前端到生产环境 / build-and-deploy (push) Successful in 1m43s
2026-07-27 16:32:45 +08:00
5 changed files with 131 additions and 11 deletions

View File

@@ -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.

View File

@@ -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

View File

@@ -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.

View File

@@ -86,7 +86,27 @@
}; };
onMounted(async () => { onMounted(async () => {
const urlIdentifier = getPathDeviceId();
const token = uni.getStorageSync('token'); const token = uni.getStorageSync('token');
const storedIdentifier = uni.getStorageSync('identifier') || '';
// 从外部链接进入时,链接中的资产标识优先于本地登录态。
// 只有相同资产才能复用已登录的会话,避免将上一台设备的数据带到新链接中。
if (urlIdentifier) {
identifier.value = urlIdentifier;
if (token && urlIdentifier === storedIdentifier) {
uni.reLaunch({ url: '/pages/index/index' });
return;
}
if (token || storedIdentifier) {
userStore.clearUser();
}
doLogin();
return;
}
if (token) { if (token) {
uni.reLaunch({ url: '/pages/index/index' }); uni.reLaunch({ url: '/pages/index/index' });
return; return;
@@ -94,7 +114,6 @@
showPostBindReloginNotice(); showPostBindReloginNotice();
handleWechatCallback(); handleWechatCallback();
getPathDeviceId();
// 初始化微信 SDK (仅在微信浏览器内执行) // 初始化微信 SDK (仅在微信浏览器内执行)
// #ifdef H5 // #ifdef H5
@@ -236,10 +255,50 @@
const getPathDeviceId = () => { const getPathDeviceId = () => {
const params = new URLSearchParams(window.location.search); const params = new URLSearchParams(window.location.search);
const idf = params.get('identifier'); return (params.get('identifier') || '').trim();
if (idf) { };
identifier.value = idf;
handleLogin(); 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
};
} }
}; };
@@ -250,13 +309,11 @@
sessionStorage.removeItem('assetToken'); sessionStorage.removeItem('assetToken');
} }
try { try {
const verifyData = await authApi.verifyAsset(identifier.value); const enteredIdentifier = identifier.value;
if (!verifyData?.asset_token) { const { verifyData, verifiedIdentifier } = await verifyAssetWithIccidRetry(enteredIdentifier);
throw { msg: '资产校验未返回有效登录凭证' };
}
userStore.setAssetToken(verifyData.asset_token); userStore.setAssetToken(verifyData.asset_token);
userStore.setIdentifier(identifier.value); userStore.setIdentifier(verifiedIdentifier);
await redirectToWxAuth(verifyData.asset_token); await redirectToWxAuth(verifyData.asset_token);
} catch (e) { } catch (e) {

View File

@@ -43,6 +43,9 @@ export const useUserStore = () => {
const clearUser = () => { const clearUser = () => {
state.token = ''; state.token = '';
state.assetToken = ''; state.assetToken = '';
state.identifier = '';
state.isDevice = false;
state.realNameStatus = 0;
state.userInfo = { avatar: '', nickname: '' }; state.userInfo = { avatar: '', nickname: '' };
uni.removeStorageSync('token'); uni.removeStorageSync('token');
uni.removeStorageSync('identifier'); uni.removeStorageSync('identifier');
@@ -59,4 +62,4 @@ export const useUserStore = () => {
setUserInfo, setUserInfo,
clearUser clearUser
}; };
}; };