2024年10月


喊了多年的互联网寒冬,今年的寒风格外凛冽,还在坚守安卓开发的朋友着实不容易。因为能转行的早就转了,能转岗的也早就转了,那么安卓程序员比较迷茫的就是,我该学什么安卓技术才好呢?还是直接扔了安卓再去搞别的技术吗?

下面探讨下安卓程序员还能在哪些方面进阶修炼,主要有以下三个方向。

一、纵向钻研谷歌爸爸推出的最新技术

谷歌就是安卓的爹,只要谷歌不倒,安卓开发就不会倒。今年的谷歌开发者大会都放到北京举办了,可见爸爸真的非常疼咱们爱咱们。
爸爸每段时间就推出新款Android的预览版、测试版和稳定版,各种新特性就像挤牙膏似的总也挤不完,咱们就慢慢吮吸总也吸不完。
爸爸每段时间就推出新款Android Studio的Alpha版、Beta版、Canary版,然后还有Release版,这些ABC加上R版本,如此顺口方便咱们细嚼慢咽。
爸爸推出了Kotlin语言,谁让当年采用Java语言的爸爸吃了官司呢?既然爸爸吃了Java官司,那么咱们赶紧把Kotlin囫囵吞下去。
爸爸推出了Flutter跨平台框架,即使爸爸裁了FuchsiaOS团队,裁了Python团队,连Go团队的技术负责人都一脚踢飞,但仍然保留着Flutter和Dart团队,所以咱们要放下顾虑,继续无脑向Flutter冲呀。
爸爸推出了Jetpack和Compose套件,这些年来Jetpack和Compose不断推陈出新,可见爸爸唯恐咱们饿了没东西啃,所以咱们年年啃月月啃。
该方向的学习难度系数为★★★,保饭碗指数为★★
。理由:爸爸的App开发技术都是公开的,而且简单易学门槛低。
嗯,学习Jetpack套件与最新的Android开发推荐这本书《Android Studio开发实战:从零基础到App上线(第3版)》,该书基于Android12和Android Studio Dolphin,介绍了包含DataStore、Room、RecyclerView、ViewPager2、WorkManager、Glide、CameraX、ExoPlayer等等在内的Jetpack套件。

二、横向拓展安卓开发的新功能新应用

除了谷歌爸爸推出的组件库,还有其他专业领域的第三方库,能够实现与众不同的新功能。
比如初级安卓只会调用HTTP的POST接口,但是物联网方面更需要Socket通信与蓝牙通信,那么SocketIO、WebSocket、Bluetooth LE就是必须掌握的。具体参见《Android Studio开发实战:从零基础到App上线(第3版)》一书的“13.4  即时通信”和“17.3  低功耗蓝牙”。
又如初级安卓只会使用画布Canvas和画笔Paint作图,但是AI视觉方面更需要三维制图和动态追踪,那么OpenGL、OpenGL ES、OpenCV就是必须掌握的。具体参见《Android App开发进阶与项目实战》一书的“5.1  OpenGL”、“5.2  OpenGL ES”和“12.2  基于计算机视觉的人脸识别”。
再如初级安卓只会通过VideoView和ExoPlayer播放视频,但是音视频方面更需要实时交互和动态剪辑,那么WebRTC、FFmpeg、RTMP/SRT(直播协议)就是必须掌握的。其中WebRTC的App开发参见《Android Studio开发实战:从零基础到App上线(第3版)》一书的“20.2  给App集成WebRTC”,FFmpeg的App开发参见《FFmpeg开发实战:从零基础到短视频上线》一书的“第12章  FFmpeg的移动开发”,手机App的直播技术参见之前的文章《利用RTMP协议构建电脑与手机的直播Demo》和《利用SRT协议构建手机APP的直播Demo》。
该方向的学习难度系数为★★★★,保饭碗指数为★★★。
理由:以上技能涉及计算机科学的专业领域知识,具备一定的技术门槛。
嗯,学习音视频和FFmpeg编程技术推荐这本书《FFmpeg开发实战:从零基础到短视频上线》,该书详细介绍了如何在Windows系统和Linux系统分别搭建FFmpeg的开发环境,第12章还介绍了如何通过Android Studio+FFmpeg开发手机App。

三、打开思路迈向Android+N的新天地

除了常见的App应用开发之外,安卓与其他行业结合还能产生更多的就业岗位。
比如Android+汽车行业就产生了车机开发,那要学习车载系统Automotive OS,以及外景系统EVS、娱乐系统IVI等等。其中Automotive OS是谷歌爸爸基于AOSP开发的,目前已经迭代到了Automotive OS 14。
又如Android+游戏行业就产生了手游开发,那要学习Unity3D、Cocos2d-x、Unreal4、CryEngine3等游戏引擎。其中Unity3D是国外研发的历史悠久游戏引擎,而Cocos2d-x是国产的后起之秀游戏引擎。
再如Android+安全行业就产生了网安开发,那要学习逆向工具Frida、系统框架工具LSPosed、全局注入管理工具RxPosed、脱抽取壳工具dumpDex、自定义APK模块加载器HideApk,以及逆向观测技术jvmti、Native层观测技术gdbinjec等等.
该方向的学习难度系数为★★★★★,保饭碗指数为★★★★。
理由:以上属于细分行业的专门技能,独特的行业经验拥有高门槛。
嗯,学习Android原生App的安全和逆向技术推荐这本书《Frida Android SO逆向深入实践》,该书详细介绍了如何使用Frida揭示原生App的逆向、分析和破解之奥秘,还探讨了ARM/ELF的文件格式和反编译工具IDA。

更多详细的FFmpeg开发知识参见
《FFmpeg开发实战:从零基础到短视频上线》
一书。


title: Nuxt.js 应用中的 ready 事件钩子详解
date: 2024/10/12
updated: 2024/10/12
author:
cmdragon

excerpt:
ready 钩子是 Nuxt.js 中一个重要的生命周期事件,它在 Nuxt 实例初始化完成后被调用。当 Nuxt 已经准备好并准备开始处理请求或渲染页面时,这一钩子会被触发。

categories:

  • 前端开发

tags:

  • Nuxt.js
  • 生命周期
  • ready钩子
  • 应用初始化
  • 前端开发
  • Nuxt实例
  • 请求处理


image
image

扫描
二维码
关注或者微信搜一搜:
编程智域 前端至全栈交流与成长

ready
钩子是 Nuxt.js 中一个重要的生命周期事件,它在 Nuxt 实例初始化完成后被调用。当 Nuxt
已经准备好并准备开始处理请求或渲染页面时,这一钩子会被触发。通过使用
ready
钩子,开发者可以在应用初始化后执行一些必要的操作。


目录

  1. 概述
  2. ready 钩子的详细说明
  3. 具体使用示例
  4. 应用场景
  5. 实际开发中的最佳实践
  6. 注意事项
  7. 关键要点
  8. 练习题
  9. 总结


1. 概述

ready
钩子在 Nuxt 应用完成初始化并准备好接收用户请求或渲染页面时被调用。这使得开发者可以在这个阶段进行一些后期的设置或配置。

2. ready 钩子的详细说明

2.1 钩子的定义与作用

ready
钩子的主要功能包括:

  • 执行应用启动后的初始化逻辑
  • 设定全局变量或配置
  • 进行日志记录或监测

2.2 调用时机

  • 执行环境
    : 可在客户端和服务器端使用。
  • 挂载时机
    : 当 Nuxt 实例完成初始化并准备处理请求时,
    ready
    钩子会被调用。

2.3 返回值与异常处理

钩子没有返回值。钩子内部的异常应被妥善处理,以避免影响应用的正常运行。

3. 具体使用示例

3.1 基本用法示例

假设我们希望在 Nuxt 初始化完成后进行一些全局设置,比如初始化一个 API 客户端:

// plugins/readyPlugin.js
export default defineNuxtPlugin((nuxtApp) => {
    nuxtApp.hooks.ready(() => {
        console.log('Nuxt app is ready!');
        // 初始化 API 客户端等
        nuxtApp.$api = createApiClient();
    });
});

在这个示例中,我们在 Nuxt 实例准备好后输出日志并初始化一个 API 客户端。

3.2 与其他钩子结合使用

ready
钩子可以与其他钩子结合使用,以实现复杂的初始化逻辑:

// plugins/readyPlugin.js
export default defineNuxtPlugin((nuxtApp) => {
    nuxtApp.hooks.ready(() => {
        console.log('Nuxt app is ready!');
        // 设置全局状态
        nuxtApp.$store.dispatch('initGlobalState');
    });

    nuxtApp.hooks('page:transition:finish', () => {
        console.log('Page transition finished.');
    });
});

在这个例子中,我们在 Nuxt 准备好后初始化全局状态,同时监听页面过渡完成的事件。

4. 应用场景

  1. 全局配置
    : 在应用启动时进行全局变量或配置项的设定。
  2. 服务初始化
    : 初始化第三方服务,比如 Analytics、API 客户端等。
  3. 性能监测
    : 在应用准备好后开始性能监测。

5. 实际开发中的最佳实践

  1. 简洁明了
    : 在
    ready
    钩子中只执行必要的初始化逻辑,避免过于复杂的操作。
  2. 错误处理
    : 钩子内部应充分捕获可能出现的异常,以提高应用的健壮性。
  3. 模块化
    : 将不同的初始化代码分散到不同的插件中,以提升可维护性。

6. 注意事项

  • 性能考虑
    : 确保在钩子中执行的操作不会显著影响应用的加载时间。
  • 依赖管理
    : 确保在
    ready
    阶段的时候,所有需要的依赖已经准备好。

7. 关键要点

  • ready
    钩子在 Nuxt 实例完成初始化后被调用,用于执行基本配置和启动逻辑。
  • 合理利用此钩子可以提高应用的启动效率和用户体验。
  • 处理钩子中的异常非常重要,以确保应用的正常运行。

8. 练习题

  1. 全局状态初始化
    : 在
    ready
    钩子中实现全局状态的初始化逻辑。
  2. API 请求检测
    : 在应用准备好后,自动发送一次 API 请求以检测 API 是否正常。
  3. 性能日志
    : 在
    ready
    钩子中记录应用的启动时间,以分析性能瓶颈。

9. 总结

ready
是一个非常有用的钩子,它允许开发者在 Nuxt 应用完成初始化后执行必要的操作。合理利用这一钩子可以增强应用的可用性和用户体验。

余下文章内容请点击跳转至 个人博客页面 或者 扫码关注或者微信搜一搜:
编程智域 前端至全栈交流与成长
,阅读完整的文章:
Nuxt.js 应用中的 ready 事件钩子详解 | cmdragon's Blog

往期文章归档:

  • 实在是不知道标题写什么了 可以在评论区给个建议哈哈哈哈 先用这个作为标题吧

尝试使用 国内给出的 AI 大模型做出一个 可以和 AI 对话的 网站出来

    <dependency>
        <groupId>cn.bigmodel.openapi</groupId>
        <artifactId>oapi-java-sdk</artifactId>
        <version>release-V4-2.0.0</version>
    </dependency>
  • 使用 普通的 java -- Maven项目 只能在控制台 查看结果 也就是 说没有办法在其他平台 使
    用 制作出来的 AI ChatRobot
  • 思来想去 不如 将这个东西写成 QQ 机器人
  • 但是因为我找到的 那个 不更新了 或者 腾讯不支持了 让我放弃了 写成 QQ 机器人的想法
  • 于是我就尝试将这个写成一个本地的 AI 对话机器人 但是 在翻看 官方给出的 Demo 我偶然发现了一个方法 他的 输出似乎是一个 json 转换成的 String
  • 这个方法并没有将这个String 返回出来 而是 直接在控制台打印
package com.codervibe.utils;

import com.alibaba.fastjson.JSON;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.PropertyNamingStrategy;
import com.zhipu.oapi.ClientV4;
import com.zhipu.oapi.Constants;
import com.zhipu.oapi.service.v4.image.CreateImageRequest;
import com.zhipu.oapi.service.v4.image.ImageApiResponse;
import com.zhipu.oapi.service.v4.model.*;
import io.reactivex.Flowable;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicBoolean;

public class ChatAPIUtils {
    private static final String API_KEY = "cb11ad7f3b68ce03ed9be6e13573aa19";

    private static final String API_SECRET = "nG7UQrrXqsXtqD1S";

    private static final ClientV4 client = new ClientV4.Builder(API_KEY, API_SECRET).build();

    private static final ObjectMapper mapper = defaultObjectMapper();


    public static ObjectMapper defaultObjectMapper() {
        ObjectMapper mapper = new ObjectMapper();
        mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
        mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
        mapper.setPropertyNamingStrategy(PropertyNamingStrategy.SNAKE_CASE);
        mapper.addMixIn(ChatFunction.class, ChatFunctionMixIn.class);
        mapper.addMixIn(ChatCompletionRequest.class, ChatCompletionRequestMixIn.class);
        mapper.addMixIn(ChatFunctionCall.class, ChatFunctionCallMixIn.class);
        return mapper;
    }

    // 请自定义自己的业务id
    private static final String requestIdTemplate = "mycompany-%d";



    /**
     * 同步调用
     */
    public static String InvokeApi(String content) throws JsonProcessingException {
        List<ChatMessage> messages = new ArrayList<>();
        ChatMessage chatMessage = new ChatMessage(ChatMessageRole.USER.value(), content);
        messages.add(chatMessage);
        String requestId = String.format(requestIdTemplate, System.currentTimeMillis());
        // 函数调用参数构建部分
        List<ChatTool> chatToolList = new ArrayList<>();
        ChatTool chatTool = new ChatTool();
        chatTool.setType(ChatToolType.FUNCTION.value());
        ChatFunctionParameters chatFunctionParameters = new ChatFunctionParameters();
        chatFunctionParameters.setType("object");
        Map<String, Object> properties = new HashMap<>();
        properties.put("location", new HashMap<String, Object>() {{
            put("type", "string");
            put("description", "城市,如:北京");
        }});
        properties.put("unit", new HashMap<String, Object>() {{
            put("type", "string");
            put("enum", new ArrayList<String>() {{
                add("celsius");
                add("fahrenheit");
            }});
        }});
        chatFunctionParameters.setProperties(properties);
        ChatFunction chatFunction = ChatFunction.builder()
                .name("get_weather")
                .description("Get the current weather of a location")
                .parameters(chatFunctionParameters)
                .build();
        chatTool.setFunction(chatFunction);
        chatToolList.add(chatTool);
        ChatCompletionRequest chatCompletionRequest = ChatCompletionRequest.builder()
                .model(Constants.ModelChatGLM4)
                .stream(Boolean.FALSE)
                .invokeMethod(Constants.invokeMethod)
                .messages(messages)
                .requestId(requestId)
                .tools(chatToolList)
                .toolChoice("auto")
                .build();
        ModelApiResponse invokeModelApiResp = client.invokeModelApi(chatCompletionRequest);
        try {
        // 这里返回出去是一个 json 
            return mapper.writeValueAsString(invokeModelApiResp);
        } catch (JsonProcessingException e) {
            e.printStackTrace();
        }
        return mapper.writeValueAsString(new ModelApiResponse());
    }

    public static void CreateImage(String content) {
        CreateImageRequest createImageRequest = new CreateImageRequest();
        createImageRequest.setModel(Constants.ModelCogView);
        createImageRequest.setPrompt(content);
        ImageApiResponse imageApiResponse = client.createImage(createImageRequest);
        System.out.println("imageApiResponse:" + JSON.toJSONString(imageApiResponse));
    }

}

  • 工具类中 InvokeApi 方法 最后获得的是一个 ModelApiResponse类 这个类有点类似于 统一返回类型 但是我在这里 只需要里面的具体方法 请求状态和 信息 并不需要 (有另外一个统一返回类型定义 ) 所以在 后面我将这个方法 修改 改为 将我需要的数据返回给controller
  • 实际上这是不应该直接返回给 controller 的 而是 应该 通过 service 的 因为service中才是真正的业务代码
  • 修改后的方法 代码如下
    /**
     * 同步调用
     */
    public static ModelData InvokeApi(String content) throwsJsonProcessingException{
        List<ChatMessage> messages = new ArrayList<>();
        ChatMessage chatMessage = new ChatMessage(ChatMessageRole.USER.value(), content);
        messages.add(chatMessage);
        String requestId = String.format(requestIdTemplate, System.currentTimeMillis());
        // 函数调用参数构建部分
        List<ChatTool> chatToolList = new ArrayList<>();
        ChatTool chatTool = new ChatTool();
        chatTool.setType(ChatToolType.FUNCTION.value());
        ChatFunctionParameters chatFunctionParameters = new ChatFunctionParameters();
        chatFunctionParameters.setType("object");
        Map<String, Object> properties = new HashMap<>();
        properties.put("location", new HashMap<String, Object>() {{
            put("type", "string");
            put("description", "城市,如:北京");
        }});
        properties.put("unit", new HashMap<String, Object>() {{
            put("type", "string");
            put("enum", new ArrayList<String>() {{
                add("celsius");
                add("fahrenheit");
            }});
        }});
        chatFunctionParameters.setProperties(properties);
        ChatFunction chatFunction = ChatFunction.builder()
                .name("get_weather")
                .description("Get the current weather of a location")
                .parameters(chatFunctionParameters)
                .build();
        chatTool.setFunction(chatFunction);
        chatToolList.add(chatTool);
        ChatCompletionRequest chatCompletionRequest = ChatCompletionRequest.builder()
                .model(Constants.ModelChatGLM4)
                .stream(Boolean.FALSE)
                .invokeMethod(Constants.invokeMethod)
                .messages(messages)
                .requestId(requestId)
                .tools(chatToolList)
                .toolChoice("auto")
                .build();
        ModelApiResponse invokeModelApiResp = client.invokeModelApi(chatCompletionRequest);
        ModelData data = invokeModelApiResp.getData();
        return data;
  • 而这里的信息实际上是一层层 抽丝剥茧 剥离出来的
    List<Choice> choices = data.getChoices();
    System.out.println("choices.toString() = " + choices.toString());
    for (Choice choice : choices) {
        ChatMessage message = choice.getMessage();
        System.out.println("message.getContent() = " + message.getContent());
        //本来这里想返回具体的信息类但是发现 上面的的那个ModelApiResponse类 也是一个 统一返回类型 也包含这 请求状态码 之类的定义
        return message;
    }
    return new ChatMessage();
    try {
        return mapper.writeValueAsString(invokeModelApiResp);
    } catch (JsonProcessingException e) {
            e.printStackTrace();
    }
    return mapper.writeValueAsString(new ModelApiResponse());    
  • 可以看到我的这段代码 有多个 return 所以这实际上是一段假 代码
  • 每一个return 实际上官方都 对应的 model 或者说 resoponse
  • controller 代码
    @PostMapping("/chat")
    public R chat(@RequestParam("content") String content) throws JsonProcessingException {
        /**
         * data 中的 choices 是一个 List<Choice> 类型但是实际上只有一个所以索性直接获取数组下标0的对象
         */
        logger.info(ChatAPIUtils.InvokeApi(content).getChoices().get(0).getMessage().getContent().toString());
        return R.ok().data("content", ChatAPIUtils.InvokeApi(content));
    }
  • 修改 由 service 层 调用 工具类
  • service 代码
  • service 接口
package com.codervibe.server.service;

import com.zhipu.oapi.service.v4.image.ImageResult;
import com.zhipu.oapi.service.v4.model.ModelData;

public interface ChatService {
    /**
     * AI 对话
     */
    ModelData AIdialogue(String content);

    /**
     * AI  画图
     */
    ImageResult AIcreateimage(String content);
}
  • service 接口实现

package com.codervibe.server.Impl;

import com.codervibe.server.service.ChatService;
import com.codervibe.utils.ChatAPIUtils;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.zhipu.oapi.service.v4.image.ImageResult;
import com.zhipu.oapi.service.v4.model.ModelData;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;

@Service("chatService")
public class ChatServiceImpl implements ChatService {
    Logger logger = LoggerFactory.getLogger(ChatServiceImpl.class);
    /**
     * AI 对话
     * @param content
     */
    @Override
    public ModelData AIdialogue(String content) {
        logger.info(ChatAPIUtils.InvokeApi(content).getChoices().get(0).getMessage().getContent().toString());
        return ChatAPIUtils.InvokeApi(content);
    }

    /**
     * AI  画图
     *
     * @param content
     */
    @Override
    public ImageResult AIcreateimage(String content) {
        logger.info(ChatAPIUtils.CreateImage(content).getData().get(0).getUrl());
        return ChatAPIUtils.CreateImage(content);
    }
}

  • controller 层调用 service
****package com.codervibe.web.controller;

import com.codervibe.server.service.ChatService;
import com.codervibe.utils.ChatAPIUtils;
import com.codervibe.web.common.response.R;
import com.fasterxml.jackson.core.JsonProcessingException;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.annotation.Resource;

@RestController
@RequestMapping("/chat")
public class ChatController {
    Logger logger = LoggerFactory.getLogger(ChatController.class);
    @Resource
    private ChatService chatService;
    @PostMapping("/content")
    public R chat(@RequestParam("content") String content) {
        return R.ok().data("content", chatService.AIdialogue(content));
    }
    @PostMapping("/AIcreateimage")
    public R AIcreateimage(@RequestParam("content") String content){
        return R.ok().data("image",chatService.AIcreateimage(content));
    }
}

  • 现在 虽然可以 和 AI 进行对话 但是 数据返回的速度实在是太慢 所以我打算 将 常见的问题和答案 存储在本地的数据库中以提升 数据返回的速度 这只是一个初步的想法
  • 最后的想法 还未实现 先这样
  • 粉丝群 企鹅 179469398

大家好,我是汤师爷~

今天聊聊开放平台架构设计。

为什么需要搭建开放平台

增强产品能力

开放平台能够让三方开发者和合作伙伴开发新的应用或服务,增加原有SaaS产品能力。这样就可以满足更多用户需求,从而提高用户的满意度和黏性。

促进创新

三方开发者能够在SaaS标准产品的基础上,创造新的解决方案,为平台带来创新的业务模式,这些可能为SaaS企业带来更多的盈利机会。

构建生态系统

开放平台能够建立一个以SaaS标准产品为中心的生态系统,吸引开发者、合作伙伴和其他相关方参加,共同构建一个互惠互利的生态圈。

降低开发和运营成本

通过邀请三方开发者来创造和扩展产品能力,他们可以有效分担SaaS企业的开发、运营成本,更聚焦于核心产品的优化和创新。

开放平台的服务对象是谁?

SaaS企业的开放平台通常包括以下关键用户角色:

第三方开发者

他们期望能快速入驻开放平台,并构建应用,通过用户订购应用获取收益。因此,他们需要API文档、SDK工具和开发者后台,帮助开发者构建、测试和部署应用,并且利用平台资源推广自己的应用。

定制客户

定制客户一般为拥有自研能力的企业客户,有定制化功能需求,例如与内部系统(如ERP、CRM)进行打通,实现企业自身的业务流程。

平台运营人员

平台运营人员需要为三方开放者和企业客户服务,帮助他们解决问题,因此,需要客户管理,应用申请流程管理、服务配置、参数配置、角色分配、财务对账管理等产品能力。

开放平台的运营流程

SaaS开放平台的运营流程涉及平台的管理和维护,为企业客户、三方开发者提供服务,包括吸引与管理三方开发者,提供必要的开发工具和支持,对开发者创建的应用进行审核和上线管理,通过数据监控和分析评估平台的健康度和用户活跃度,确保提供有效的服务支持,和维护平台的安全和合规性。

下图展示了开放平台的整体运营流程,实际的开放平台项目可以基于该流程做变更。

开放平台整体架构设计

管理后台

层针对不同角色,提供不同的管理后台:

  • 开发者后台:为三方开发者提供的工作台,包括应用管理、API接入、开发工具、数据分析和测试工具等。
  • 平台运营后台:用于平台运营团队管理整个系统的工作台,包括客户管理、权限控制、计费管理、系统监控等功能。
  • 商家后台:SaaS企业客户的后台系统,主要用于订购应用市场的三方应用,授权应用,并使用其提供的服务能力。

服务层

服务层为上层的管理后台提供核心服务能力:

  • 开发者接入:提供API文档、SDK工具、开发指南,应用的注册、管理等。
  • 运营管理:包括平台用户信息管理、权限设置、用户资料管理。对开发者提交的应用进行审核,确保应用的质量和安全性。管理平台计费、结算,收集和分析平台的运营数据。
  • 监控中心:包括服务器、应用、网络、数据库、安全、中间件和存储监控。这些功能确保开放平台的稳定性、性能和安全,通过实时监控、告警支持技术团队进行有效管理和维护。

API网关

API网关是整个开放平台的流量入口,它提供的能力确保了平台操作的安全、稳定和高效管理。

业务开放能力

业务开放能力由各个业务域系统提供,这些开放能力提供了核心业务数据/功能的交互能力。

开放能力设计

开放能力可以分为以下几种类型:

  • 前端扩展:开发者可创建个性化的前端H5/小程序页面,满足企业客户不同场景不同行业的需求。
  • API 接口/消息推送:API接口允许开发者通过定义的接口与平台系统交互,实现数据和功能的集成,例如商品创建接口。消息推送是指平台系统主动通知三方系统,如订单状态变更通知。
  • 后端扩展:开发者可以通过扩展点,自由嵌入自定义流程节点,构建个性化的业务逻辑。
  • 数据模型扩展:允许将三方系统的数据模型整合到平台系统中,在平台系统中可以查看或处理三方数据。

以商品系统为例,列出不同类型开放能力的使用场景:

  • 前端扩展:页面串联、定制商详组件、定制商品详情、定制B端管理页面。
  • API 接口/消息推送:商品发布接口、同步分店接口、查询商品详情接口、商品价格修改接口、商品修改接口、商品属性接口、商品上下架接口、商品类目接口、商品创建消息、商品变更消息。
  • 后端扩展:商品校验类扩展点(例如,商品创建时,校验商品编码是否符合定制需求的规范)、商品的定制信息计算扩展点(例如,通过外部接口计算出商品重量信息)。
  • 数据模型扩展:商品模型扩展并存储个性化的字段信息。

开放API设计原则

RESTful风格API

RESTful API 是一种遵循 REST 原则的 API 设计方式。REST 是一组约束条件和原则,由 Roy Fielding 在 2000 年的博士论文中提出。

RESTful API 的设计依赖于网络协议,主要是 HTTP,并且它使用 HTTP 的原生功能(比如 HTTP 的动词和状态码)来执行操作。以下是 RESTful API 的一些主要特点:

  • 面向资源:在REST架构中,所有内容都被视为"资源",每个"资源"都有一个独特的URI(统一资源标识符)。
  • 无状态:RESTful API不保存状态,这意味着每个请求都应包含执行请求所需的信息。服务器不会保存客户端的任何信息。
  • 统一接口:RESTful API应该有一个统一的接口,客户端和服务器基于接口交互,实现解耦。交互通常通过HTTP动词实现,如GET(获取资源)、POST(创建资源)、PUT(更新资源)、DELETE(删除资源)。

RESTful API的三个显著优势如下:

  • 它建立在HTTP协议上,协议简洁易用,得到了广泛的应用。
  • 接口设计以资源为中心,让接口易于理解和使用,比较直观。
  • 数据交换采用XML或JSON格式,大大简化了数据的处理和传输过程。

但严格遵循 RESTful API风格,也有一些缺陷:

HTTP协议的动词受限

当业务需求变得复杂时,仅依赖于HTTP的动词方法来对资源操作,可能不足以满足需求,这时往往需要通过接口名称来进一步区分。此外,一些特定的HTTP请求,如PUT和DELETE,可能会在网络传输过程中被某些防火墙设备拦截。

URL包含参数,可读性差

在URL中嵌入参数占位符(例如:GET /Api/Orders/{id}/OrderItems/{id})会降低其可读性。如果需要基于URL统计接口的调用次数,需要对具有相同URL的不同参数进行额外的处理。

HTTP状态码的表达性差

使用如20X、30X、4XX、5XX等标准的HTTP状态码,不足以描述复杂的业务场景的状态。

建议接口设计遵循以下准则:

  • 限制HTTP方法的使用,仅采用GET和POST。
  • 避免在URL中包含参数占位符,尽量使用URL的参数传参。
  • 使用自定义的业务状态码来提供更丰富的响应信息。

API分组原则

根据业务领域,对开放API进行分组。例如店铺API、商品API、库存API、订单API、物流API、客户API、营销API。

SaaS标准产品一般都基于DDD进行架构设计,根据业务领域组织开放API,是普遍采用的最佳实践。当需要改进或变更某个特定业务领域的功能时,开发人员可以直接找到相关的API组进行修改,不会影响到其他领域的API。

对于三方开发人员,可以更容易地找到与某个业务功能相关的API,因为它们通过业务域的划分逻辑组织在一起。

版本管理

为了统一和清晰地标识不同版本的相同接口,建议将版本号放置在接口路径的末尾,示例如下:

返回数据

每个接口的响应数据,应遵循统一的JSON或XML格式规范,并且至少应包含以下关键字段:

  • 状态码 (code):表示请求的总体结果,通常用于标识操作的成功或异常状态。
  • 消息 (msg):提供关于状态码的详细描述,以帮助理解请求的具体结果。
  • 数据 (data):包含与请求相关的具体业务信息和数据。
{
	"code":200,
	"msg":"OK",
	"data":{
	"item_id":"123456",
	"product_name":"奶油蛋糕"
	}
}

安全措施

接口签名

为开发者分配AccessKey(开发者标识,确保唯一)和SecretKey(用于接口加密,确保不易被穷举,生成算法不易被猜测)。

按照请求参数名的字母升序排列非空请求参数(包含AccessKey),使用URL键值对的格式(即key1=value1&key2=value2…)拼接成字符串A。

在字符串A最后拼接上Secretkey得到字符串B。

对字符串B进行MD5运算,得到Sign值。请求时,携带参数AccessKey和Sign,只有拥有合法的身份AccessKey和正确的签名Sign才能放行。这样就解决了身份验证和参数篡改问题,即使请求参数被劫持,由于获取不到SecretKey(仅作本地加密使用,不参与网络传输),也无法伪造合法的请求。

数据加密

敏感数据,如用户信息,应使用加密算法进行保护,常用的加密方法包括RSA和AES。

访问控制

在接口访问的API网关,应设置访问控制,仅允许来自被商家授权的白名单的请求。商家可以通过商家后台系统自主管理其白名单。

消息推送

消息推送是平台主动通知三方系统,提供数据更新的一种机制,满足三方系统对信息实时性的需求。例如,当商家成功创建订单后,三方系统可以通过订单查询接口来获取订单的当前状态。

三方系统若想实时获取订单状态,可以选择定时查询接口,但这样效率低并消耗大量资源。通过系统主动推送订单状态信息,可以有效地解决这一问题。但消息推送也带来了一些挑战:

  • 顺序性问题:订单可能经历多个状态,且这些状态在业务上有特定顺序。网络延迟可能导致状态送达三方系统时,顺序错乱,这时三方系统需要通过校验订单状态,判断变更是否符合业务逻辑,来确保订单状态的准确性。
  • 消息丢失风险:目前系统通常采用消息队列异步发送消息推送,尽管有消息中间件的机制确保消息的可靠性,但三方系统出现网络问题,仍有可能导致推送失败。解决方案:三方系统可以定期全量查询订单状态,对双边的订单状态进行对账处理,确保数据的完整性。

本文已收录于,我的技术网站:
tangshiye.cn
里面有,算法Leetcode详解,面试八股文、BAT面试真题、简历模版、架构设计,等经验分享。

游戏登录是玩家进入游戏世界的重要步骤,是玩家进入游戏后接触到的第一个可以交互的界面,它看似简单,但却意义重大。游戏登录不仅是进入游戏的必要步骤,也是保障游戏体验、数据安全和社区互动的关键环节。

HarmonyOS SDK
游戏服务
(Game Service Kit)主要提供快速、低成本构建游戏基本能力与游戏场景优化服务,有效提升游戏开发效率,帮助您进行游戏运营。游戏服务提供游戏登录能力,允许用户使用华为账号登录游戏,从而迅速推广游戏,共享华为庞大的用户价值。

游戏登录
包含使用
华为账号登录
、使用
游戏官方账号登录场景
。接入游戏登录后,游戏启动时会进行初始化,并向玩家展示联合登录面板,玩家可以选择任意一种方式进入游戏。

image
image

场景介绍

使用华为账号登录

转移场景

将玩家的游戏进度从HarmonyOS/EMUI系统渠道包转移至HarmonyOS NEXT系统包体,即将该华为账号与该游戏HarmonyOS/EMUI系统下渠道包对应的玩家标识ID的值,转移至该华为账号在该游戏HarmonyOS NEXT系统包体对应的玩家标识ID上。转移成功后,玩家可以使用华为账号进入HarmonyOS NEXT游戏,但无法再通过该华为账号进入HarmonyOS/EMUI游戏。
image
image
image

关联场景(快捷登录)

用户授权提供手机号,实现通过华为账号快速创建或关联一个游戏官方账号并进入游戏。

image
image
image

使用游戏官方账号登录

直接通过游戏官方账号登录游戏,用户体验和游戏官方包保持一致。

开发步骤

注:游戏登录涉及的具体步骤请参考
接入指南
,本文仅阐述关键开发步骤。

联合登录

初始化成功后,游戏可调用
unionLogin
接口进行联合登录,Game Service Kit向玩家展示联合登录弹框。

let context = getContext(this) as common.UIAbilityContext;
let thirdAccountInfo1: gamePlayer.ThirdAccountInfo = {
  'accountName': 'testName1', // 游戏开放给玩家接入的账号类型名字,例如"官方账号"、"xx账号"等,并不是具体某个玩家ID或开发者ID。
  'accountIcon': $r('app.media.icon') // 游戏官方账号图标资源信息
};
let request: gamePlayer.UnionLoginParam = {
  showLoginDialog: true,
  thirdAccountInfos: [
    thirdAccountInfo1    
  ]
};
try {
  gamePlayer.unionLogin(context, request).then((result: gamePlayer.UnionLoginResult) => {
    hilog.info(0x0000, 'testTag', `Succeeded in logining: ${result?.accountName}`);
  }).catch((error: BusinessError) => {
    hilog.error(0x0000, 'testTag', `Failed to login. Code: ${error.code}, message: ${error.message}`);
  });
} catch (error) {
  let err = error as BusinessError;
  hilog.error(0x0000, 'testTag', `Failed to login. Code: ${err.code}, message: ${err.message}`);
}

华为账号认证与授权

游戏判断accountName为"hw_account"时,可根据needBinding值选择通过Account Kit对应的创建授权/登录请求接口,获取用于服务器校验的Authorization Code信息。

需要华为玩家标识与游戏官方账号绑定(needBinding为true)

1.调用
createAuthorizationWithHuaweiIDRequest
创建授权请求并设置参数。

// 创建授权请求,并设置参数
let authRequest = new authentication.HuaweiIDProvider().createAuthorizationWithHuaweiIDRequest();
// 获取头像、昵称以及手机号授权需要传如下scope
authRequest.scopes = ['profile', 'phone'];
// 若开发者需要进行服务端开发,则需传如下permission获取authorizationCode
authRequest.permissions = ['serviceauthcode'];
// 用户是否需要登录授权,该值为true且用户未登录或未授权时,会拉起用户登录或授权页面
authRequest.forceAuthorization = true;
authRequest.state = util.generateRandomUUID();

2.调用
AuthenticationController
对象的
executeRequest
方法执行授权请求,并在Callback中处理授权结果,从授权结果中解析出头像昵称。

// 执行授权请求
try {
  let controller = new authentication.AuthenticationController(getContext(this));
  controller.executeRequest(authRequest, (err, data) => {
    if (err) {
      hilog.error(0x0000, 'testTag', `Failed to authenticate. Code: ${err.code}, message: ${err.message}`);
      return;
    }
    let authorizationWithHuaweiIDResponse = data as authentication.AuthorizationWithHuaweiIDResponse;
    let state = authorizationWithHuaweiIDResponse.state;
    if (state != undefined && authRequest.state != state) {
      hilog.error(0x0000, 'testTag', `Failed to authenticate. State is different.`);
      return;
    }
    hilog.info(0x0000, 'testTag', `Succeeded in authenticating.`);
    let authorizationWithHuaweiIDCredential = authorizationWithHuaweiIDResponse.data!;
    let avatarUri = authorizationWithHuaweiIDCredential.avatarUri;
    let nickName = authorizationWithHuaweiIDCredential.nickName;
    let authorizationCode = authorizationWithHuaweiIDCredential.authorizationCode;
    // 开发者处理vatarUri, nickName, authorizationCode信息
  });
} catch (error) {
  let err = error as BusinessError;
  hilog.error(0x0000, 'testTag', `Failed to authenticate. Code: ${err.code}, message: ${err.message}`);
}

不需要华为玩家标识与游戏官方账号绑定(即needBinding为false)

1.调用
createLoginWithHuaweiIDRequest
创建登录请求并设置参数。

// 创建登录请求,并设置参数
let loginRequest = new authentication.HuaweiIDProvider().createLoginWithHuaweiIDRequest();
// 当用户未登录华为账号时,是否强制拉起华为账号登录界面
loginRequest.forceLogin = true;
loginRequest.state = util.generateRandomUUID();

2.调用
AuthenticationController
对象的
executeRequest方法执行登录请求,并在Callback中处理登录结果,获取到Authorization Code。

// 执行登录请求
try {
  let controller = new authentication.AuthenticationController(getContext(this));
  controller.executeRequest(loginRequest, (err, data) => {
    if (err) {
      hilog.error(0x0000, 'testTag', `Failed to login. Code: ${err.code}, message: ${err.message}`);
      return;
    }
    let loginWithHuaweiIDResponse = data as authentication.LoginWithHuaweiIDResponse;
    let state = loginWithHuaweiIDResponse.state;
    if (state != undefined && loginRequest.state != state) {
      hilog.error(0x0000, 'testTag', `Failed to login. State is different.`);
      return;
    }
    hilog.info(0x0000, 'testTag', `Succeeded in logining.`);

    let loginWithHuaweiIDCredential = loginWithHuaweiIDResponse.data!;
    let authorizationCode = loginWithHuaweiIDCredential.authorizationCode;
    // 开发者处理authorizationCode
  });
} catch (error) {
  let err = error as BusinessError;
  hilog.error(0x0000, 'testTag', `Failed to login. Code: ${err.code}, message: ${err.message}`);
}

关联游戏官方账号

当联合登录接口获取的needBinding值为true时,游戏可调用
bindPlayer
接口绑定华为玩家标识teamPlayerId与游戏官方账号。

let context = getContext(this) as common.UIAbilityContext;
let thirdOpenId = '123xxxx'; // thirdOpenId表示游戏官方账号ID
let teamPlayerId = '456xxx'; // teamPlayerId表示玩家华为账号对应的teamPlayerId
try {
  gamePlayer.bindPlayer(context, thirdOpenId, teamPlayerId).then(() => {
    hilog.info(0x0000, 'testTag', `Succeeded in binding.`);
  }).catch((error: BusinessError) => {
    hilog.error(0x0000, 'testTag', `Failed to bind. Code: ${error.code}, message: ${error.message}`);
  });
} catch (error) {
  let err = error as BusinessError;
  hilog.error(0x0000, 'testTag', `Failed to bind. Code: ${err.code}, message: ${err.message}`);
}

未成年人防沉迷

调用
verifyLocalPlayer
接口进行账号实名认证和游戏防沉迷管控合规校验。

let context = getContext(this) as common.UIAbilityContext;
let request: gamePlayer.ThirdUserInfo = {
  thirdOpenId: '123xxxx', // 游戏官方账号ID
  isRealName: true // 玩家是否实名,该值为true时表示已实名,为false时表示未实名
};
try {
  gamePlayer.verifyLocalPlayer(context, request).then(() => {
    hilog.info(0x0000, 'testTag', `Succeeded in verifying.`);
  }).catch((error: BusinessError) => {
    hilog.error(0x0000, 'testTag', `Failed to verify. Code: ${error.code}, message: ${error.message}`);
  });
} catch (error) {
  let err = error as BusinessError;
  hilog.error(0x0000, 'testTag', `Failed to verify. Code: ${err.code}, message: ${err.message}`);
}

提交玩家角色信息

玩家成功登录游戏并选择角色、区服后,游戏需要调用
savePlayerRole
接口,将玩家角色信息上报至华为服务器。

let context = getContext(this) as common.UIAbilityContext;
let request: gamePlayer.GSKPlayerRole = {
  roleId: '123',   // 玩家角色ID,如游戏没有角色系统,请传入"0",务必不要传""和null。
  roleName: 'Jason', // 玩家角色名,如游戏没有角色系统,请传入"default",务必不要传""和null。
  serverId: '456',
  serverName: 'Zhangshan',
  gamePlayerId: '789', // 若是转移场景,请根据实际获取到的gamePlayerId传值。
  teamPlayerId: '345', // 若是关联场景,请根据实际获取到的teamPlayerId传值。
  thirdOpenId: '123'   // 若游戏官方账号登录,请根据实际获取到的thirdOpenId传值。
};
try {
  gamePlayer.savePlayerRole(context, request).then(() => {
     hilog.info(0x0000, 'testTag', `Succeeded in saving.`);
  });
} catch (error) {
  let err = error as BusinessError;
  hilog.error(0x0000, 'testTag', `Failed to save. Code: ${err.code}, message: ${err.message}`);
}

了解更多详情>>

访问
游戏服务联盟官网

获取
基础游戏服务功能开发指导文档