3 Commits

Author SHA1 Message Date
luo
6a1543f9f6 feat: 套餐分层级 2026-09-08 16:12:45 +08:00
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
7 changed files with 107 additions and 98 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

@@ -1,21 +0,0 @@
# Change: Update asset package history hierarchy
## Why
资产套餐历史接口已从平铺使用记录升级为主套餐与关联加油包的关系组。当前 H5 页面把每一条记录当作独立卡片,并以返回条数推断是否还有下一页,无法正确展示层级、默认展开状态和按关系组分页的结果。
## What Changes
- Consume `children` recursively and render a main-package card with its associated addon-package cards.
- Honor `expand_by_default` for the initial expanded state and allow users to expand or collapse a group without losing any child records.
- Preserve and render relationship metadata needed by the contract, including `master_usage_id` for addons and `relationship_status` / `relationship_status_name` for a `master_missing` standalone item.
- Use the response pagination shell (`page`, `size`, `total`) as top-level relationship-group pagination; never infer completion from the number of returned nodes.
- Keep the complete returned group when the API filter matches either its main package or one of its addons. The client will pass filters through unchanged and will not re-filter individual nodes.
- Update the local API documentation to match the hierarchical response fields and pagination shell.
## Impact
- Affected capability: `asset-package-history-display` (new)
- Affected code: `pages/asset-package-history/asset-package-history.vue`, `api/modules/asset.js`, `docs/API.md`
- Affected API: `GET /api/c/v1/asset/package-history`
- No backend filtering, relationship recovery, or error-code mapping is implemented in this H5 repository; the client continues to surface the API's unified error response through the shared request layer.

View File

@@ -1,56 +0,0 @@
## ADDED Requirements
### Requirement: Hierarchical package history display
The H5 client SHALL render each item returned by `GET /api/c/v1/asset/package-history` as a top-level relationship group and SHALL render its recursively supplied `children` as associated addon-package entries without flattening, omitting, or moving them to another page.
#### Scenario: Main package with addon children
- **WHEN** a returned top-level package has one or more `children`
- **THEN** the client displays the main package and all supplied children within the same relationship group
- **AND** each node retains its own package, usage, status, and traffic-display fields
#### Scenario: Relationship exception item
- **WHEN** a top-level item has `relationship_status` equal to `master_missing`
- **THEN** the client displays it as a standalone relationship-exception item
- **AND** displays `relationship_status_name` when supplied
### Requirement: Group expansion behavior
The H5 client SHALL initialize a package group's visibility from `expand_by_default` and SHALL allow the customer to toggle groups that have children. Addon nodes SHALL retain their supplied `master_usage_id` metadata.
#### Scenario: Default-expanded relationship group
- **WHEN** a top-level group is returned with `expand_by_default` equal to true
- **THEN** its children are visible on initial render
#### Scenario: Customer expands a collapsed group
- **WHEN** a top-level group with children is returned with `expand_by_default` equal to false and the customer activates its expand control
- **THEN** every returned child in that group becomes visible
- **AND** no additional API request is made to retrieve those children
### Requirement: Relationship-group pagination
The H5 client SHALL treat `page`, `size`, and `total` from the package-history response as pagination metadata for top-level relationship groups. It SHALL not use a child count or the number of returned nodes to decide whether more pages are available.
#### Scenario: Last page has a full number of groups
- **WHEN** the response contains `size` top-level groups and `page * size` is greater than or equal to `total`
- **THEN** the client indicates that no more relationship groups are available
#### Scenario: Filtered group response
- **WHEN** the API returns a complete relationship group because either its main node or an addon node jointly matches the supplied `status` and `package_type` filter
- **THEN** the client displays the complete returned group without applying node-level filtering
### Requirement: Package history failure handling
The H5 client SHALL rely on the shared request layer for non-success package-history responses, preserving the API-provided unified error message and authentication handling.
#### Scenario: Related master cannot be read
- **WHEN** the API returns a non-success response because a related master record exists but cannot be displayed
- **THEN** the client does not synthesize a relationship item
- **AND** the shared request layer displays the unified API error response

View File

@@ -1,16 +0,0 @@
## 1. API contract and data handling
- [x] 1.1 Document the hierarchical node fields, relationship exceptions, and top-level pagination shell for package history.
- [x] 1.2 Add a client-side response normalizer that preserves recursively returned `children` and all relationship metadata.
- [x] 1.3 Use `total` and returned top-level groups to drive pagination, so children never consume page capacity or cross pages.
## 2. Package history UI
- [x] 2.1 Render returned top-level relationship groups and their addon children with the existing package metrics and status styling.
- [x] 2.2 Initialize groups from `expand_by_default` and add an accessible expand/collapse control for groups with children.
- [x] 2.3 Render `master_missing` as a standalone relationship-exception item and preserve normal shared-request error handling for all other API failures.
## 3. Verification
- [x] 3.1 Verify nested, collapsed, expanded, filtered-group, `master_missing`, and multi-page response fixtures.
- [x] 3.2 Run the H5 production build and inspect the changed files for contract consistency.

View File

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