vending-machine/.kiro/specs/mobile-app/design.md
2026-04-08 20:45:41 +08:00

17 KiB
Raw Blame History

设计文档贩卖机移动端APP

概述

本设计文档描述贩卖机移动端APP的技术架构和实现方案。APP基于UniApp + Vue3Composition API技术栈开发使用Pinia进行状态管理vue-i18n实现多语言国际化uQRCode生成二维码。APP通过封装的HTTP模块与后端REST API通信使用JWT Bearer Token进行认证。支付集成通过UniApp原生插件调用Google Pay和Apple Pay。

架构

整体架构

graph TB
    subgraph 移动端APP
        Pages[页面层<br/>UniApp Pages]
        Components[组件层<br/>可复用组件]
        Stores[状态管理层<br/>Pinia Stores]
        API[API层<br/>请求封装]
        Utils[工具层<br/>i18n/支付/二维码]
    end

    subgraph 外部服务
        Backend[后端API<br/>.NET REST API]
        GooglePay[Google Pay]
        ApplePay[Apple Pay]
    end

    Pages --> Components
    Pages --> Stores
    Stores --> API
    API --> Backend
    Utils --> GooglePay
    Utils --> ApplePay

目录结构

mobile/
├── pages/                    # 页面
│   ├── index/                # 首页
│   ├── login/                # 登录页
│   ├── membership/           # 会员页
│   ├── stamps/               # 节日印花页
│   ├── mine/                 # 我的页
│   ├── points/               # 我的积分页
│   ├── coupons/              # 我的优惠券页
│   ├── agreement/            # 用户协议页
│   ├── privacy/              # 隐私政策页
│   └── about/                # 关于页
├── components/               # 可复用组件
│   ├── QrcodePopup.vue       # 会员二维码弹窗
│   ├── CouponCard.vue        # 优惠券卡片
│   ├── RedeemPopup.vue       # 兑换确认弹窗
│   ├── GiftPointsPopup.vue   # 赠送积分弹窗
│   ├── CouponGuidePopup.vue  # 使用说明弹窗
│   └── LanguagePicker.vue    # 语言选择器
├── stores/                   # Pinia状态管理
│   ├── user.js               # 用户状态
│   ├── membership.js         # 会员状态
│   └── app.js                # 应用全局状态(语言等)
├── api/                      # API请求模块
│   ├── request.js            # 请求封装(拦截器)
│   ├── user.js               # 用户相关API
│   ├── membership.js         # 会员相关API
│   ├── coupon.js             # 优惠券相关API
│   ├── points.js             # 积分相关API
│   └── content.js            # 内容相关API
├── i18n/                     # 多语言
│   ├── index.js              # i18n配置
│   ├── zh-CN.js              # 简体中文
│   ├── zh-TW.js              # 繁体中文
│   └── en.js                 # 英文
├── utils/                    # 工具函数
│   ├── payment.js            # 支付封装
│   ├── qrcode.js             # 二维码生成
│   ├── storage.js            # 本地存储封装
│   └── auth.js               # 认证工具
├── static/                   # 静态资源
├── App.vue
├── main.js
├── pages.json
├── manifest.json
└── uni.scss

组件与接口

API请求模块 (api/request.js)

统一封装HTTP请求自动处理Token注入、语言标识和错误拦截。

// 请求拦截器
// - 自动从本地存储读取JWT Token添加到Authorization请求头
// - 自动从Pinia store读取当前语言添加到Accept-Language请求头
// - 设置Content-Type为application/json

// 响应拦截器
// - 检查HTTP状态码401时清除Token并跳转登录页
// - 解析响应体根据success字段判断业务成功/失败
// - 网络错误时展示友好提示

接口签名:

/**
 * 发起GET请求
 * @param {string} url - 请求路径
 * @param {object} params - 查询参数
 * @returns {Promise<{success: boolean, data: any, message: string}>}
 */
function get(url, params)

/**
 * 发起POST请求
 * @param {string} url - 请求路径
 * @param {object} data - 请求体
 * @returns {Promise<{success: boolean, data: any, message: string}>}
 */
function post(url, data)

/**
 * 发起DELETE请求
 * @param {string} url - 请求路径
 * @returns {Promise<{success: boolean, data: any, message: string}>}
 */
function del(url)

用户状态管理 (stores/user.js)

// Pinia Store: useUserStore
// state:
//   token: string | null       - JWT Token
//   userInfo: object | null    - 用户信息 { uid, nickname, isMember, ... }
//   isLoggedIn: boolean        - 计算属性token !== null
//
// actions:
//   login(phone, code, areaCode) - 调用登录API存储token
//   logout()                      - 调用登出API清除状态
//   fetchUserInfo()               - 获取用户信息
//   deleteAccount()               - 注销账号
//   clearAuth()                   - 清除本地认证信息

会员状态管理 (stores/membership.js)

// Pinia Store: useMembershipStore
// state:
//   membershipInfo: object | null  - 会员信息
//   products: array                - 会员商品列表
//   subscriptionStatus: object     - 订阅状态
//
// actions:
//   fetchMembershipInfo()    - 获取会员信息
//   fetchProducts()          - 获取商品列表
//   purchase(productId, receipt) - 购买单月会员
//   subscribe(productId, receipt) - 订阅会员
//   fetchSubscriptionStatus() - 获取订阅状态

应用状态管理 (stores/app.js)

// Pinia Store: useAppStore
// state:
//   locale: string  - 当前语言 ('zh-CN' | 'zh-TW' | 'en')
//
// actions:
//   setLocale(locale)   - 切换语言,持久化到本地存储
//   initLocale()        - 初始化语言(从本地存储或系统语言)

支付封装 (utils/payment.js)

/**
 * 统一支付接口内部根据平台选择Google Pay或Apple Pay
 * @param {object} params
 * @param {string} params.productId - 商品ID
 * @param {number} params.price - 价格
 * @param {string} params.type - 'purchase' | 'subscribe'
 * @returns {Promise<{success: boolean, receipt: string}>}
 */
function pay(params)

/**
 * 检查支付环境是否可用
 * @returns {Promise<boolean>}
 */
function isPaymentAvailable()

二维码生成 (utils/qrcode.js)

/**
 * 生成会员二维码
 * @param {string} token - 用户token
 * @returns {string} base64编码的二维码图片
 */
function generateQRCode(token)

/**
 * 创建带有效期管理的二维码实例
 * @param {Function} getToken - 获取新token的异步函数
 * @param {number} ttl - 有效期毫秒默认3000005分钟
 * @returns {{ qrcodeData: Ref<string>, remainingTime: Ref<number>, refresh: Function, destroy: Function }}
 */
function useQRCode(getToken, ttl)

页面组件

首页 (pages/index/index.vue)

  • 加载Banner轮播图swiper组件
  • 展示功能入口图片网格
  • 展示可兑换优惠券列表
  • 包含会员二维码弹窗、使用说明弹窗、兑换确认弹窗

登录页 (pages/login/login.vue)

  • 手机区号选择器 + 手机号输入框
  • 验证码输入框 + 发送验证码按钮含60秒倒计时
  • 协议勾选复选框 + 协议链接
  • 登录按钮

会员页 (pages/membership/membership.vue)

  • 会员宣传长图展示
  • 会员商品信息展示
  • 开通会员/订阅会员按钮(根据状态动态显示)
  • 支付流程集成

节日印花页 (pages/stamps/stamps.vue)

  • 印花Banner图展示
  • 印花优惠券列表
  • 兑换按钮(根据会员状态和兑换状态动态显示)

我的页 (pages/mine/mine.vue)

  • 用户信息区域头像、昵称、UID
  • 积分展示区域
  • 功能菜单列表(优惠券、语言、协议、关于等)
  • 赠送积分弹窗、语言选择弹窗、退出确认弹窗

我的积分页 (pages/points/points.vue)

  • Tab切换获取记录/使用记录)
  • 积分记录列表(支持分页加载)

我的优惠券页 (pages/coupons/coupons.vue)

  • Tab切换可使用/已使用/已过期)
  • 优惠券卡片列表(含状态标识)

数据模型

用户信息

{
  uid: string,           // 用户唯一标识
  nickname: string,      // 昵称,格式:"用户" + 6位随机数字
  phone: string,         // 手机号(脱敏)
  isMember: boolean,     // 是否为会员
  memberExpireAt: string  // 会员到期时间ISO 8601
}

会员商品

{
  productId: string,     // 商品ID
  name: string,          // 商品名称
  price: number,         // 价格
  duration: number,      // 生效时长(天)
  type: string           // 'monthly' | 'subscription'
}

优惠券

{
  couponId: string,        // 优惠券ID
  name: string,            // 优惠券名称
  type: string,            // 'thresholddiscount' | 'directdiscount'
  thresholdAmount: number, // 满减门槛满减券有值抵扣券为null
  discountAmount: number,  // 抵扣金额
  requiredPoints: number,  // 兑换所需积分
  expireAt: string,        // 到期时间
  status: string           // 'available' | 'used' | 'expired'
}

印花优惠券

{
  stampCouponId: string,   // 印花优惠券ID
  name: string,            // 名称
  requiredPoints: number,  // 兑换所需积分可为0
  expireAt: string,        // 到期时间
  redeemed: boolean        // 当前用户是否已兑换
}

积分记录

{
  id: string,              // 记录ID
  type: string,            // 'earn' | 'spend'
  source: string,          // 来源/使用方式描述
  amount: number,          // 积分数量
  createdAt: string        // 时间
}

Banner

{
  id: string,              // Banner ID
  imageUrl: string,        // 图片URL
  linkType: string,        // 'internal' | 'external'
  linkUrl: string          // 跳转链接
}

入口图片

{
  id: string,              // 入口ID
  key: string,             // 入口标识:'membership' | 'stamps' | 'qrcode' | 'guide'
  imageUrl: string         // 图片URL
}

正确性属性

属性是系统在所有有效执行中应保持为真的特征或行为——本质上是关于系统应该做什么的形式化陈述。属性是人类可读规范与机器可验证正确性保证之间的桥梁。

Property 1: 语言切换翻译正确性

对于任意支持的语言locale和任意翻译键key切换到该locale后翻译函数t(key)应返回该语言对应的非空文本,且不等于其他语言的翻译文本(除非原文相同)。

Validates: Requirements 1.2

Property 2: 语言设置持久化Round-Trip

对于任意支持的语言locale设置语言后持久化到本地存储再从本地存储读取并初始化最终的locale应等于最初设置的值。

Validates: Requirements 1.4, 1.5

Property 3: 请求拦截器请求头注入

对于任意已存储的JWT Token和任意当前语言设置通过请求模块发出的每个HTTP请求其请求头中Authorization字段应为"Bearer {token}"Accept-Language字段应为当前语言标识。

Validates: Requirements 1.3, 2.6, 11.1

Property 4: 未勾选协议阻止登录

对于任意手机号和验证码输入当协议勾选状态为false时调用登录操作应被阻止且不发出API请求。

Validates: Requirements 2.3

Property 5: 登录Token存储

对于任意成功的登录API响应包含token字段登录操作完成后本地存储中应包含该token值且用户状态应为已登录。

Validates: Requirements 2.5

Property 6: 401响应自动清除认证

对于任意API请求当响应状态码为401时本地存储中的Token应被清除用户状态应重置为未登录。

Validates: Requirements 2.8, 11.3

Property 7: Banner导航正确性

对于任意Banner配置项当linkType为"internal"时点击应调用内部页面导航当linkType为"external"时点击应调用外部链接打开且目标URL应等于配置的linkUrl。

Validates: Requirements 3.2

Property 8: 二维码生成Round-Trip

对于任意有效的token字符串生成二维码后解码应得到与原始token相同的字符串。

Validates: Requirements 4.1

Property 9: 会员按钮状态正确性

对于任意会员状态组合(非会员/单月会员/订阅会员),会员页按钮的显示状态应满足:非会员时显示"开通会员"可点击按钮;单月会员时隐藏单月按钮、显示"订阅会员"可点击按钮;订阅会员时显示"已购买订阅会员"灰色不可点击按钮。

Validates: Requirements 5.5, 5.8, 5.9

Property 10: 印花优惠券兑换状态显示

对于任意印花优惠券列表redeemed为true的优惠券应显示"已兑换"灰色不可点击按钮redeemed为false的应显示可点击的"兑换"按钮。

Validates: Requirements 6.6

Property 11: 分页加载参数正确性

对于任意初始页码和连续N次加载更多操作第i次请求的page参数应等于初始页码+i。

Validates: Requirements 8.4

Property 12: 优惠券Tab状态参数正确性

对于任意Tab切换操作可使用/已使用/已过期API请求中的status参数应分别对应"available"、"used"、"expired"。

Validates: Requirements 9.2

Property 13: 优惠券状态标识显示正确性

对于任意优惠券列表status为"used"的优惠券应显示"已使用"标识status为"expired"的优惠券应显示"已过期"标识status为"available"的优惠券不显示状态标识。

Validates: Requirements 9.3, 9.4

Property 14: API响应统一处理

对于任意API响应体当success为true时应返回data数据当success为false时应抛出或返回包含message的错误信息。

Validates: Requirements 11.2

Property 15: 网络错误友好提示

对于任意网络错误(超时、断网、服务器错误),请求模块应捕获错误并调用提示函数展示错误信息,而非抛出未处理异常。

Validates: Requirements 11.5

Property 16: 支付渠道平台选择

对于任意支付请求当平台为Android时应调用Google Pay接口当平台为iOS时应调用Apple Pay接口。

Validates: Requirements 12.2, 12.3

错误处理

API错误处理

错误场景 处理方式
401 未授权 清除本地Token跳转登录页
网络超时 展示"网络连接超时,请重试"提示
网络断开 展示"网络连接失败,请检查网络设置"提示
500 服务器错误 展示"服务器繁忙,请稍后重试"提示
业务错误success=false 展示后端返回的message信息

支付错误处理

错误场景 处理方式
支付取消 关闭支付弹窗,不做额外处理
支付失败 展示"支付失败,请重试"提示
支付环境不可用 展示"当前设备不支持该支付方式"提示
支付成功但后端确认失败 展示"支付已完成,会员状态稍后更新"提示

业务错误处理

错误场景 处理方式
积分不足兑换优惠券 弹出"积分不足,无法兑换"提示
优惠券已下架 弹出"优惠券已下架"提示
积分不足赠送 弹出"剩余积分不足,无法赠送"提示
未勾选协议登录 弹出"请阅读并同意协议"提示
验证码发送频率限制 展示倒计时,按钮不可点击

测试策略

测试框架

  • 单元测试Vitest
  • 属性测试fast-check配合Vitest
  • 组件测试:@vue/test-utils

单元测试

单元测试用于验证具体示例、边界情况和错误条件:

  • API模块验证各接口调用参数和URL正确性
  • Store模块验证状态变更逻辑登录/登出/语言切换)
  • 工具函数:验证支付渠道选择、存储读写
  • 组件:验证条件渲染(登录/未登录状态、会员/非会员状态)

属性测试

属性测试用于验证跨所有输入的通用属性每个属性测试至少运行100次迭代

  • 每个属性测试必须引用设计文档中的属性编号
  • 标签格式:Feature: mobile-app, Property {number}: {property_text}
  • 使用fast-check库生成随机测试数据
  • 每个正确性属性由一个独立的属性测试实现

测试覆盖重点

模块 单元测试 属性测试
请求拦截器 基本请求示例 Property 3, 6, 14, 15
多语言模块 语言文件完整性 Property 1, 2
用户认证 登录/登出流程 Property 4, 5
首页 Banner渲染、入口导航 Property 7
二维码 生成和显示 Property 8
会员页 按钮状态 Property 9
印花页 兑换状态 Property 10
积分页 Tab切换 Property 11
优惠券页 Tab切换、状态标识 Property 12, 13
支付模块 支付流程 Property 16