鸿蒙开发系列 · 高级篇
前言
地图功能跑通了,但一个完整的商业应用还缺两块关键拼图:用户是谁(账号体系)和怎么收钱(支付能力)。自己搭一套账号系统?意味着要处理注册、登录、密码找回、风控、合规等一系列问题。自己接支付?要面对渠道费率、对账、退款、税务的复杂流程。
HarmonyOS提供了**华为账号服务(Account Kit)和应用内支付(IAP Kit)**作为官方解决方案。华为账号覆盖全球数亿用户,一键授权即可获取用户身份;IAP Kit提供了标准化的商品管理、订单创建、支付收银台和票据校验流程。
本文将带你完成两条主链路:华为账号登录授权,以及应用内商品的配置与支付接入。
一、华为账号服务:Account Kit
1.1 华为账号的优势
对于HarmonyOS应用,华为账号几乎是标配选择。用户已经登录了系统账号,你的应用只需请求授权即可。
1.2 授权模式选择
Account Kit提供两种授权模式:
静默登录适合应用启动时的自动识别;授权码模式适合首次使用或需要更多用户信息的场景。
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提供的应用内支付解决方案,支持三种商品类型:
2.2 商品配置(AppGallery Connect)
在写代码之前,必须先在AppGallery Connect后台配置商品:
进入你的项目 → 盈利 → 应用内支付
创建商品:
商品ID(唯一标识,如
premium_monthly)商品类型(消耗型/非消耗型/订阅型)
价格(支持多币种)
名称和描述(多语言)
保存后等待审核(通常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;
}
四、小结与下篇预告
本文打通了两条商业化核心链路:
华为账号登录:静默登录与显式授权两种模式、IDToken服务端校验、用户态管理
应用内支付:商品配置、购买流程、消耗型/非消耗型/订阅型三种商品类型、服务端票据校验
安全要点:PurchaseToken必须服务端校验、消耗型商品及时consume、订阅型商品确认续订
账号和支付解决的是"人"和"钱"的问题,而下一个浪潮是"智能"。端侧AI正在从噱头走向实用——语音识别、图像分类、文本生成,这些能力不再需要云端大模型,可以直接在设备上运行。第17篇将深入HiAI Foundation Kit,讲解如何在鸿蒙应用中集成端侧AI能力,实现本地模型推理和NLP任务处理。
参考阅读
本系列第14篇:《音视频开发实战:AVPlayer、录音与媒体会话管理》
本系列第15篇:《地图与位置服务:Map Kit集成与轨迹绘制》