Administrator
发布于 2026-09-01 / 55 阅读
0
0

鸿蒙开发系列(第16篇):支付与账号集成——华为账号、应用内支付接入指南

鸿蒙开发系列 · 高级篇

前言

地图功能跑通了,但一个完整的商业应用还缺两块关键拼图:用户是谁(账号体系)和怎么收钱(支付能力)。自己搭一套账号系统?意味着要处理注册、登录、密码找回、风控、合规等一系列问题。自己接支付?要面对渠道费率、对账、退款、税务的复杂流程。

HarmonyOS提供了**华为账号服务(Account Kit)和应用内支付(IAP Kit)**作为官方解决方案。华为账号覆盖全球数亿用户,一键授权即可获取用户身份;IAP Kit提供了标准化的商品管理、订单创建、支付收银台和票据校验流程。

本文将带你完成两条主链路:华为账号登录授权,以及应用内商品的配置与支付接入。


一、华为账号服务:Account Kit

1.1 华为账号的优势

维度

自建账号

华为账号

开发成本

高(注册、登录、找回、安全)

低(几行代码接入)

用户体验

需填写手机号/密码

一键授权,无需输入

安全性

依赖自身能力

华为级安全体系

合规成本

需自行处理隐私合规

华为统一处理授权协议

跨设备

需自行同步

华为账号天然跨设备

用户覆盖

从零积累

数亿华为设备用户

对于HarmonyOS应用,华为账号几乎是标配选择。用户已经登录了系统账号,你的应用只需请求授权即可。

1.2 授权模式选择

Account Kit提供两种授权模式:

模式

获取信息

适用场景

静默登录(Silent Sign-In)

OpenID、UnionID

无需用户交互的自动登录

授权码模式(Authorization Code)

头像、昵称、手机号等

需要用户同意的场景

静默登录适合应用启动时的自动识别;授权码模式适合首次使用或需要更多用户信息的场景。

1.3 客户端接入实战

步骤1:配置权限与模块

// module.json5
{
  "module": {
    "requestPermissions": [
      {
        "name": "ohos.permission.GET_NETWORK_INFO"
      }
    ]
  }
}

步骤2:华为账号管理器封装

// entry/src/main/ets/utils/HuaweiAccountManager.ets
import { authentication } from '@kit.AccountKit';
import { promptAction } from '@kit.ArkUI';

export interface UserProfile {
  unionId: string;
  openId: string;
  displayName: string;
  avatarUri: string;
  idToken?: string;
  authorizationCode?: string;
}

export class HuaweiAccountManager {
  private auth: authentication.HuaweiIDProvider | null = null;

  constructor() {
    this.auth = new authentication.HuaweiIDProvider();
  }

  // 静默登录(应用启动时调用)
  async silentSignIn(): Promise<UserProfile | null> {
    try {
      const controller = new authentication.HuaweiIDProvider().getAuthorizationWithHuaweiIDController();
      const request = new authentication.HuaweiIDProvider().createAuthorizationWithHuaweiIDRequest();

      request.scopes = ['openid', 'profile'];
      request.permissions = [];
      request.state = this.generateState();

      const response = await controller.executeRequest(request);

      if (response.data) {
        return this.parseUserProfile(response.data);
      }
      return null;
    } catch (err) {
      console.info('静默登录失败,需要显式授权');
      return null;
    }
  }

  // 显式授权登录(弹窗让用户确认)
  async explicitSignIn(): Promise<UserProfile | null> {
    try {
      const controller = new authentication.HuaweiIDProvider().getAuthorizationWithHuaweiIDController();
      const request = new authentication.HuaweiIDProvider().createAuthorizationWithHuaweiIDRequest();

      // 请求的授权范围
      request.scopes = ['openid', 'profile', 'phone'];
      request.permissions = ['idtoken'];
      request.forceAuthorization = true; // 强制弹窗
      request.state = this.generateState();

      const response = await controller.executeRequest(request);

      if (response.data) {
        const profile = this.parseUserProfile(response.data);
        promptAction.showToast({ message: `欢迎,${profile.displayName}` });
        return profile;
      }
      return null;
    } catch (err) {
      console.error('登录失败:', err);
      promptAction.showToast({ message: '登录取消或失败' });
      return null;
    }
  }

  // 登出
  async signOut(): Promise<void> {
    try {
      const controller = new authentication.HuaweiIDProvider().getAuthorizationWithHuaweiIDController();
      const request = new authentication.HuaweiIDProvider().createCancelAuthorizationRequest();
      await controller.executeRequest(request);
      console.info('登出成功');
    } catch (err) {
      console.error('登出失败:', err);
    }
  }

  // 解析用户信息
  private parseUserProfile(data: authentication.AuthorizationWithHuaweiIDResponse): UserProfile {
    return {
      unionId: data.unionID ?? '',
      openId: data.openID ?? '',
      displayName: data.displayName ?? '华为用户',
      avatarUri: data.avatarUri ?? '',
      idToken: data.IDToken ?? '',
      authorizationCode: data.authorizationCode ?? ''
    };
  }

  // 生成防CSRF随机串
  private generateState(): string {
    const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
    let result = '';
    for (let i = 0; i < 16; i++) {
      result += chars.charAt(Math.floor(Math.random() * chars.length));
    }
    return result;
  }
}

步骤3:登录页面

// entry/src/main/ets/pages/LoginPage.ets
import { HuaweiAccountManager, UserProfile } from '../utils/HuaweiAccountManager';
import { router } from '@kit.ArkUI';

@Entry
@Component
struct LoginPage {
  private accountManager: HuaweiAccountManager = new HuaweiAccountManager();
  @State isLoading: boolean = true;
  @State user: UserProfile | null = null;

  aboutToAppear() {
    this.trySilentLogin();
  }

  private async trySilentLogin() {
    const profile = await this.accountManager.silentSignIn();
    if (profile) {
      this.user = profile;
      // 自动跳转到首页
      setTimeout(() => {
        router.replaceUrl({ url: 'pages/Index' });
      }, 500);
    } else {
      this.isLoading = false;
    }
  }

  private async handleLogin() {
    this.isLoading = true;
    const profile = await this.accountManager.explicitSignIn();
    if (profile) {
      this.user = profile;
      // 保存登录态
      AppStorage.setOrCreate('userProfile', profile);
      router.replaceUrl({ url: 'pages/Index' });
    }
    this.isLoading = false;
  }

  build() {
    Column({ space: 24 }) {
      if (this.isLoading) {
        LoadingProgress()
          .width(48)
          .height(48)
          .color('#2196F3')
      } else {
        Image($r('app.media.ic_app_logo'))
          .width(100)
          .height(100)
          .borderRadius(20)

        Text('语音备忘录Pro')
          .fontSize(24)
          .fontWeight(FontWeight.Bold)

        Text('登录后即可同步您的录音到云端')
          .fontSize(14)
          .fontColor('#888')

        Button() {
          Row({ space: 8 }) {
            Image($r('app.media.ic_huawei_logo'))
              .width(20)
              .height(20)
            Text('华为账号一键登录')
              .fontSize(16)
              .fontColor('#FFFFFF')
          }
        }
        .width(280)
        .height(48)
        .backgroundColor('#2196F3')
        .borderRadius(24)
        .onClick(() => this.handleLogin())

        Text('登录即表示同意《用户协议》和《隐私政策》')
          .fontSize(12)
          .fontColor('#AAA')
      }
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)
    .backgroundColor('#FAFAFA')
  }
}

1.4 IDToken 服务端校验

登录完成后,如果需要将用户身份同步到自有服务器,必须校验IDToken的真实性:

// 服务端(Node.js示例)
import jwt from 'jsonwebtoken';
import jwksClient from 'jwks-rsa';

const JWKS_URI = 'https://oauth-login.cloud.huawei.com/.well-known/jwks.json';

const client = jwksClient({
  jwksUri: JWKS_URI,
  cache: true,
  rateLimit: true
});

async function verifyHuaweiIDToken(idToken: string): Promise<jwt.JwtPayload> {
  const decoded = jwt.decode(idToken, { complete: true });
  if (!decoded) throw new Error('无效的IDToken');

  const key = await client.getSigningKey(decoded.header.kid);
  const publicKey = key.getPublicKey();

  return jwt.verify(idToken, publicKey, {
    algorithms: ['RS256'],
    issuer: 'https://accounts.huawei.com',
    audience: 'YOUR_CLIENT_ID' // 你的应用Client ID
  }) as jwt.JwtPayload;
}

// 使用示例
// const payload = await verifyHuaweiIDToken(idToken);
// payload.sub 即为用户的UnionID

二、应用内支付:IAP Kit

2.1 IAP Kit 核心概念

IAP(In-App Purchase)Kit是HarmonyOS提供的应用内支付解决方案,支持三种商品类型:

类型

说明

示例

消耗型

可重复购买,每次消耗后失效

游戏金币、会员时长

非消耗型

一次购买,永久有效

去广告、解锁高级功能

自动续期订阅

周期性扣费,自动续订

月度会员、年度VIP

2.2 商品配置(AppGallery Connect)

在写代码之前,必须先在AppGallery Connect后台配置商品:

  1. 登录 AppGallery Connect

  2. 进入你的项目 → 盈利 → 应用内支付

  3. 创建商品:

    • 商品ID(唯一标识,如 premium_monthly)

    • 商品类型(消耗型/非消耗型/订阅型)

    • 价格(支持多币种)

    • 名称和描述(多语言)

  4. 保存后等待审核(通常1-2个工作日)

2.3 客户端支付流程

┌─────────────┐    1.查询商品    ┌─────────────┐
│   你的应用   │ ───────────────→ │   IAP Kit   │
│             │ ←─────────────── │             │
└─────────────┘   返回商品信息    └─────────────┘
       │
       │ 2.用户点击购买
       ▼
┌─────────────┐    3.创建订单    ┌─────────────┐
│   你的应用   │ ───────────────→ │   IAP Kit   │
│             │ ←─────────────── │  弹出收银台  │
└─────────────┘   返回支付结果    └─────────────┘
       │
       │ 4.支付成功,获取PurchaseToken
       ▼
┌─────────────┐    5.校验票据    ┌─────────────┐
│   你的服务器  │ ───────────────→ │  华为服务器  │
│             │ ←─────────────── │             │
└─────────────┘   确认支付有效性   └─────────────┘
       │
       │ 6.发货(发放商品/权益)
       ▼
┌─────────────┐    7.确认发货    ┌─────────────┐
│   你的应用   │ ───────────────→ │   IAP Kit   │
│             │                 │  标记为已完成  │
└─────────────┘                 └─────────────┘

2.4 IAP 客户端封装

// entry/src/main/ets/utils/IapManager.ets
import { iap } from '@kit.IAPKit';
import { promptAction } from '@kit.ArkUI';

export interface Product {
  productId: string;
  productName: string;
  productDesc: string;
  price: string;
  currency: string;
  productType: iap.ProductType;
}

export class IapManager {
  // 查询可购买的商品
  async queryProducts(productIds: string[]): Promise<Product[]> {
    try {
      const request: iap.ProductIds = {
        productIds: productIds,
        productType: iap.ProductType.CONSUMABLE // 或 NONCONSUMABLE / SUBSCRIPTION
      };

      const result = await iap.queryProducts(getContext(), request);

      if (result.code === iap.IAPErrorCode.SUCCESS) {
        return result.data?.map(p => ({
          productId: p.productId,
          productName: p.productName,
          productDesc: p.productDesc,
          price: p.price,
          currency: p.currency,
          productType: p.productType
        })) ?? [];
      } else {
        throw new Error(`查询商品失败: ${result.code}`);
      }
    } catch (err) {
      console.error('查询商品异常:', err);
      throw err;
    }
  }

  // 购买商品
  async purchase(productId: string, productType: iap.ProductType): Promise<string> {
    try {
      const request: iap.PurchaseParams = {
        productId: productId,
        productType: productType,
        developerPayload: JSON.stringify({
          orderId: this.generateOrderId(),
          timestamp: Date.now()
        })
      };

      const result = await iap.purchase(getContext(), request);

      if (result.code === iap.IAPErrorCode.SUCCESS && result.data) {
        const purchaseData = result.data;
        console.info('购买成功:', purchaseData.purchaseToken);
        return purchaseData.purchaseToken;
      } else if (result.code === iap.IAPErrorCode.ORDER_PRODUCT_OWNED) {
        promptAction.showToast({ message: '您已拥有该商品' });
        throw new Error('商品已拥有');
      } else {
        throw new Error(`购买失败: ${result.code}`);
      }
    } catch (err) {
      console.error('购买异常:', err);
      throw err;
    }
  }

  // 查询已购商品(用于恢复购买)
  async queryPurchases(productType: iap.ProductType): Promise<iap.PurchaseData[]> {
    try {
      const result = await iap.queryPurchases(getContext(), productType);

      if (result.code === iap.IAPErrorCode.SUCCESS) {
        return result.data ?? [];
      }
      return [];
    } catch (err) {
      console.error('查询已购商品失败:', err);
      return [];
    }
  }

  // 消耗商品(消耗型商品必须调用,否则无法再次购买)
  async consumePurchase(purchaseToken: string): Promise<void> {
    try {
      const request: iap.ConsumeParams = {
        purchaseToken: purchaseToken
      };

      const result = await iap.consumePurchase(getContext(), request);

      if (result.code === iap.IAPErrorCode.SUCCESS) {
        console.info('商品消耗成功');
      } else {
        throw new Error(`消耗失败: ${result.code}`);
      }
    } catch (err) {
      console.error('消耗商品异常:', err);
      throw err;
    }
  }

  // 确认订阅(订阅型商品需确认)
  async confirmSubscription(purchaseToken: string): Promise<void> {
    try {
      const request: iap.ConfirmSubscriptionParams = {
        purchaseToken: purchaseToken
      };

      const result = await iap.confirmSubscription(getContext(), request);

      if (result.code === iap.IAPErrorCode.SUCCESS) {
        console.info('订阅确认成功');
      } else {
        throw new Error(`确认失败: ${result.code}`);
      }
    } catch (err) {
      console.error('确认订阅异常:', err);
      throw err;
    }
  }

  private generateOrderId(): string {
    return `ORD${Date.now()}${Math.floor(Math.random() * 1000)}`;
  }
}

2.5 商品购买页面

// entry/src/main/ets/pages/VipPage.ets
import { IapManager, Product } from '../utils/IapManager';
import { iap } from '@kit.IAPKit';

@Entry
@Component
struct VipPage {
  private iapManager: IapManager = new IapManager();
  @State products: Product[] = [];
  @State isLoading: boolean = true;
  @State purchasingId: string = '';

  aboutToAppear() {
    this.loadProducts();
  }

  private async loadProducts() {
    try {
      this.products = await this.iapManager.queryProducts([
        'premium_monthly',
        'premium_yearly',
        'coin_pack_100'
      ]);
    } catch (err) {
      console.error('加载商品失败:', err);
    } finally {
      this.isLoading = false;
    }
  }

  private async handlePurchase(product: Product) {
    this.purchasingId = product.productId;

    try {
      // 1. 购买
      const token = await this.iapManager.purchase(
        product.productId,
        product.productType
      );

      // 2. 服务端校验(此处简化,实际应调用你的服务器)
      const verified = await this.verifyWithServer(token);

      if (verified) {
        // 3. 发放权益
        await this.grantBenefits(product);

        // 4. 消耗商品(消耗型)
        if (product.productType === iap.ProductType.CONSUMABLE) {
          await this.iapManager.consumePurchase(token);
        }

        promptAction.showToast({ message: '购买成功!' });
      }
    } catch (err) {
      promptAction.showToast({ message: '购买失败,请重试' });
    } finally {
      this.purchasingId = '';
    }
  }

  private async verifyWithServer(token: string): Promise<boolean> {
    // 实际项目中,将token发送到你的服务器
    // 服务器调用华为订单校验API确认支付有效性
    console.info('校验PurchaseToken:', token);
    return true; // 简化示例
  }

  private async grantBenefits(product: Product): Promise<void> {
    // 根据商品发放对应权益
    console.info('发放权益:', product.productName);
  }

  build() {
    Column({ space: 16 }) {
      Text('升级会员')
        .fontSize(24)
        .fontWeight(FontWeight.Bold)
        .margin({ top: 32, bottom: 16 })

      Text('解锁无限云存储、AI转文字、高级导出')
        .fontSize(14)
        .fontColor('#888')

      if (this.isLoading) {
        LoadingProgress()
          .width(40)
          .height(40)
          .margin(32)
      } else {
        ForEach(this.products, (product: Product) => {
          Column({ space: 8 }) {
            Row() {
              Column({ space: 4 }) {
                Text(product.productName)
                  .fontSize(18)
                  .fontWeight(FontWeight.Medium)
                Text(product.productDesc)
                  .fontSize(13)
                  .fontColor('#888')
              }
              .layoutWeight(1)
              .alignItems(HorizontalAlign.Start)

              Text(product.price)
                .fontSize(20)
                .fontWeight(FontWeight.Bold)
                .fontColor('#2196F3')
            }
            .width('100%')

            Button(this.purchasingId === product.productId ? '处理中...' : '立即购买')
              .width('100%')
              .height(44)
              .backgroundColor('#2196F3')
              .enabled(this.purchasingId !== product.productId)
              .onClick(() => this.handlePurchase(product))
          }
          .width('100%')
          .padding(20)
          .backgroundColor('#FFFFFF')
          .borderRadius(12)
          .margin({ left: 16, right: 16 })
        })
      }
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#F5F7FA')
  }
}

三、服务端票据校验

支付安全的核心在于服务端校验。客户端返回的PurchaseToken必须由你的服务器向华为服务器确认:

// 服务端(Node.js)
import axios from 'axios';

const HUAWEI_IAP_VERIFY_URL = 'https://orders-drcn.iap.hicloud.com/orderUtilsService/v2/verifyPurchases';

interface VerifyRequest {
  purchaseToken: string;
  productId: string;
}

interface VerifyResponse {
  responseCode: string;
  purchaseTokenData?: {
    productId: string;
    purchaseToken: string;
    orderId: string;
    purchaseTime: number;
    purchaseState: number; // 0:已购买, 1:已取消
    consumptionState: number; // 0:未消耗, 1:已消耗
  };
}

async function verifyPurchaseToken(
  accessToken: string,
  purchaseToken: string,
  productId: string
): Promise<boolean> {
  const response = await axios.post<VerifyResponse>(
    HUAWEI_IAP_VERIFY_URL,
    {
      purchaseToken,
      productId
    },
    {
      headers: {
        Authorization: `Bearer ${accessToken}`,
        'Content-Type': 'application/json'
      }
    }
  );

  const data = response.data;
  return data.responseCode === '0' &&
         data.purchaseTokenData?.purchaseState === 0;
}

四、小结与下篇预告

本文打通了两条商业化核心链路:

  1. 华为账号登录:静默登录与显式授权两种模式、IDToken服务端校验、用户态管理

  2. 应用内支付:商品配置、购买流程、消耗型/非消耗型/订阅型三种商品类型、服务端票据校验

  3. 安全要点:PurchaseToken必须服务端校验、消耗型商品及时consume、订阅型商品确认续订

账号和支付解决的是"人"和"钱"的问题,而下一个浪潮是"智能"。端侧AI正在从噱头走向实用——语音识别、图像分类、文本生成,这些能力不再需要云端大模型,可以直接在设备上运行。第17篇将深入HiAI Foundation Kit,讲解如何在鸿蒙应用中集成端侧AI能力,实现本地模型推理和NLP任务处理。


参考阅读


评论