HarmonyOS NEXT + Spring Boot 实现华为推送服务完整教程

前言

本文详细介绍如何在 HarmonyOS NEXT 应用中集成华为 Push Kit 推送服务,实现服务端向客户端发送推送通知的完整流程。

本教程包含:

  • 客户端获取 Push Token
  • 客户端上传 Token 到服务器
  • 服务端调用华为推送 API 发送通知

一、整体架构

推送服务的整体流程分为两个阶段:

阶段一:Token 注册

  1. App 启动时,调用华为 Push Kit SDK 获取设备的 Push Token
  2. 将 Push Token 上传到自己的服务器保存

阶段二:发送推送

  1. 服务端需要推送时,从数据库查询目标用户的 Push Token
  2. 服务端调用华为推送 API,将消息推送到指定设备

二、客户端开发(HarmonyOS NEXT)

2.1 配置 Push Kit

首先在 AGC 控制台开通 Push Kit 服务,下载 agconnect-services.json 配置文件,放到项目的 entry/src/main/resources/rawfile/ 目录。

2.2 获取 Push Token

在应用的 EntryAbility 中初始化推送服务并获取 Token。

EntryAbility.ets

import { UIAbility, AbilityConstant, Want } from '@kit.AbilityKit';
import { pushService } from '@kit.PushKit';
import { preferences } from '@kit.ArkData';
import { http } from '@kit.NetworkKit';
import { hilog } from '@kit.PerformanceAnalysisKit';

const TAG = 'EntryAbility';

export default class EntryAbility extends UIAbility {
  private pushToken: string = '';

  onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
    hilog.info(0x0000, TAG, 'onCreate');
    
    // 初始化推送服务
    this.initPushService();
  }

  /**
   * 初始化推送服务,获取 Push Token
   */
  private initPushService(): void {
    hilog.info(0x0000, TAG, 'initPushService: Starting...');
    
    pushService.getToken()
      .then((token: string) => {
        if (!token || token.length === 0) {
          hilog.warn(0x0000, TAG, 'initPushService: Got empty token');
          return;
        }
        
        hilog.info(0x0000, TAG, 'initPushService: Got push token successfully');
        this.pushToken = token;
        
        // 保存到本地存储
        this.saveTokenToLocal(token);
        
        // 上传到服务器
        this.uploadTokenToServer(token);
      })
      .catch((err: Error) => {
        hilog.error(0x0000, TAG, `initPushService: Failed: ${err.message}`);
      });
  }

  /**
   * 保存 Token 到本地存储
   */
  private async saveTokenToLocal(token: string): Promise<void> {
    try {
      const options: preferences.Options = { name: 'app_preferences' };
      const dataPreferences = await preferences.getPreferences(this.context, options);
      await dataPreferences.put('push_token', token);
      await dataPreferences.flush();
      hilog.info(0x0000, TAG, 'saveTokenToLocal: Success');
    } catch (e) {
      hilog.error(0x0000, TAG, `saveTokenToLocal: Failed: ${e}`);
    }
  }

  /**
   * 上传 Token 到服务器
   */
  private async uploadTokenToServer(token: string): Promise<void> {
    try {
      // 从本地获取用户登录的 auth token
      const options: preferences.Options = { name: 'app_preferences' };
      const dataPreferences = await preferences.getPreferences(this.context, options);
      const authToken = await dataPreferences.get('auth_token', '') as string;
      
      if (!authToken || authToken.length === 0) {
        hilog.warn(0x0000, TAG, 'uploadTokenToServer: User not logged in');
        return;
      }
      
      // 发送 HTTP 请求上传 Push Token
      const httpRequest = http.createHttp();
      const response = await httpRequest.request(
        'https://your-server.com/api/user/push-token',
        {
          method: http.RequestMethod.POST,
          header: {
            'Content-Type': 'application/json',
            'Authorization': `Bearer ${authToken}`
          },
          extraData: JSON.stringify({ pushToken: token })
        }
      );
      
      hilog.info(0x0000, TAG, `uploadTokenToServer: Response code=${response.responseCode}`);
      httpRequest.destroy();
      
    } catch (e) {
      hilog.error(0x0000, TAG, `uploadTokenToServer: Failed: ${e}`);
    }
  }

  // ... 其他生命周期方法
}

2.3 关键说明

  1. pushService.getToken() 是异步方法,返回设备唯一的 Push Token
  2. Push Token 需要保存到本地,方便后续使用
  3. 用户登录后,需要将 Push Token 上传到服务器与用户关联
  4. 如果用户未登录,可以先保存 Token,等登录后再上传

三、服务端开发(Spring Boot)

3.1 用户实体类

首先在用户表中添加 pushToken 字段,用于存储用户设备的 Push Token。

User.java

@Entity
@Table(name = "users")
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    private String username;
    private String password;
    private String nickname;
    
    // Push Token 字段
    @Column(length = 512)
    private String pushToken;
    
    // Getters and Setters
    public Long getId() { return id; }
    public void setId(Long id) { this.id = id; }
    
    public String getPushToken() { return pushToken; }
    public void setPushToken(String pushToken) { this.pushToken = pushToken; }
    
    // ... 其他字段的 getter/setter
}

3.2 接收 Push Token 的接口

UserController.java

@RestController
@RequestMapping("/api/user")
public class UserController {
    
    @Autowired
    private UserRepository userRepository;
    
    @Autowired
    private JwtUtil jwtUtil;
    
    /**
     * 更新用户的 Push Token
     */
    @PostMapping("/push-token")
    public ResponseEntity<?> updatePushToken(
            @RequestHeader("Authorization") String authorization,
            @RequestBody Map<String, String> request) {
        
        try {
            // 从 JWT 中解析用户 ID
            String token = authorization.replace("Bearer ", "");
            Long userId = jwtUtil.getUserIdFromToken(token);
            
            // 获取请求中的 Push Token
            String pushToken = request.get("pushToken");
            if (pushToken == null || pushToken.isEmpty()) {
                return ResponseEntity.badRequest().body(Map.of(
                    "code", 400,
                    "message", "pushToken 不能为空"
                ));
            }
            
            // 更新用户的 Push Token
            Optional<User> userOpt = userRepository.findById(userId);
            if (userOpt.isEmpty()) {
                return ResponseEntity.status(404).body(Map.of(
                    "code", 404,
                    "message", "用户不存在"
                ));
            }
            
            User user = userOpt.get();
            user.setPushToken(pushToken);
            userRepository.save(user);
            
            return ResponseEntity.ok(Map.of(
                "code", 200,
                "message", "Push Token 更新成功"
            ));
            
        } catch (Exception e) {
            return ResponseEntity.status(500).body(Map.of(
                "code", 500,
                "message", "更新失败: " + e.getMessage()
            ));
        }
    }
}

3.3 华为推送服务配置

application.properties 中添加华为推送的配置:

# 华为推送配置(从 AGC 控制台获取)
huawei.push.app-id=你的AppID
huawei.push.client-id=你的ClientID  
huawei.push.client-secret=你的ClientSecret
huawei.push.project-id=你的ProjectID

创建配置类读取这些配置:

HuaweiPushConfig.java

@Configuration
@ConfigurationProperties(prefix = "huawei.push")
public class HuaweiPushConfig {
    private String appId;
    private String clientId;
    private String clientSecret;
    private String projectId;
    
    // Getters and Setters
    public String getAppId() { return appId; }
    public void setAppId(String appId) { this.appId = appId; }
    
    public String getClientId() { return clientId; }
    public void setClientId(String clientId) { this.clientId = clientId; }
    
    public String getClientSecret() { return clientSecret; }
    public void setClientSecret(String clientSecret) { this.clientSecret = clientSecret; }
    
    public String getProjectId() { return projectId; }
    public void setProjectId(String projectId) { this.projectId = projectId; }
}

3.4 华为推送服务实现

这是核心的推送服务类,负责调用华为推送 API。

HuaweiPushService.java

@Service
public class HuaweiPushService {
    
    private static final Logger log = LoggerFactory.getLogger(HuaweiPushService.class);
    
    // 华为 OAuth2 Token 获取地址
    private static final String TOKEN_URL = "https://oauth-login.cloud.huawei.com/oauth2/v3/token";
    
    // 华为推送 API 地址(v3 版本,用于 HarmonyOS NEXT)
    private static final String PUSH_URL_TEMPLATE = "https://push-api.cloud.huawei.com/v3/%s/messages:send";
    
    @Autowired
    private HuaweiPushConfig config;
    
    @Autowired
    private UserRepository userRepository;
    
    @Autowired
    private ObjectMapper objectMapper;
    
    // 缓存 Access Token
    private String accessToken;
    private long tokenExpireTime = 0;
    
    /**
     * 发送推送通知给指定用户
     * @param userId 目标用户ID
     * @param title 通知标题
     * @param body 通知内容
     * @param data 自定义数据(可选)
     * @return 是否发送成功
     */
    public boolean sendNotificationToUser(Long userId, String title, String body, Map<String, Object> data) {
        log.info("sendNotificationToUser: userId={}, title={}", userId, title);
        
        try {
            // 1. 查询用户的 Push Token
            Optional<User> userOpt = userRepository.findById(userId);
            if (userOpt.isEmpty()) {
                log.warn("User not found: {}", userId);
                return false;
            }
            
            String pushToken = userOpt.get().getPushToken();
            if (pushToken == null || pushToken.isEmpty()) {
                log.warn("User {} has no push token", userId);
                return false;
            }
            
            // 2. 发送推送
            return sendPushNotification(pushToken, title, body, data);
            
        } catch (Exception e) {
            log.error("sendNotificationToUser failed", e);
            return false;
        }
    }
    
    /**
     * 发送推送通知
     * @param pushToken 设备 Push Token
     * @param title 通知标题
     * @param body 通知内容
     * @param data 自定义数据
     * @return 是否发送成功
     */
    public boolean sendPushNotification(String pushToken, String title, String body, Map<String, Object> data) {
        log.info("sendPushNotification: title={}, body={}", title, body);
        
        // 检查配置
        if (!isConfigured()) {
            log.warn("Huawei Push is not configured");
            return false;
        }
        
        try {
            // 1. 获取 Access Token
            refreshAccessTokenIfNeeded();
            
            // 2. 构建推送消息
            Map<String, Object> message = buildPushMessage(pushToken, title, body, data);
            String messageJson = objectMapper.writeValueAsString(message);
            log.info("Push message: {}", messageJson);
            
            // 3. 发送推送请求
            String pushUrl = String.format(PUSH_URL_TEMPLATE, config.getProjectId());
            String response = sendHttpPost(pushUrl, messageJson, accessToken);
            log.info("Push response: {}", response);
            
            // 4. 解析响应
            Map<String, Object> result = objectMapper.readValue(response, Map.class);
            String code = String.valueOf(result.get("code"));
            
            // 华为推送成功码是 "80000000"
            boolean success = "80000000".equals(code);
            if (!success) {
                log.error("Push failed: code={}, msg={}", code, result.get("msg"));
            }
            
            return success;
            
        } catch (Exception e) {
            log.error("sendPushNotification failed", e);
            return false;
        }
    }
    
    // ... 后续方法见下文
}

3.5 构建推送消息体

华为推送 v3 API 的消息格式如下:

/**
 * 构建推送消息体(v3 API 格式)
 */
private Map<String, Object> buildPushMessage(String pushToken, String title, String body, Map<String, Object> data) {
    Map<String, Object> root = new HashMap<>();
    
    // target: 推送目标
    Map<String, Object> target = new HashMap<>();
    target.put("token", new String[]{pushToken});
    root.put("target", target);
    
    // payload: 推送内容
    Map<String, Object> payload = new HashMap<>();
    
    // notification: 通知消息
    Map<String, Object> notification = new HashMap<>();
    notification.put("title", title);
    notification.put("body", body);
    notification.put("category", "IM");  // 消息类别
    
    // clickAction: 点击动作
    Map<String, Object> clickAction = new HashMap<>();
    clickAction.put("actionType", 0);  // 0: 打开应用首页
    notification.put("clickAction", clickAction);
    
    payload.put("notification", notification);
    
    // data: 自定义数据(点击通知时传递给应用)
    if (data != null && !data.isEmpty()) {
        try {
            payload.put("data", objectMapper.writeValueAsString(data));
        } catch (Exception e) {
            log.warn("Failed to serialize data", e);
        }
    }
    
    // pushOptions: 推送选项
    Map<String, Object> pushOptions = new HashMap<>();
    pushOptions.put("testMessage", false);
    payload.put("pushOptions", pushOptions);
    
    root.put("payload", payload);
    return root;
}

生成的 JSON 格式示例:

{
  "target": {
    "token": ["IQAAAACy0T5MAAC..."]
  },
  "payload": {
    "notification": {
      "title": "您有一条新消息",
      "body": "张三给您发送了一条消息",
      "category": "IM",
      "clickAction": {
        "actionType": 0
      }
    },
    "data": "{\"type\":\"chat\",\"senderId\":123}",
    "pushOptions": {
      "testMessage": false
    }
  }
}

3.6 获取华为 Access Token

调用华为推送 API 需要先获取 Access Token,Token 有效期为 1 小时。

/**
 * 刷新 Access Token(如果过期)
 */
private void refreshAccessTokenIfNeeded() throws Exception {
    // 检查 Token 是否仍然有效
    if (accessToken != null && System.currentTimeMillis() < tokenExpireTime) {
        return;
    }
    
    log.info("Refreshing access token...");
    
    // 构建 OAuth2 请求参数
    String params = "grant_type=client_credentials"
        + "&client_id=" + URLEncoder.encode(config.getAppId(), StandardCharsets.UTF_8)
        + "&client_secret=" + URLEncoder.encode(config.getClientSecret(), StandardCharsets.UTF_8);
    
    // 发送请求
    String response = sendHttpPostForm(TOKEN_URL, params);
    log.info("Token response: {}", response);
    
    // 解析响应
    Map<String, Object> result = objectMapper.readValue(response, Map.class);
    
    if (result.containsKey("access_token")) {
        accessToken = (String) result.get("access_token");
        int expiresIn = ((Number) result.get("expires_in")).intValue();
        
        // 设置过期时间(提前 5 分钟刷新)
        tokenExpireTime = System.currentTimeMillis() + (expiresIn - 300) * 1000L;
        
        log.info("Access token obtained, expires in {} seconds", expiresIn);
    } else {
        String error = String.valueOf(result.get("error"));
        String errorDesc = String.valueOf(result.get("error_description"));
        throw new RuntimeException("Failed to get access token: " + error + " - " + errorDesc);
    }
}

3.7 HTTP 请求工具方法

/**
 * 发送 HTTP POST 请求(JSON 格式)
 */
private String sendHttpPost(String urlStr, String body, String bearerToken) throws Exception {
    URL url = new URL(urlStr);
    HttpURLConnection conn = (HttpURLConnection) url.openConnection();
    
    try {
        conn.setRequestMethod("POST");
        conn.setDoOutput(true);
        conn.setConnectTimeout(5000);
        conn.setReadTimeout(5000);
        
        // 设置请求头
        conn.setRequestProperty("Authorization", "Bearer " + bearerToken);
        conn.setRequestProperty("Content-Type", "application/json; charset=UTF-8");
        conn.setRequestProperty("push-type", "0");  // v3 API 需要此 header
        
        // 发送请求体
        try (OutputStream os = conn.getOutputStream()) {
            os.write(body.getBytes(StandardCharsets.UTF_8));
        }
        
        // 读取响应
        int responseCode = conn.getResponseCode();
        InputStream is = responseCode >= 400 ? conn.getErrorStream() : conn.getInputStream();
        
        if (is == null) {
            return "{\"code\":\"" + responseCode + "\"}";
        }
        
        try (BufferedReader br = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8))) {
            StringBuilder sb = new StringBuilder();
            String line;
            while ((line = br.readLine()) != null) {
                sb.append(line);
            }
            return sb.toString();
        }
    } finally {
        conn.disconnect();
    }
}

/**
 * 发送 HTTP POST 请求(表单格式,用于获取 Token)
 */
private String sendHttpPostForm(String urlStr, String params) throws Exception {
    URL url = new URL(urlStr);
    HttpURLConnection conn = (HttpURLConnection) url.openConnection();
    
    try {
        conn.setRequestMethod("POST");
        conn.setDoOutput(true);
        conn.setConnectTimeout(5000);
        conn.setReadTimeout(5000);
        conn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
        
        try (OutputStream os = conn.getOutputStream()) {
            os.write(params.getBytes(StandardCharsets.UTF_8));
        }
        
        int responseCode = conn.getResponseCode();
        InputStream is = responseCode >= 400 ? conn.getErrorStream() : conn.getInputStream();
        
        if (is == null) {
            return "{\"error\":\"" + responseCode + "\"}";
        }
        
        try (BufferedReader br = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8))) {
            StringBuilder sb = new StringBuilder();
            String line;
            while ((line = br.readLine()) != null) {
                sb.append(line);
            }
            return sb.toString();
        }
    } finally {
        conn.disconnect();
    }
}

/**
 * 检查推送服务是否已配置
 */
private boolean isConfigured() {
    return config.getAppId() != null && !config.getAppId().isEmpty()
        && config.getClientSecret() != null && !config.getClientSecret().isEmpty()
        && config.getProjectId() != null && !config.getProjectId().isEmpty();
}

四、使用示例

4.1 在业务代码中发送推送

例如,当用户收到新消息时发送推送通知:

MessageService.java

@Service
public class MessageService {
    
    @Autowired
    private HuaweiPushService pushService;
    
    @Autowired
    private MessageRepository messageRepository;
    
    /**
     * 发送消息
     */
    public Message sendMessage(Long senderId, Long receiverId, String content) {
        // 1. 保存消息到数据库
        Message message = new Message();
        message.setSenderId(senderId);
        message.setReceiverId(receiverId);
        message.setContent(content);
        message.setCreateTime(System.currentTimeMillis());
        messageRepository.save(message);
        
        // 2. 发送推送通知给接收者
        String title = "您有一条新消息";
        String body = content.length() > 50 ? content.substring(0, 50) + "..." : content;
        
        // 自定义数据,用于点击通知后跳转
        Map<String, Object> data = new HashMap<>();
        data.put("type", "chat");
        data.put("senderId", senderId);
        
        pushService.sendNotificationToUser(receiverId, title, body, data);
        
        return message;
    }
}

4.2 测试推送接口

可以创建一个测试接口来验证推送功能:

TestController.java

@RestController
@RequestMapping("/api/test")
public class TestController {
    
    @Autowired
    private HuaweiPushService pushService;
    
    /**
     * 测试推送(仅用于开发测试)
     */
    @PostMapping("/push")
    public ResponseEntity<?> testPush(@RequestBody Map<String, Object> request) {
        Long userId = ((Number) request.get("userId")).longValue();
        String title = (String) request.get("title");
        String body = (String) request.get("body");
        
        boolean success = pushService.sendNotificationToUser(userId, title, body, null);
        
        return ResponseEntity.ok(Map.of(
            "code", success ? 200 : 500,
            "message", success ? "推送成功" : "推送失败"
        ));
    }
}

测试请求:

curl -X POST http://localhost:8080/api/test/push \
  -H "Content-Type: application/json" \
  -d '{
    "userId": 1,
    "title": "测试推送",
    "body": "这是一条测试消息"
  }'

五、AGC 控制台配置说明

5.1 开通 Push Kit 服务

  1. 登录 华为 AGC 控制台
  2. 选择或创建你的项目
  3. 在左侧菜单选择「增长」→「推送服务」
  4. 点击「开通服务」

5.2 获取配置参数

  1. 进入「项目设置」→「常规」

  2. 记录以下信息:

    • App ID
    • Client ID
    • Project ID
  3. 进入「项目设置」→「Server SDK」

  4. 创建或查看 Client Secret

5.3 下载配置文件

  1. 在「项目设置」→「常规」页面
  2. 下载 agconnect-services.json 文件
  3. 将文件放到 HarmonyOS 项目的 entry/src/main/resources/rawfile/ 目录

六、常见问题

问题 1:获取 Push Token 失败

可能原因:

  • AGC 配置文件未正确放置
  • Push Kit 服务未开通
  • 设备网络异常

解决方案:

  • 检查 agconnect-services.json 文件是否存在
  • 确认 AGC 控制台已开通 Push Kit
  • 检查设备网络连接

问题 2:Access Token 获取失败

可能原因:

  • App ID 或 Client Secret 配置错误
  • 网络无法访问华为服务器

解决方案:

  • 核对 AGC 控制台的配置信息
  • 检查服务器网络是否能访问 oauth-login.cloud.huawei.com

问题 3:推送发送成功但收不到通知

可能原因:

  • Push Token 已过期或无效
  • 设备通知权限未开启
  • 应用被系统限制后台运行

解决方案:

  • 让用户重新打开 App 获取新的 Push Token
  • 检查设备的通知权限设置
  • 检查应用的后台运行权限

问题 4:华为推送返回错误码

常见错误码说明:

  • 80000000:成功
  • 80100000:参数错误,检查请求格式
  • 80100001:Access Token 无效,需要重新获取
  • 80300007:Push Token 无效,设备需要重新获取 Token
  • 80300008:消息体过大,减少内容长度

七、总结

本文介绍了 HarmonyOS NEXT 应用集成华为推送服务的完整流程:

  1. 客户端:通过 Push Kit SDK 获取设备 Token,上传到服务器
  2. 服务端:保存用户的 Push Token,需要推送时调用华为 API 发送通知
  3. 华为云:负责将通知推送到目标设备

关键点:

  • Push Token 是设备唯一标识,需要与用户关联保存
  • Access Token 有效期 1 小时,需要缓存并定时刷新
  • v3 API 用于 HarmonyOS NEXT,消息格式与 v1 略有不同

希望本文对你有所帮助!

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐