From 8bd9de15cd533e2f97c81c53352ce46ebae4a817 Mon Sep 17 00:00:00 2001 From: luo Date: Wed, 16 Sep 2026 16:32:08 +0800 Subject: [PATCH] feat: new --- api/index.js | 1 + api/modules/notification.js | 3 +- api/modules/popup.js | 21 + components/PopupCandidate.vue | 296 +++ docs/产品迭代8月份/11.md | 1779 +++++++++++++++++ .../add-h5-popup-candidate-flow/proposal.md | 28 + .../specs/h5-popup-candidate/spec.md | 52 + .../specs/h5-risk-exchange-address/spec.md | 28 + .../specs/personal-notification-list/spec.md | 36 + .../add-h5-popup-candidate-flow/tasks.md | 36 + .../proposal.md | 37 + .../specs/index-phone-bind-gate/spec.md | 23 + .../phone-bind-association-prefill/spec.md | 36 + .../specs/phone-change-migration/spec.md | 29 + .../tasks.md | 29 + pages/bind/bind.vue | 21 +- pages/index/index.vue | 39 +- pages/my-wallet/my-wallet.vue | 11 + pages/notifications/notifications.vue | 14 +- pages/package-order/package-order.vue | 11 + qt2.md | 2 + utils/popup.js | 3 + 22 files changed, 2502 insertions(+), 33 deletions(-) create mode 100644 api/modules/popup.js create mode 100644 components/PopupCandidate.vue create mode 100644 docs/产品迭代8月份/11.md create mode 100644 openspec/changes/add-h5-popup-candidate-flow/proposal.md create mode 100644 openspec/changes/add-h5-popup-candidate-flow/specs/h5-popup-candidate/spec.md create mode 100644 openspec/changes/add-h5-popup-candidate-flow/specs/h5-risk-exchange-address/spec.md create mode 100644 openspec/changes/add-h5-popup-candidate-flow/specs/personal-notification-list/spec.md create mode 100644 openspec/changes/add-h5-popup-candidate-flow/tasks.md create mode 100644 openspec/changes/update-phone-bind-association-flow/proposal.md create mode 100644 openspec/changes/update-phone-bind-association-flow/specs/index-phone-bind-gate/spec.md create mode 100644 openspec/changes/update-phone-bind-association-flow/specs/phone-bind-association-prefill/spec.md create mode 100644 openspec/changes/update-phone-bind-association-flow/specs/phone-change-migration/spec.md create mode 100644 openspec/changes/update-phone-bind-association-flow/tasks.md create mode 100644 qt2.md create mode 100644 utils/popup.js diff --git a/api/index.js b/api/index.js index b99ee97..3830824 100644 --- a/api/index.js +++ b/api/index.js @@ -4,6 +4,7 @@ export { deviceApi } from './modules/device.js'; export { exchangeApi } from './modules/exchange.js'; export { orderApi } from './modules/order.js'; export { notificationApi } from './modules/notification.js'; +export { popupApi } from './modules/popup.js'; export { realnameApi } from './modules/realname.js'; export { walletApi } from './modules/wallet.js'; export { wechatApi } from './modules/wechat.js'; diff --git a/api/modules/notification.js b/api/modules/notification.js index 7dd44f9..a9d4d16 100644 --- a/api/modules/notification.js +++ b/api/modules/notification.js @@ -23,7 +23,8 @@ export const notificationApi = { markRead(id) { return request({ url: `/api/c/v1/notifications/${id}/read`, - method: 'PUT' + method: 'PUT', + data: { id } }); }, diff --git a/api/modules/popup.js b/api/modules/popup.js new file mode 100644 index 0000000..faf025a --- /dev/null +++ b/api/modules/popup.js @@ -0,0 +1,21 @@ +import request from '@/utils/request.js'; + +export const popupApi = { + getCandidate(page, identifier) { + return request({ + url: '/api/c/v1/popup-candidates', + method: 'GET', + data: { page, identifier }, + // 资源不可见 / 无候选时静默处理,不弹全局错误提示 + showError: false + }); + }, + + submitRiskExchangeAddress(assetId, params) { + return request({ + url: `/api/c/v1/risk-exchanges/${assetId}/address`, + method: 'POST', + data: { asset_id: assetId, ...params } + }); + } +}; diff --git a/components/PopupCandidate.vue b/components/PopupCandidate.vue new file mode 100644 index 0000000..72912fd --- /dev/null +++ b/components/PopupCandidate.vue @@ -0,0 +1,296 @@ + + + + + diff --git a/docs/产品迭代8月份/11.md b/docs/产品迭代8月份/11.md new file mode 100644 index 0000000..340d1aa --- /dev/null +++ b/docs/产品迭代8月份/11.md @@ -0,0 +1,1779 @@ +# 资产验证 + +## OpenAPI Specification + +```yaml +openapi: 3.0.1 +info: + title: '' + description: '' + version: 1.0.0 +paths: + /api/c/v1/auth/verify-asset: + post: + summary: 资产验证 + deprecated: false + description: '' + tags: + - 个人客户 - 认证 + - 个人客户 - 认证 + parameters: [] + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/DtoVerifyAssetRequest' + responses: + '200': + description: 成功 + content: + application/json: + schema: + properties: + code: + description: 响应码 + type: integer + examples: + - 0 + data: + $ref: '#/components/schemas/DtoVerifyAssetResponse' + msg: + description: 响应消息 + type: string + examples: + - success + timestamp: + description: 时间戳 + format: date-time + type: string + required: + - code + - data + - msg + - timestamp + type: object + x-apifox-orders: + - code + - data + - msg + - timestamp + x-apifox-ignore-properties: [] + headers: {} + x-apifox-name: '' + '400': + description: 请求参数错误 + content: + application/json: + schema: &ref_0 + $ref: '#/components/schemas/ErrorResponse' + headers: {} + x-apifox-name: '' + '500': + description: 服务器内部错误 + content: + application/json: + schema: *ref_0 + headers: {} + x-apifox-name: '' + security: [] + x-apifox-folder: 个人客户 - 认证 + x-apifox-status: released + x-run-in-apifox: https://app.apifox.com/web/project/7591618/apis/api-430306471-run +components: + schemas: + DtoVerifyAssetRequest: + properties: + identifier: + description: 资产标识符(SN/IMEI/虚拟号/ICCID/MSISDN) + maxLength: 50 + minLength: 1 + type: string + required: + - identifier + type: object + x-apifox-orders: + - identifier + x-apifox-ignore-properties: [] + x-apifox-folder: '' + DtoVerifyAssetResponse: + properties: + asset_token: + description: 资产令牌(5分钟有效) + type: string + expires_in: + description: 过期时间(秒) + type: integer + miniapp_app_id: + description: 当前生效的小程序 AppID + type: string + oa_app_id: + description: 当前生效的公众号 AppID + type: string + type: object + x-apifox-orders: + - asset_token + - expires_in + - miniapp_app_id + - oa_app_id + x-apifox-ignore-properties: [] + x-apifox-folder: '' + ErrorResponse: + properties: + code: + description: 错误码 + type: integer + examples: + - 1001 + data: + description: 错误详情(可选) + type: object + x-apifox-orders: [] + properties: {} + x-apifox-ignore-properties: [] + msg: + description: 错误消息 + type: string + examples: + - 参数验证失败 + timestamp: + description: 时间戳 + format: date-time + type: string + required: + - code + - msg + - timestamp + type: object + x-apifox-orders: + - code + - data + - msg + - timestamp + x-apifox-ignore-properties: [] + x-apifox-folder: '' + securitySchemes: + BearerAuth: + bearerFormat: JWT + scheme: bearer + type: jwt + AgentOpenAPIAccount: + description: 代理账号标识,填写代理登录平台使用的用户名或手机号,例如 agent001。 + in: header + name: X-Agent-Account + type: apikey + AgentOpenAPINonce: + description: 调用方每次请求自行生成的随机串,不需要平台提前分配;同一账号在 5 分钟内不可重复。 + in: header + name: X-Agent-Nonce + type: apikey + AgentOpenAPIPassword: + description: 代理账号当前登录密码,同时作为 HMAC-SHA256 签名密钥;密码变更后必须使用新密码签名。 + in: header + name: X-Agent-Password + type: apikey + AgentOpenAPISign: + description: 调用方按签名规则实时计算的 HMAC-SHA256 小写十六进制签名,不是固定值。 + in: header + name: X-Agent-Sign + type: apikey + AgentOpenAPITimestamp: + description: 调用方生成的请求发起时间,支持 Unix 秒、Unix 毫秒或 RFC3339 时间,必须与签名原文 timestamp 行一致。 + in: header + name: X-Agent-Timestamp + type: apikey + Combination: + group: + - id: 1262265 + - id: 1262266 + - id: 1262267 + - id: 1262268 + - id: 1262269 + type: combination +servers: + - url: https://cmp-api.boss160.cn + description: 测试环境 +security: [] + +``` +# 公众号登录 + +## OpenAPI Specification + +```yaml +openapi: 3.0.1 +info: + title: '' + description: '' + version: 1.0.0 +paths: + /api/c/v1/auth/wechat-login: + post: + summary: 公众号登录 + deprecated: false + description: '' + tags: + - 个人客户 - 认证 + - 个人客户 - 认证 + parameters: [] + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/DtoWechatLoginRequest' + responses: + '200': + description: 成功 + content: + application/json: + schema: + properties: + code: + description: 响应码 + type: integer + examples: + - 0 + data: + $ref: '#/components/schemas/DtoWechatLoginResponse' + msg: + description: 响应消息 + type: string + examples: + - success + timestamp: + description: 时间戳 + format: date-time + type: string + required: + - code + - data + - msg + - timestamp + type: object + x-apifox-orders: + - code + - data + - msg + - timestamp + x-apifox-ignore-properties: [] + headers: {} + x-apifox-name: '' + '400': + description: 请求参数错误 + content: + application/json: + schema: &ref_0 + $ref: '#/components/schemas/ErrorResponse' + headers: {} + x-apifox-name: '' + '500': + description: 服务器内部错误 + content: + application/json: + schema: *ref_0 + headers: {} + x-apifox-name: '' + security: [] + x-apifox-folder: 个人客户 - 认证 + x-apifox-status: released + x-run-in-apifox: https://app.apifox.com/web/project/7591618/apis/api-430306472-run +components: + schemas: + DtoWechatLoginRequest: + properties: + asset_token: + description: A1返回的资产令牌 + type: string + code: + description: 微信OAuth授权码 + type: string + required: + - asset_token + - code + type: object + x-apifox-orders: + - asset_token + - code + x-apifox-ignore-properties: [] + x-apifox-folder: '' + DtoWechatLoginResponse: + properties: + is_new_user: + description: 是否新创建用户 + type: boolean + need_bind_phone: + description: 是否需要绑定手机号 + type: boolean + token: + description: 登录JWT令牌 + type: string + type: object + x-apifox-orders: + - is_new_user + - need_bind_phone + - token + x-apifox-ignore-properties: [] + x-apifox-folder: '' + ErrorResponse: + properties: + code: + description: 错误码 + type: integer + examples: + - 1001 + data: + description: 错误详情(可选) + type: object + x-apifox-orders: [] + properties: {} + x-apifox-ignore-properties: [] + msg: + description: 错误消息 + type: string + examples: + - 参数验证失败 + timestamp: + description: 时间戳 + format: date-time + type: string + required: + - code + - msg + - timestamp + type: object + x-apifox-orders: + - code + - data + - msg + - timestamp + x-apifox-ignore-properties: [] + x-apifox-folder: '' + securitySchemes: + BearerAuth: + bearerFormat: JWT + scheme: bearer + type: jwt + AgentOpenAPIAccount: + description: 代理账号标识,填写代理登录平台使用的用户名或手机号,例如 agent001。 + in: header + name: X-Agent-Account + type: apikey + AgentOpenAPINonce: + description: 调用方每次请求自行生成的随机串,不需要平台提前分配;同一账号在 5 分钟内不可重复。 + in: header + name: X-Agent-Nonce + type: apikey + AgentOpenAPIPassword: + description: 代理账号当前登录密码,同时作为 HMAC-SHA256 签名密钥;密码变更后必须使用新密码签名。 + in: header + name: X-Agent-Password + type: apikey + AgentOpenAPISign: + description: 调用方按签名规则实时计算的 HMAC-SHA256 小写十六进制签名,不是固定值。 + in: header + name: X-Agent-Sign + type: apikey + AgentOpenAPITimestamp: + description: 调用方生成的请求发起时间,支持 Unix 秒、Unix 毫秒或 RFC3339 时间,必须与签名原文 timestamp 行一致。 + in: header + name: X-Agent-Timestamp + type: apikey + Combination: + group: + - id: 1262265 + - id: 1262266 + - id: 1262267 + - id: 1262268 + - id: 1262269 + type: combination +servers: + - url: https://cmp-api.boss160.cn + description: 测试环境 +security: [] + +``` +# 发送验证码 + +## OpenAPI Specification + +```yaml +openapi: 3.0.1 +info: + title: '' + description: '' + version: 1.0.0 +paths: + /api/c/v1/auth/send-code: + post: + summary: 发送验证码 + deprecated: false + description: '' + tags: + - 个人客户 - 认证 + - 个人客户 - 认证 + parameters: [] + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/DtoClientSendCodeRequest' + responses: + '200': + description: 成功 + content: + application/json: + schema: + properties: + code: + description: 响应码 + type: integer + examples: + - 0 + data: + $ref: '#/components/schemas/DtoClientSendCodeResponse' + msg: + description: 响应消息 + type: string + examples: + - success + timestamp: + description: 时间戳 + format: date-time + type: string + required: + - code + - data + - msg + - timestamp + type: object + x-apifox-orders: + - code + - data + - msg + - timestamp + x-apifox-ignore-properties: [] + headers: {} + x-apifox-name: '' + '400': + description: 请求参数错误 + content: + application/json: + schema: &ref_0 + $ref: '#/components/schemas/ErrorResponse' + headers: {} + x-apifox-name: '' + '500': + description: 服务器内部错误 + content: + application/json: + schema: *ref_0 + headers: {} + x-apifox-name: '' + security: [] + x-apifox-folder: 个人客户 - 认证 + x-apifox-status: released + x-run-in-apifox: https://app.apifox.com/web/project/7591618/apis/api-430306470-run +components: + schemas: + DtoClientSendCodeRequest: + properties: + phone: + description: 手机号 + maxLength: 11 + minLength: 11 + type: string + scene: + description: >- + 业务场景 (bind_phone:绑定手机号, change_phone_old:换绑旧手机, + change_phone_new:换绑新手机) + type: string + required: + - phone + - scene + type: object + x-apifox-orders: + - phone + - scene + x-apifox-ignore-properties: [] + x-apifox-folder: '' + DtoClientSendCodeResponse: + properties: + cooldown_seconds: + description: 冷却秒数 + type: integer + type: object + x-apifox-orders: + - cooldown_seconds + x-apifox-ignore-properties: [] + x-apifox-folder: '' + ErrorResponse: + properties: + code: + description: 错误码 + type: integer + examples: + - 1001 + data: + description: 错误详情(可选) + type: object + x-apifox-orders: [] + properties: {} + x-apifox-ignore-properties: [] + msg: + description: 错误消息 + type: string + examples: + - 参数验证失败 + timestamp: + description: 时间戳 + format: date-time + type: string + required: + - code + - msg + - timestamp + type: object + x-apifox-orders: + - code + - data + - msg + - timestamp + x-apifox-ignore-properties: [] + x-apifox-folder: '' + securitySchemes: + BearerAuth: + bearerFormat: JWT + scheme: bearer + type: jwt + AgentOpenAPIAccount: + description: 代理账号标识,填写代理登录平台使用的用户名或手机号,例如 agent001。 + in: header + name: X-Agent-Account + type: apikey + AgentOpenAPINonce: + description: 调用方每次请求自行生成的随机串,不需要平台提前分配;同一账号在 5 分钟内不可重复。 + in: header + name: X-Agent-Nonce + type: apikey + AgentOpenAPIPassword: + description: 代理账号当前登录密码,同时作为 HMAC-SHA256 签名密钥;密码变更后必须使用新密码签名。 + in: header + name: X-Agent-Password + type: apikey + AgentOpenAPISign: + description: 调用方按签名规则实时计算的 HMAC-SHA256 小写十六进制签名,不是固定值。 + in: header + name: X-Agent-Sign + type: apikey + AgentOpenAPITimestamp: + description: 调用方生成的请求发起时间,支持 Unix 秒、Unix 毫秒或 RFC3339 时间,必须与签名原文 timestamp 行一致。 + in: header + name: X-Agent-Timestamp + type: apikey + Combination: + group: + - id: 1262265 + - id: 1262266 + - id: 1262267 + - id: 1262268 + - id: 1262269 + type: combination +servers: + - url: https://cmp-api.boss160.cn + description: 测试环境 +security: [] + +``` +# 绑定手机号 + +## OpenAPI Specification + +```yaml +openapi: 3.0.1 +info: + title: '' + description: '' + version: 1.0.0 +paths: + /api/c/v1/auth/bind-phone: + post: + summary: 绑定手机号 + deprecated: false + description: '' + tags: + - 个人客户 - 认证 + - 个人客户 - 认证 + parameters: [] + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/DtoBindPhoneRequest' + responses: + '200': + description: 成功 + content: + application/json: + schema: + properties: + code: + description: 响应码 + type: integer + examples: + - 0 + data: + $ref: '#/components/schemas/DtoBindPhoneResponse' + msg: + description: 响应消息 + type: string + examples: + - success + timestamp: + description: 时间戳 + format: date-time + type: string + required: + - code + - data + - msg + - timestamp + type: object + x-apifox-orders: + - code + - data + - msg + - timestamp + x-apifox-ignore-properties: [] + headers: {} + x-apifox-name: '' + '400': + description: 请求参数错误 + content: + application/json: + schema: &ref_0 + $ref: '#/components/schemas/ErrorResponse' + headers: {} + x-apifox-name: '' + '401': + description: 未认证或认证已过期 + content: + application/json: + schema: *ref_0 + headers: {} + x-apifox-name: '' + '403': + description: 无权访问 + content: + application/json: + schema: *ref_0 + headers: {} + x-apifox-name: '' + '500': + description: 服务器内部错误 + content: + application/json: + schema: *ref_0 + headers: {} + x-apifox-name: '' + security: + - BearerAuth: [] + x-apifox: + schemeGroups: + - id: QWhSmAXxQX3rSb4Ya24X5 + schemeIds: + - BearerAuth + required: true + use: + id: QWhSmAXxQX3rSb4Ya24X5 + scopes: + QWhSmAXxQX3rSb4Ya24X5: + BearerAuth: [] + x-apifox-folder: 个人客户 - 认证 + x-apifox-status: released + x-run-in-apifox: https://app.apifox.com/web/project/7591618/apis/api-430306466-run +components: + schemas: + DtoBindPhoneRequest: + properties: + code: + description: 验证码 + maxLength: 6 + minLength: 6 + type: string + phone: + description: 手机号 + maxLength: 11 + minLength: 11 + type: string + required: + - code + - phone + type: object + x-apifox-orders: + - code + - phone + x-apifox-ignore-properties: [] + x-apifox-folder: '' + DtoBindPhoneResponse: + properties: + bound_at: + description: 绑定时间 + type: string + phone: + description: 已绑定手机号 + type: string + type: object + x-apifox-orders: + - bound_at + - phone + x-apifox-ignore-properties: [] + x-apifox-folder: '' + ErrorResponse: + properties: + code: + description: 错误码 + type: integer + examples: + - 1001 + data: + description: 错误详情(可选) + type: object + x-apifox-orders: [] + properties: {} + x-apifox-ignore-properties: [] + msg: + description: 错误消息 + type: string + examples: + - 参数验证失败 + timestamp: + description: 时间戳 + format: date-time + type: string + required: + - code + - msg + - timestamp + type: object + x-apifox-orders: + - code + - data + - msg + - timestamp + x-apifox-ignore-properties: [] + x-apifox-folder: '' + securitySchemes: + BearerAuth: + bearerFormat: JWT + scheme: bearer + type: jwt + AgentOpenAPIAccount: + description: 代理账号标识,填写代理登录平台使用的用户名或手机号,例如 agent001。 + in: header + name: X-Agent-Account + type: apikey + AgentOpenAPINonce: + description: 调用方每次请求自行生成的随机串,不需要平台提前分配;同一账号在 5 分钟内不可重复。 + in: header + name: X-Agent-Nonce + type: apikey + AgentOpenAPIPassword: + description: 代理账号当前登录密码,同时作为 HMAC-SHA256 签名密钥;密码变更后必须使用新密码签名。 + in: header + name: X-Agent-Password + type: apikey + AgentOpenAPISign: + description: 调用方按签名规则实时计算的 HMAC-SHA256 小写十六进制签名,不是固定值。 + in: header + name: X-Agent-Sign + type: apikey + AgentOpenAPITimestamp: + description: 调用方生成的请求发起时间,支持 Unix 秒、Unix 毫秒或 RFC3339 时间,必须与签名原文 timestamp 行一致。 + in: header + name: X-Agent-Timestamp + type: apikey + Combination: + group: + - id: 1262265 + - id: 1262266 + - id: 1262267 + - id: 1262268 + - id: 1262269 + type: combination +servers: + - url: https://cmp-api.boss160.cn + description: 测试环境 +security: [] + +``` +# 更换手机号 + +## OpenAPI Specification + +```yaml +openapi: 3.0.1 +info: + title: '' + description: '' + version: 1.0.0 +paths: + /api/c/v1/auth/change-phone: + post: + summary: 更换手机号 + deprecated: false + description: '' + tags: + - 个人客户 - 认证 + - 个人客户 - 认证 + parameters: [] + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/DtoChangePhoneRequest' + responses: + '200': + description: 成功 + content: + application/json: + schema: + properties: + code: + description: 响应码 + type: integer + examples: + - 0 + data: + $ref: '#/components/schemas/DtoChangePhoneResponse' + msg: + description: 响应消息 + type: string + examples: + - success + timestamp: + description: 时间戳 + format: date-time + type: string + required: + - code + - data + - msg + - timestamp + type: object + x-apifox-orders: + - code + - data + - msg + - timestamp + x-apifox-ignore-properties: [] + headers: {} + x-apifox-name: '' + '400': + description: 请求参数错误 + content: + application/json: + schema: &ref_0 + $ref: '#/components/schemas/ErrorResponse' + headers: {} + x-apifox-name: '' + '401': + description: 未认证或认证已过期 + content: + application/json: + schema: *ref_0 + headers: {} + x-apifox-name: '' + '403': + description: 无权访问 + content: + application/json: + schema: *ref_0 + headers: {} + x-apifox-name: '' + '500': + description: 服务器内部错误 + content: + application/json: + schema: *ref_0 + headers: {} + x-apifox-name: '' + security: + - BearerAuth: [] + x-apifox: + schemeGroups: + - id: '-dyyjrlq-bq-ppwJqlHKH' + schemeIds: + - BearerAuth + required: true + use: + id: '-dyyjrlq-bq-ppwJqlHKH' + scopes: + '-dyyjrlq-bq-ppwJqlHKH': + BearerAuth: [] + x-apifox-folder: 个人客户 - 认证 + x-apifox-status: released + x-run-in-apifox: https://app.apifox.com/web/project/7591618/apis/api-430306467-run +components: + schemas: + DtoChangePhoneRequest: + properties: + new_code: + description: 新手机号验证码 + maxLength: 6 + minLength: 6 + type: string + new_phone: + description: 新手机号 + maxLength: 11 + minLength: 11 + type: string + old_code: + description: 旧手机号验证码 + maxLength: 6 + minLength: 6 + type: string + old_phone: + description: 旧手机号 + maxLength: 11 + minLength: 11 + type: string + required: + - new_code + - new_phone + - old_code + - old_phone + type: object + x-apifox-orders: + - new_code + - new_phone + - old_code + - old_phone + x-apifox-ignore-properties: [] + x-apifox-folder: '' + DtoChangePhoneResponse: + properties: + changed_at: + description: 换绑时间 + type: string + phone: + description: 换绑后手机号 + type: string + type: object + x-apifox-orders: + - changed_at + - phone + x-apifox-ignore-properties: [] + x-apifox-folder: '' + ErrorResponse: + properties: + code: + description: 错误码 + type: integer + examples: + - 1001 + data: + description: 错误详情(可选) + type: object + x-apifox-orders: [] + properties: {} + x-apifox-ignore-properties: [] + msg: + description: 错误消息 + type: string + examples: + - 参数验证失败 + timestamp: + description: 时间戳 + format: date-time + type: string + required: + - code + - msg + - timestamp + type: object + x-apifox-orders: + - code + - data + - msg + - timestamp + x-apifox-ignore-properties: [] + x-apifox-folder: '' + securitySchemes: + BearerAuth: + bearerFormat: JWT + scheme: bearer + type: jwt + AgentOpenAPIAccount: + description: 代理账号标识,填写代理登录平台使用的用户名或手机号,例如 agent001。 + in: header + name: X-Agent-Account + type: apikey + AgentOpenAPINonce: + description: 调用方每次请求自行生成的随机串,不需要平台提前分配;同一账号在 5 分钟内不可重复。 + in: header + name: X-Agent-Nonce + type: apikey + AgentOpenAPIPassword: + description: 代理账号当前登录密码,同时作为 HMAC-SHA256 签名密钥;密码变更后必须使用新密码签名。 + in: header + name: X-Agent-Password + type: apikey + AgentOpenAPISign: + description: 调用方按签名规则实时计算的 HMAC-SHA256 小写十六进制签名,不是固定值。 + in: header + name: X-Agent-Sign + type: apikey + AgentOpenAPITimestamp: + description: 调用方生成的请求发起时间,支持 Unix 秒、Unix 毫秒或 RFC3339 时间,必须与签名原文 timestamp 行一致。 + in: header + name: X-Agent-Timestamp + type: apikey + Combination: + group: + - id: 1262265 + - id: 1262266 + - id: 1262267 + - id: 1262268 + - id: 1262269 + type: combination +servers: + - url: https://cmp-api.boss160.cn + description: 测试环境 +security: [] + +``` +# 资产信息 + +## OpenAPI Specification + +```yaml +openapi: 3.0.1 +info: + title: '' + description: '' + version: 1.0.0 +paths: + /api/c/v1/asset/info: + get: + summary: 资产信息 + deprecated: false + description: '' + tags: + - 个人客户 - 资产 + - 个人客户 - 资产 + parameters: + - name: identifier + in: query + description: 资产标识符(SN/IMEI/虚拟号/ICCID/MSISDN) + required: true + schema: + description: 资产标识符(SN/IMEI/虚拟号/ICCID/MSISDN) + maxLength: 50 + minLength: 1 + type: string + responses: + '200': + description: 成功 + content: + application/json: + schema: + properties: + code: + description: 响应码 + type: integer + examples: + - 0 + data: + $ref: '#/components/schemas/DtoAssetInfoResponse' + msg: + description: 响应消息 + type: string + examples: + - success + timestamp: + description: 时间戳 + format: date-time + type: string + required: + - code + - data + - msg + - timestamp + type: object + x-apifox-orders: + - code + - data + - msg + - timestamp + x-apifox-ignore-properties: [] + headers: {} + x-apifox-name: '' + '400': + description: 请求参数错误 + content: + application/json: + schema: &ref_0 + $ref: '#/components/schemas/ErrorResponse' + headers: {} + x-apifox-name: '' + '401': + description: 未认证或认证已过期 + content: + application/json: + schema: *ref_0 + headers: {} + x-apifox-name: '' + '403': + description: 无权访问 + content: + application/json: + schema: *ref_0 + headers: {} + x-apifox-name: '' + '500': + description: 服务器内部错误 + content: + application/json: + schema: *ref_0 + headers: {} + x-apifox-name: '' + security: + - BearerAuth: [] + x-apifox: + schemeGroups: + - id: JiRokJdNwH5k5TeDMhlek + schemeIds: + - BearerAuth + required: true + use: + id: JiRokJdNwH5k5TeDMhlek + scopes: + JiRokJdNwH5k5TeDMhlek: + BearerAuth: [] + x-apifox-folder: 个人客户 - 资产 + x-apifox-status: released + x-run-in-apifox: https://app.apifox.com/web/project/7591618/apis/api-430306480-run +components: + schemas: + DtoAssetInfoResponse: + properties: + activated_at: + description: 激活时间 + format: date-time + type: string + nullable: true + activation_status: + description: 激活状态(0:未激活, 1:已激活;设备需有生效中主套餐且任意绑定卡已实名) + type: integer + activation_status_name: + description: 激活状态名称(中文) + type: string + allowed_payment_methods: + description: 当前资产允许的支付方式列表 (wallet:钱包, wechat:微信, alipay:支付宝) + items: + type: string + type: array + nullable: true + asset_id: + description: 资产ID + minimum: 0 + type: integer + asset_type: + description: 资产类型(card:卡, device:设备) + type: string + bound_card_count: + description: 绑定卡数量 + type: integer + bound_device_id: + description: 绑定的设备ID + minimum: 0 + type: integer + nullable: true + bound_device_name: + description: 绑定的设备名称 + type: string + bound_device_no: + description: 绑定的设备虚拟号 + type: string + bound_phone: + description: 当前登录用户绑定的手机号 + type: string + card_category: + description: 卡业务类型(normal:普通卡, industry:行业卡) + type: string + cards: + description: 绑定卡列表(含每张卡的ICCID/MSISDN/网络状态/实名状态/插槽位置) + items: + $ref: '#/components/schemas/DtoBoundCardInfo' + type: array + carrier_id: + description: 运营商ID + minimum: 0 + type: integer + carrier_name: + description: 运营商名称 + type: string + carrier_type: + description: 运营商类型(CMCC/CUCC/CTCC/CBN) + type: string + current_package: + description: 当前套餐名称(无套餐时为空) + type: string + current_package_activated_at: + description: 当前主套餐开始时间 + format: date-time + type: string + nullable: true + current_package_expires_at: + description: 当前主套餐过期时间 + format: date-time + type: string + nullable: true + current_package_id: + description: 当前主套餐ID,无主套餐时为0,可用于发起续费 + minimum: 0 + type: integer + current_package_usage_id: + description: 当前主套餐的套餐使用记录ID,无主套餐时为 null + minimum: 0 + type: integer + nullable: true + days_until_final_expiry: + description: 距预计套餐到期的上海自然日天数,无法精确推算时为 null + type: integer + nullable: true + device_model: + description: 设备型号 + type: string + device_name: + description: 设备名称 + type: string + device_protect_status: + description: 设备保护期状态(none:无, stop:停机保护, start:开机保护) + type: string + device_realtime: + $ref: '#/components/schemas/DtoDeviceRealtimeInfo' + device_type: + description: 设备类型 + type: string + effective_realname_policy: + description: >- + 当前资产实际生效的实名认证策略 (none:无需实名, before_order:先实名后充值/购买, + after_order:先充值/购买后实名) + type: string + enable_virtual_data: + description: 当前主套餐是否启用虚流量(按套餐使用记录快照返回) + type: boolean + estimated_final_expires_at: + description: 预计套餐到期时间,无法精确推算时为 null + format: date-time + type: string + nullable: true + expiry_estimate_status: + description: >- + 预计套餐到期推算状态 (exact:可精确推算, waiting_activation:待激活后起算, none:无参与套餐, + invalid_data:数据异常) + type: string + expiry_estimate_status_name: + description: 预计套餐到期推算状态名称(中文) + type: string + gateway_extend: + description: Gateway 卡状态扩展字段,原样返回上游 extend,用于展示运营商侧实际停机原因 + type: string + generation: + description: 世代 + type: string + iccid: + description: 卡ICCID + type: string + identifier: + description: 资产标识符 + type: string + imei: + description: 设备IMEI + type: string + is_expiring: + description: 是否临期(仅精确推算且剩余 0 至 15 个上海自然日时为 true) + type: boolean + manufacturer: + description: 制造商 + type: string + max_sim_slots: + description: 最大插槽数 + type: integer + msisdn: + description: 手机号 + type: string + network_status: + description: 网络状态(0:停机, 1:开机) + type: integer + network_status_name: + description: 网络状态名称(中文) + type: string + real_name_status: + description: 实名状态(0:未实名, 1:已实名) + type: integer + real_name_status_name: + description: 实名状态名称(中文) + type: string + real_total_mb: + description: 当前主套餐真实总量(MB) + type: integer + real_used_mb: + description: 当前主套餐真实已用量(MB) + type: integer + realname_policy: + description: 实名认证策略 (none:无需实名, before_order:先实名后充值/购买, after_order:先充值/购买后实名) + type: string + realname_required: + description: 当前资产是否要求完成实名认证 + type: boolean + reduction_pct: + description: 展示增幅比例,公式为(real_total_mb / virtual_total_mb) - 1 + type: number + renewal_price: + description: 当前主套餐续费价格(分,按当前销售渠道的生效零售价计算;无主套餐或当前渠道不可续费时为 null) + type: integer + nullable: true + sn: + description: 设备序列号 + type: string + status: + description: 资产归属状态(1:在库, 2:已分销;卡仍沿用原卡状态枚举) + type: integer + status_name: + description: 状态名称(中文) + type: string + virtual_no: + description: 虚拟号 + type: string + virtual_total_mb: + description: 当前主套餐业务停机阈值(MB) + type: integer + virtual_used_mb: + description: 当前主套餐展示已用量(MB) + type: number + wallet_balance: + description: 钱包余额(分) + type: integer + type: object + x-apifox-orders: + - activated_at + - activation_status + - activation_status_name + - allowed_payment_methods + - asset_id + - asset_type + - bound_card_count + - bound_device_id + - bound_device_name + - bound_device_no + - bound_phone + - card_category + - cards + - carrier_id + - carrier_name + - carrier_type + - current_package + - current_package_activated_at + - current_package_expires_at + - current_package_id + - current_package_usage_id + - days_until_final_expiry + - device_model + - device_name + - device_protect_status + - device_realtime + - device_type + - effective_realname_policy + - enable_virtual_data + - estimated_final_expires_at + - expiry_estimate_status + - expiry_estimate_status_name + - gateway_extend + - generation + - iccid + - identifier + - imei + - is_expiring + - manufacturer + - max_sim_slots + - msisdn + - network_status + - network_status_name + - real_name_status + - real_name_status_name + - real_total_mb + - real_used_mb + - realname_policy + - realname_required + - reduction_pct + - renewal_price + - sn + - status + - status_name + - virtual_no + - virtual_total_mb + - virtual_used_mb + - wallet_balance + x-apifox-ignore-properties: [] + x-apifox-folder: '' + DtoDeviceRealtimeInfo: + properties: + battery_level: + description: 电池电量百分比 + type: integer + nullable: true + client_number: + description: 当前已连接客户端数 + type: integer + nullable: true + connect_time: + description: 设备本次联网时间(秒) + type: string + nullable: true + created_at: + description: Gateway创建时间(Unix时间戳) + type: integer + nullable: true + current_iccid: + description: 当前使用的ICCID + type: string + nullable: true + daily_usage: + description: 日使用流量(字节) + type: string + nullable: true + device_id: + description: Gateway设备ID(IMEI/SN) + type: string + nullable: true + device_name: + description: Gateway返回的设备名称 + type: string + nullable: true + device_type: + description: Gateway返回的设备型号 + type: string + nullable: true + dl_stats: + description: 本次开机下载流量(字节) + type: string + nullable: true + gateway_msg: + description: Gateway 上游消息(成功时为 null,失败时为错误信息) + type: string + nullable: true + imei: + description: Gateway返回的IMEI号 + type: string + nullable: true + imsi: + description: Gateway返回的IMSI + type: string + nullable: true + ip_address: + description: IP地址 + type: string + nullable: true + lan_ip: + description: 局域网网关IP地址 + type: string + nullable: true + last_online_time: + description: 设备最后在线时间 + type: string + nullable: true + last_update_time: + description: 设备信息最后更新时间 + type: string + nullable: true + limit_speed: + description: 限速速率(KB/s) + type: integer + nullable: true + mac_address: + description: MAC地址 + type: string + nullable: true + max_clients: + description: 最大连接客户端数 + type: integer + nullable: true + online_status: + description: 在线状态(1:在线, 2:离线) + type: integer + nullable: true + rsrp: + description: 参考信号接收功率(dBm) + type: integer + nullable: true + rsrq: + description: 参考信号接收质量(dB) + type: integer + nullable: true + rssi: + description: 接收信号强度 + type: string + nullable: true + run_time: + description: 设备本次开机运行时间(秒) + type: string + nullable: true + signal_bad_reason: + description: 信号较弱时的怀疑原因提示 + type: string + nullable: true + signal_quality: + description: 信号综合判断(信号很好/信号正常/信号较弱/信号很差/暂无数据) + type: string + nullable: true + sinr: + description: 信噪比(dB) + type: integer + nullable: true + software_version: + description: 软件版本号 + type: string + nullable: true + ssid: + description: WiFi热点名称 + type: string + nullable: true + status: + description: 设备状态(1:正常, 0:禁用) + type: integer + nullable: true + switch_mode: + description: 切卡模式(0:自动, 1:手动) + type: integer + nullable: true + sync_interval: + description: 信息上报周期(秒) + type: integer + nullable: true + ul_stats: + description: 本次开机上传流量(字节) + type: string + nullable: true + updated_at: + description: Gateway更新时间(Unix时间戳) + type: integer + nullable: true + wan_ip: + description: 基站分配IPv4地址 + type: string + nullable: true + wifi_enabled: + description: WiFi开关状态 + type: boolean + nullable: true + wifi_password: + description: WiFi密码 + type: string + nullable: true + type: object + x-apifox-orders: + - battery_level + - client_number + - connect_time + - created_at + - current_iccid + - daily_usage + - device_id + - device_name + - device_type + - dl_stats + - gateway_msg + - imei + - imsi + - ip_address + - lan_ip + - last_online_time + - last_update_time + - limit_speed + - mac_address + - max_clients + - online_status + - rsrp + - rsrq + - rssi + - run_time + - signal_bad_reason + - signal_quality + - sinr + - software_version + - ssid + - status + - switch_mode + - sync_interval + - ul_stats + - updated_at + - wan_ip + - wifi_enabled + - wifi_password + x-apifox-ignore-properties: [] + x-apifox-folder: '' + DtoBoundCardInfo: + properties: + card_id: + description: 卡ID + minimum: 0 + type: integer + carrier_id: + description: 运营商ID + minimum: 0 + type: integer + carrier_name: + description: 运营商名称 + type: string + carrier_type: + description: 运营商类型 + type: string + gateway_extend: + description: Gateway 卡状态扩展字段,原样返回上游 extend,用于展示运营商侧实际停机原因 + type: string + iccid: + description: ICCID + type: string + is_current: + description: 是否为当前使用的卡 + type: boolean + last_card_status_check_at: + description: 最后一次卡状态检查时间 + format: date-time + type: string + nullable: true + last_data_check_at: + description: 最后一次流量检查时间 + format: date-time + type: string + nullable: true + last_real_name_check_at: + description: 最后一次实名检查时间 + format: date-time + type: string + nullable: true + last_sync_time: + description: 最后同步时间 + format: date-time + type: string + nullable: true + msisdn: + description: 手机号 + type: string + network_status: + description: 网络状态:0停机 1开机 + type: integer + real_name_at: + description: 该卡最近一次完成实名的时间,未实名时为 null + format: date-time + type: string + nullable: true + real_name_status: + description: 实名状态:0未实名 1已实名 + type: integer + realname_policy: + description: 实名认证策略 (none:无需实名, before_order:先实名后充值/购买, after_order:先充值/购买后实名) + type: string + slot_position: + description: 插槽位置 + type: integer + type: object + x-apifox-orders: + - card_id + - carrier_id + - carrier_name + - carrier_type + - gateway_extend + - iccid + - is_current + - last_card_status_check_at + - last_data_check_at + - last_real_name_check_at + - last_sync_time + - msisdn + - network_status + - real_name_at + - real_name_status + - realname_policy + - slot_position + x-apifox-ignore-properties: [] + x-apifox-folder: '' + ErrorResponse: + properties: + code: + description: 错误码 + type: integer + examples: + - 1001 + data: + description: 错误详情(可选) + type: object + x-apifox-orders: [] + properties: {} + x-apifox-ignore-properties: [] + msg: + description: 错误消息 + type: string + examples: + - 参数验证失败 + timestamp: + description: 时间戳 + format: date-time + type: string + required: + - code + - msg + - timestamp + type: object + x-apifox-orders: + - code + - data + - msg + - timestamp + x-apifox-ignore-properties: [] + x-apifox-folder: '' + securitySchemes: + BearerAuth: + bearerFormat: JWT + scheme: bearer + type: jwt + AgentOpenAPIAccount: + description: 代理账号标识,填写代理登录平台使用的用户名或手机号,例如 agent001。 + in: header + name: X-Agent-Account + type: apikey + AgentOpenAPINonce: + description: 调用方每次请求自行生成的随机串,不需要平台提前分配;同一账号在 5 分钟内不可重复。 + in: header + name: X-Agent-Nonce + type: apikey + AgentOpenAPIPassword: + description: 代理账号当前登录密码,同时作为 HMAC-SHA256 签名密钥;密码变更后必须使用新密码签名。 + in: header + name: X-Agent-Password + type: apikey + AgentOpenAPISign: + description: 调用方按签名规则实时计算的 HMAC-SHA256 小写十六进制签名,不是固定值。 + in: header + name: X-Agent-Sign + type: apikey + AgentOpenAPITimestamp: + description: 调用方生成的请求发起时间,支持 Unix 秒、Unix 毫秒或 RFC3339 时间,必须与签名原文 timestamp 行一致。 + in: header + name: X-Agent-Timestamp + type: apikey + Combination: + group: + - id: 1262265 + - id: 1262266 + - id: 1262267 + - id: 1262268 + - id: 1262269 + type: combination +servers: + - url: https://cmp-api.boss160.cn + description: 测试环境 +security: [] + +``` \ No newline at end of file diff --git a/openspec/changes/add-h5-popup-candidate-flow/proposal.md b/openspec/changes/add-h5-popup-candidate-flow/proposal.md new file mode 100644 index 0000000..b63edab --- /dev/null +++ b/openspec/changes/add-h5-popup-candidate-flow/proposal.md @@ -0,0 +1,28 @@ +## Why + +The August iteration adds backend-driven H5 popups. `GET /api/c/v1/popup-candidates` returns at most one candidate (risk-exchange or operation) for the current page and asset; risk-exchange candidates require the user to submit a shipping address through `POST /api/c/v1/risk-exchanges/{asset_id}/address`. Popups are persisted as personal notifications, so the existing notification contracts change: list items gain `popup_snapshot` and two new `type` values, unread count includes popups, and a popup is only dismissed by marking its notification read (`PUT /api/c/v1/notifications/{id}/read`). The current H5 client-side priority re-sort contradicts the new fixed backend ordering and must be removed. + +## What Changes + +- Query `GET /api/c/v1/popup-candidates` once per page entry on `home` (index), `package_purchase` (package-order), and `asset_wallet_recharge` (my-wallet); `asset_detail` is explicitly out of scope +- Render the returned candidate (title/body) in a shared `PopupCandidate` component and keep the popup silent when no candidate exists +- Map `action_type` with a frontend whitelist only: `package_purchase` -> package-order page, `asset_wallet_recharge` -> my-wallet page; empty value shows no action +- Dismiss or act on a popup only by marking the notification read through `PUT /api/c/v1/notifications/{id}/read` +- Risk-exchange popups show a shipping address form and submit it with the candidate `asset_id` (never derived from the H5 identifier); success shows the exchange number and status +- Treat invisible assets (HTTP 400 / `code=1180`) uniformly as resource-invisible with no UI branching +- Keep the backend-fixed notification ordering (remove client-side priority re-sort); honor the unread badge contract; include the notification id in mark-read requests +- Never use `read-all` inside the popup display chain + +## Capabilities + +### New Capabilities +- `h5-popup-candidate`: Define once-per-entry popup candidate query, rendering, whitelist action mapping, and read-mark dismissal +- `h5-risk-exchange-address`: Define the risk-exchange shipping address form and idempotent submission + +### Modified Capabilities +- `personal-notification-list`: Align the personal notification list/unread contracts with popup notifications + +## Impact + +- Affected code: new `api/modules/popup.js`, new `components/PopupCandidate.vue`, `pages/index/index.vue`, `pages/package-order/package-order.vue`, `pages/my-wallet/my-wallet.vue`, `pages/notifications/notifications.vue`, `api/modules/notification.js` +- Backend contracts: C-end popup/risk-exchange/notification endpoints only; admin endpoints are out of scope diff --git a/openspec/changes/add-h5-popup-candidate-flow/specs/h5-popup-candidate/spec.md b/openspec/changes/add-h5-popup-candidate-flow/specs/h5-popup-candidate/spec.md new file mode 100644 index 0000000..7094ffa --- /dev/null +++ b/openspec/changes/add-h5-popup-candidate-flow/specs/h5-popup-candidate/spec.md @@ -0,0 +1,52 @@ +## ADDED Requirements + +### Requirement: H5 SHALL query popup candidates once per page entry +The system SHALL call `GET /api/c/v1/popup-candidates` exactly once per page entry with the current page and asset identifier, and SHALL render the returned candidate or stay silent when none exists. The query SHALL NOT be prefetched, polled, or repeated inside one entry. + +#### Scenario: Candidate exists on page entry +- **WHEN** the user enters `home`, `package_purchase`, or `asset_wallet_recharge` and the backend returns a candidate +- **THEN** the system SHALL show the candidate title/body popup + +#### Scenario: No candidate on page entry +- **WHEN** the backend returns an empty candidate +- **THEN** the system SHALL show no popup +- **AND** the page SHALL continue its normal flow + +#### Scenario: Same notification id within one day +- **WHEN** a later page entry returns the same `notification_id` already handled this session +- **THEN** the system SHALL NOT re-popup the same notification + +### Requirement: Popup actions SHALL use a frontend whitelist +The system SHALL map `action_type` through a frontend whitelist only and SHALL NOT accept URLs or routes from the backend. `package_purchase` routes to the package purchase page, `asset_wallet_recharge` routes to the wallet page, and an empty value shows no action. + +#### Scenario: package_purchase action +- **WHEN** the candidate `action_type` is `package_purchase` +- **THEN** the popup SHALL navigate to the package purchase page + +#### Scenario: asset_wallet_recharge action +- **WHEN** the candidate `action_type` is `asset_wallet_recharge` +- **THEN** the popup SHALL navigate to the asset wallet page + +#### Scenario: No action +- **WHEN** `action_type` is empty +- **THEN** the popup SHALL show no action button + +### Requirement: Popup dismissal SHALL mark the notification read +The system SHALL call `PUT /api/c/v1/notifications/{id}/read` with the candidate `notification_id` when the popup is dismissed or when its action is taken. Popup closing and action-taking SHALL NOT use `read-all`. + +#### Scenario: User closes the popup +- **WHEN** the user closes the popup +- **THEN** the notification SHALL be marked read + +#### Scenario: User takes the popup action +- **WHEN** the user taps the action button +- **THEN** the notification SHALL be marked read +- **AND** the mapped page SHALL open + +### Requirement: Resource-invisible responses SHALL be silent +The system SHALL treat HTTP 400 / `code=1180` responses uniformly as resource-invisible and SHALL NOT branch UI, show special prompts, or distinguish asset-missing from not-owned states. + +#### Scenario: Invisible asset on popup query +- **WHEN** the popup query returns an invisible-asset error +- **THEN** the system SHALL show no popup +- **AND** the page SHALL continue its normal flow diff --git a/openspec/changes/add-h5-popup-candidate-flow/specs/h5-risk-exchange-address/spec.md b/openspec/changes/add-h5-popup-candidate-flow/specs/h5-risk-exchange-address/spec.md new file mode 100644 index 0000000..9120641 --- /dev/null +++ b/openspec/changes/add-h5-popup-candidate-flow/specs/h5-risk-exchange-address/spec.md @@ -0,0 +1,28 @@ +## ADDED Requirements + +### Requirement: Risk-exchange popup SHALL collect a shipping address +The system SHALL present a shipping address form when the candidate `popup_type` is `risk_exchange`, collecting recipient name, recipient phone, and full address, and SHALL submit them to `POST /api/c/v1/risk-exchanges/{asset_id}/address` using the candidate `asset_id` (never derived from the H5 identifier). + +#### Scenario: Valid address submission +- **WHEN** the user fills the required fields and submits +- **THEN** the risk-exchange request SHALL be submitted with the candidate `asset_id` +- **AND** the system SHALL show the returned exchange number and status + +#### Scenario: Invalid or incomplete fields +- **WHEN** the user submits empty or invalid fields +- **THEN** the system SHALL block submission and prompt for the missing fields + +### Requirement: Address submission SHALL be idempotent +The system SHALL NOT provide an edit-address UI. Repeated or concurrent submissions return the first-created exchange order and address without overwriting. + +#### Scenario: Repeated submission +- **WHEN** the user submits the address again for the same asset +- **THEN** the system SHALL return the first-created exchange order and address + +### Requirement: Risk-exchange failures SHALL keep the user in place +The system SHALL keep the address form open on failure and treat invisible-asset failures uniformly without UI branching. + +#### Scenario: Submission failure +- **WHEN** the address submission fails +- **THEN** the system SHALL keep the form open +- **AND** the user SHALL be able to retry diff --git a/openspec/changes/add-h5-popup-candidate-flow/specs/personal-notification-list/spec.md b/openspec/changes/add-h5-popup-candidate-flow/specs/personal-notification-list/spec.md new file mode 100644 index 0000000..aad301b --- /dev/null +++ b/openspec/changes/add-h5-popup-candidate-flow/specs/personal-notification-list/spec.md @@ -0,0 +1,36 @@ +## MODIFIED Requirements + +### Requirement: Personal notification list SHALL preserve backend ordering +The system SHALL display personal notifications in the backend-fixed order (created time and notification id descending) and SHALL NOT re-sort items client-side by severity or priority. + +#### Scenario: Backend returns a fixed order +- **WHEN** the notification list is loaded +- **THEN** the items SHALL be rendered in the returned order + +### Requirement: Notification items SHALL pass through popup fields and types +The system SHALL render items with the new `type` values `h5.popup.risk_exchange` and `h5.popup.operation` (category `system`) and SHALL keep `popup_snapshot` available for popup-related actions without treating it as a route. + +#### Scenario: List contains a popup notification +- **WHEN** an item has `type` `h5.popup.risk_exchange` or `h5.popup.operation` +- **THEN** the item SHALL render as a normal system-category notification + +### Requirement: Unread badge SHALL reflect popup notifications +The system SHALL count popup notifications in the unread badge and collapse the badge to `99+` above 99 per the `display_count` contract. + +#### Scenario: Popup notification unread +- **WHEN** a popup notification is unread +- **THEN** the unread badge SHALL increase accordingly + +### Requirement: Mark-read SHALL include the notification id and be idempotent +The system SHALL send the notification `id` in the mark-read request body and SHALL treat success responses idempotently. + +#### Scenario: Mark-read an already-read notification +- **WHEN** the notification is already read or owned by another customer +- **THEN** the request SHALL still succeed + +### Requirement: read-all SHALL NOT close popups implicitly in the popup chain +The system SHALL NOT call `read-all` from any popup display path because it also marks today's popup notifications read. + +#### Scenario: Popup display flow +- **WHEN** the popup candidate flow runs +- **THEN** the system SHALL NOT call `read-all` diff --git a/openspec/changes/add-h5-popup-candidate-flow/tasks.md b/openspec/changes/add-h5-popup-candidate-flow/tasks.md new file mode 100644 index 0000000..f31403c --- /dev/null +++ b/openspec/changes/add-h5-popup-candidate-flow/tasks.md @@ -0,0 +1,36 @@ +## 1. API Layer + +- [ ] 1.1 Add `popupApi.getCandidate(page, identifier)` for `GET /api/c/v1/popup-candidates` +- [ ] 1.2 Add `popupApi.submitRiskExchangeAddress(assetId, params)` for `POST /api/c/v1/risk-exchanges/{asset_id}/address` +- [ ] 1.3 Include the notification `id` in the mark-read request body (R3) + +## 2. PopupCandidate Component + +- [ ] 2.1 Query the candidate once per mount and emit `empty` when none exists +- [ ] 2.2 Render title/body and hide silently on resource-invisible responses +- [ ] 2.3 Map `action_type` through the whitelist and navigate from the mapped page +- [ ] 2.4 Mark the notification read on dismiss and on action (R3) +- [ ] 2.5 Deduplicate by `notification_id` within a session so same-day repeats do not re-popup +- [ ] 2.6 Provide the risk-exchange address form, submission, and result view + +## 3. Page Wiring + +- [ ] 3.1 Wire `home` in `pages/index/index.vue` with priority over the legacy unread-notification popup +- [ ] 3.2 Wire `package_purchase` in `pages/package-order/package-order.vue` +- [ ] 3.3 Wire `asset_wallet_recharge` in `pages/my-wallet/my-wallet.vue` +- [ ] 3.4 Leave `asset_detail` unwired + +## 4. Notification Contract Alignment + +- [ ] 4.1 Remove client-side priority re-sort and rely on the backend ordering +- [ ] 4.2 Align the category label map with `approval/expiry/sync/system` +- [ ] 4.3 Keep the unread badge consistent with popup counting and `99+` display +- [ ] 4.4 Keep `read-all` out of the popup display chain + +## 5. Regression Verification + +- [ ] 5.1 Operation popup with action navigates to the mapped page and marks read +- [ ] 5.2 Risk-exchange popup submits the address and shows the exchange number +- [ ] 5.3 Repeat entry with the same `notification_id` does not re-popup +- [ ] 5.4 Invisible asset shows no popup and no error branch +- [ ] 5.5 Notification center renders new `h5.popup.*` types without layout breakage diff --git a/openspec/changes/update-phone-bind-association-flow/proposal.md b/openspec/changes/update-phone-bind-association-flow/proposal.md new file mode 100644 index 0000000..85c4633 --- /dev/null +++ b/openspec/changes/update-phone-bind-association-flow/proposal.md @@ -0,0 +1,37 @@ +## Why + +The August 2026 customer API document (`docs/产品迭代8月份/11.md`) freezes the customer-side authentication contracts used by H5. The behavioral contract changes are: + +- `need_bind_phone` widens to a three-state decision: no main phone, or main phone exists but the current asset is unassociated -> `true`; already associated -> `false`; global switch off -> always `false`. The frontend still uses it only to decide whether to guide the user into phone verification. +- `POST /api/c/v1/auth/bind-phone` becomes idempotent: when the account already has a main phone, submitting the same main phone plus a valid code creates the association instead of rejecting it. +- `POST /api/c/v1/auth/change-phone` migrates all valid phone-asset associations in one transaction and rolls back the whole operation on the 10-asset limit or an existing same-asset relation. + +The current H5 bind page starts with an empty phone input and has no prefill. Existing users who have a main phone but whose current asset is unassociated are guided to verify, yet every submitted number is rejected - a deadlock. Homepage-gate and login routing must also follow the widened `need_bind_phone` semantics instead of raw bound-phone presence. + +## What Changes + +- Bind page prefills the phone input with the account main phone from `GET /api/c/v1/asset/info` -> `bound_phone` and allows sending a `bind_phone` verification code to that number +- Bind submission treats `bind-phone` as idempotent for the account main phone; other numbers show the backend `msg` and keep the user on the page +- Keep the existing bind-completion flow: ordinary entry returns back, mandatory login-gate entry triggers the manual re-login flow +- `change-phone` keeps the old/new phone and verification-code inputs with the `change_phone_old` / `change_phone_new` send-code scenes; failures show backend rollback copy (10-item limit and same-asset conflict) and keep the user on the page +- Login and homepage entry keep `need_bind_phone` as the only phone-verification guide signal and follow the three-state semantic +- `send-code` request/response stays unchanged (`cooldown_seconds`); the new `verify-asset` fields `miniapp_app_id` / `oa_app_id` are a backend confirm item, not consumed yet + +## Capabilities + +### New Capabilities +- `phone-bind-association-prefill`: Define bind-page prefill of the account main phone and idempotent bind submission behavior +- `phone-change-migration`: Define change-phone transaction-migration behavior and backend rollback copy display + +### Modified Capabilities +- `index-phone-bind-gate`: Widen the bind-phone gate to the three-state `need_bind_phone` semantic + +## Impact + +- Affected code: `pages/bind/bind.vue`, `pages/change-phone/change-phone.vue`, `pages/index/index.vue`, `pages/login/login.vue` +- API module: `api/modules/auth.js` unchanged - endpoints and response shapes already match `11.md` +- Backend contract deltas (per `11.md`): bind-phone idempotency, change-phone transactional migration, three-state `need_bind_phone` +- Backend decisions (confirmed 2026-09-15): + - `need_bind_phone` stays wechat-login-only; `asset/info` and `verify-asset` do not add the field. The homepage gate keeps evaluating the per-entry association state via `asset/info.bound_phone`. + - No new prefill field: bind-page prefill keeps using `asset/info.bound_phone`. + - `verify-asset`'s new `miniapp_app_id` / `oa_app_id` are not consumed; H5 login keeps using `/api/c/v1/wechat/appid`, and `/api/c/v1/auth/wechat-login` remains the login authority. diff --git a/openspec/changes/update-phone-bind-association-flow/specs/index-phone-bind-gate/spec.md b/openspec/changes/update-phone-bind-association-flow/specs/index-phone-bind-gate/spec.md new file mode 100644 index 0000000..3ffb729 --- /dev/null +++ b/openspec/changes/update-phone-bind-association-flow/specs/index-phone-bind-gate/spec.md @@ -0,0 +1,23 @@ +## MODIFIED Requirements + +### Requirement: Index entry SHALL enforce the bind-phone gate from the asset association state +The system SHALL determine whether phone-verification guidance is required whenever the homepage is entered, using the current asset association state returned by `GET /api/c/v1/asset/info` -> `bound_phone` (empty means the asset is not associated and binding is required). The widened `need_bind_phone` decision stays wechat-login-only: no main phone -> required; main phone exists but the current asset is unassociated -> required; already associated -> not required; global switch off -> never required. The frontend still uses the signal only to decide whether to guide the user, and the backend does not block any business interface on the missing relation. + +#### Scenario: User has no main phone +- **WHEN** the account has no main phone and the user enters `pages/index/index` +- **THEN** the system SHALL treat phone verification as required +- **AND** the system SHALL route the user into the bind-phone guide + +#### Scenario: User has a main phone but the current asset is unassociated +- **WHEN** the account has a main phone but the current asset is not associated with it +- **THEN** the system SHALL treat phone verification as required +- **AND** the bind page SHALL prefill the account main phone from `asset/info.bound_phone` + +#### Scenario: User is already associated +- **WHEN** the current asset is already associated with the account main phone +- **THEN** the system SHALL keep the user on the homepage +- **AND** the system SHALL NOT route the user into the bind-phone guide + +#### Scenario: Global switch off +- **WHEN** the global phone-bind switch is off +- **THEN** the system SHALL never route the user into the bind-phone guide regardless of account state diff --git a/openspec/changes/update-phone-bind-association-flow/specs/phone-bind-association-prefill/spec.md b/openspec/changes/update-phone-bind-association-flow/specs/phone-bind-association-prefill/spec.md new file mode 100644 index 0000000..b74a61a --- /dev/null +++ b/openspec/changes/update-phone-bind-association-flow/specs/phone-bind-association-prefill/spec.md @@ -0,0 +1,36 @@ +## ADDED Requirements + +### Requirement: Bind page SHALL prefill the account main phone +The system SHALL prefill the bind-phone input with the logged-in account's main phone when one exists, using `GET /api/c/v1/asset/info` -> `bound_phone`, and SHALL allow sending a `bind_phone` verification code to that number. + +#### Scenario: Existing account with a main phone enters the bind page +- **WHEN** the account already has a main phone and the user enters `pages/bind/bind` +- **THEN** the phone input SHALL be prefilled with the main phone +- **AND** the user SHALL be able to request a `bind_phone` verification code for that number without being rejected + +#### Scenario: New account without a main phone enters the bind page +- **WHEN** the account has no main phone +- **THEN** the phone input SHALL remain empty +- **AND** the user SHALL enter a new number and follow the normal bind flow + +### Requirement: Bind submission SHALL be idempotent for the account main phone +When `POST /api/c/v1/auth/bind-phone` is submitted with the account main phone and a valid code, the system SHALL treat it as a successful idempotent association instead of rejecting because the phone is already in use. + +#### Scenario: Main phone plus valid code +- **WHEN** the submitted phone equals the account main phone and the code is valid +- **THEN** the bind request SHALL succeed +- **AND** the response SHALL keep the existing shape (`phone`, `bound_at`) +- **AND** the existing bind-completion routing SHALL apply unchanged + +#### Scenario: Number different from the main phone +- **WHEN** the submitted phone differs from the account main phone +- **THEN** the bind request SHALL be rejected +- **AND** the system SHALL display the backend `msg` and keep the user on the bind page + +### Requirement: Bind failures SHALL display backend messages +The system SHALL present the backend-provided error message for all bind-phone failures instead of a generic failure text, and SHALL keep the user on the bind page. + +#### Scenario: Backend rejection with message +- **WHEN** `bind-phone` returns a business error +- **THEN** the bind page SHALL show the returned `msg` +- **AND** the page SHALL NOT navigate away diff --git a/openspec/changes/update-phone-bind-association-flow/specs/phone-change-migration/spec.md b/openspec/changes/update-phone-bind-association-flow/specs/phone-change-migration/spec.md new file mode 100644 index 0000000..8d52203 --- /dev/null +++ b/openspec/changes/update-phone-bind-association-flow/specs/phone-change-migration/spec.md @@ -0,0 +1,29 @@ +## ADDED Requirements + +### Requirement: Change-phone SHALL follow transactional full migration +The system SHALL rely on `POST /api/c/v1/auth/change-phone` to migrate all valid phone-asset associations in one transaction, and SHALL treat any failure as a rollback of the whole operation. + +#### Scenario: Successful migration +- **WHEN** the old-phone code and the new-phone code are valid and no limit or conflict occurs +- **THEN** the change request SHALL succeed +- **AND** the response SHALL keep the existing shape (`phone`, `changed_at`) +- **AND** the user SHALL return to the previous page and the homepage SHALL refresh the bound state + +#### Scenario: New number exceeds the association limit +- **WHEN** the new number would exceed 10 valid associated assets +- **THEN** the whole operation SHALL be rolled back +- **AND** the system SHALL display the backend copy: 该手机号最多关联10项有效资产 + +#### Scenario: New number conflicts with an existing same-asset relation +- **WHEN** the new number already has a valid association with an asset being migrated +- **THEN** the whole operation SHALL be rolled back +- **AND** the system SHALL display the backend copy: 新手机号已存在与待迁移资产相同的有效关联,换绑已回滚 + +### Requirement: Change-phone failures SHALL keep the user on the page +The system SHALL stay on the change-phone page and show the backend error message when migration fails so the user can correct the input and retry. + +#### Scenario: Migration failure with message +- **WHEN** `change-phone` returns a business error +- **THEN** the page SHALL show the returned `msg` +- **AND** the page SHALL NOT navigate away + diff --git a/openspec/changes/update-phone-bind-association-flow/tasks.md b/openspec/changes/update-phone-bind-association-flow/tasks.md new file mode 100644 index 0000000..ebd30f3 --- /dev/null +++ b/openspec/changes/update-phone-bind-association-flow/tasks.md @@ -0,0 +1,29 @@ +## 1. Bind Page Prefill and Idempotent Bind + +- [x] 1.1 Load the account main phone on bind-page mount via `assetApi.getInfo` and prefill the phone input when present +- [x] 1.2 Allow sending a `bind_phone` verification code to the prefilled main phone +- [x] 1.3 Keep ordinary bind success returning to the previous page +- [x] 1.4 Keep mandatory login-gate bind success returning to the login page for manual re-login +- [x] 1.5 Show backend `msg` on bind rejection and stay on the page + +## 2. Change-Phone Transactional Migration Display + +- [x] 2.1 Keep old/new phone and verification-code inputs with `change_phone_old` / `change_phone_new` scenes +- [x] 2.2 Show backend rollback copy for the 10-item limit and the same-asset conflict +- [x] 2.3 Stay on the change-phone page on failure +- [x] 2.4 Refresh the homepage bound state after success (existing `onShow` reload) + +## 3. Three-State Need-Bind-Phone Routing + +- [x] 3.1 Keep `need_bind_phone` as the only phone-verification guide signal in login success +- [x] 3.2 Evaluate the three-state semantic in the homepage bind gate +- [x] 3.3 Re-check the binding state on every homepage entry, not only after login +- [x] 3.4 Confirm the homepage gate signal source with backend - decision: keep `asset/info.bound_phone`; `need_bind_phone` stays wechat-login-only + +## 4. Regression Verification + +- [ ] 4.1 New user without a main phone still binds a fresh number +- [ ] 4.2 Existing user with a main phone and an unassociated asset can prefill, send code, and bind idempotently +- [ ] 4.3 Bind with a number different from the main phone shows the backend rejection message +- [ ] 4.4 Change-phone limit/conflict errors show the fixed backend copy +- [ ] 4.5 Global-switch-off keeps the bind guide hidden diff --git a/pages/bind/bind.vue b/pages/bind/bind.vue index 8fa89d1..09f2294 100644 --- a/pages/bind/bind.vue +++ b/pages/bind/bind.vue @@ -40,7 +40,7 @@ diff --git a/pages/my-wallet/my-wallet.vue b/pages/my-wallet/my-wallet.vue index f630cfc..e343070 100644 --- a/pages/my-wallet/my-wallet.vue +++ b/pages/my-wallet/my-wallet.vue @@ -221,6 +221,10 @@ + + + diff --git a/pages/notifications/notifications.vue b/pages/notifications/notifications.vue index 1f03753..10078b8 100644 --- a/pages/notifications/notifications.vue +++ b/pages/notifications/notifications.vue @@ -61,7 +61,6 @@ const getCategoryText = (category) => ({ approval: '审批', expiry: '临期', - exchange: '换货', sync: '同步', system: '系统' }[category] || '通知'); @@ -73,17 +72,6 @@ critical: '严重' }[severity] || '提示'); - const getNotificationPriority = (item) => { - const severityRank = { info: 10, warning: 20, error: 30, critical: 40 }; - const remainingDays = Number(item?.days_until_expiry ?? item?.days_remaining ?? item?.remaining_days); - const expiryLevel = String(item?.expiry_level || '').toLowerCase(); - if (item?.category === 'expiry' && ((Number.isFinite(remainingDays) && remainingDays >= 0 && remainingDays <= 3) || - ['0_3', '0-3', '0~3', 'critical'].includes(expiryLevel))) { - return 100; - } - return severityRank[item?.severity] || 0; - }; - const loadUnreadCount = async () => { try { const data = await notificationApi.getUnreadCount(); @@ -98,7 +86,7 @@ loading.value = true; try { const data = await notificationApi.getList(page.value, pageSize); - const items = (data?.items || []).sort((a, b) => getNotificationPriority(b) - getNotificationPriority(a)); + const items = (data?.items || []); if (append) { notifications.push(...items); } else { diff --git a/pages/package-order/package-order.vue b/pages/package-order/package-order.vue index 167e96e..ace4a0f 100644 --- a/pages/package-order/package-order.vue +++ b/pages/package-order/package-order.vue @@ -95,6 +95,10 @@ + + +